PulseBoard is a production-grade SaaS analytics dashboard built with Next.js 16, Neon Postgres, and TanStack Query — giving product teams real-time visibility into revenue, user growth, and acquisition metrics. It features custom HMAC-based authentication, server-side filtered and paginated data from a live database, and a fully responsive UI with dark mode, skeleton loaders, and error boundaries.

PulseBoard is a full-stack SaaS analytics dashboard that surfaces the metrics product teams care about most — monthly recurring revenue, active users, conversion rate, and churn — in a clean, fast, production-ready interface. It demonstrates real-world frontend architecture: a live Postgres database, custom authentication, REST API layer, and a component system built for resilience and scale.
Analytics dashboards are one of the most common features in SaaS products, yet most portfolio implementations stop at static mock data and a few chart components. The goal was to build something closer to what an engineer would actually ship — with a real data layer, authentication, proper loading and error states, and UI patterns borrowed from products like Vercel, Stripe, and Linear.
The secondary challenge was staying current: the project deliberately targets Next.js 16 (with its breaking middleware → proxy rename), Tailwind CSS v4 (new @utility, @variant, and bg-linear-* APIs), and TanStack Query v5 — all of which have significant differences from their previous versions.
The project is structured as a feature-based vertical slice application with a strict client/server boundary:
fetch /api/...Authentication uses HMAC-SHA256 session tokens built on the Web Crypto API — no external auth library. Tokens are signed with a server secret, stored as httpOnly cookies, and verified in the Next.js proxy layer on every request before it reaches a route.
Filter, sort, and pagination state for the users table live entirely in the URL via useSearchParams, making every view deep-linkable and browser-history-aware. TanStack Query's keepPreviousData prevents the table from flickering to a skeleton on every page or filter change.
Recharts is loaded via next/dynamic with ssr: false, deferring ~250 kB from the initial bundle. Each major UI section is wrapped in a React error boundary so a failure in one widget never takes down the page.
| Technology | Reason |
|---|---|
| Next.js 16 (App Router, Turbopack) | Production framework with file-based routing, API routes, and proxy middleware |
| TypeScript 5 | End-to-end type safety from DB schema to component props |
| Tailwind CSS v4 | Utility-first styling with first-class custom animation utilities (@utility) |
| Neon (serverless Postgres) | Scalable, free-tier relational DB with HTTP driver suited to serverless environments |
| Drizzle ORM | Lightweight, type-safe SQL with schema-first migrations |
| TanStack Query v5 | Stale-while-revalidate caching, background refetch, and keepPreviousData |
| Recharts | Composable chart primitives with clean SVG output |
| Vitest | Fast unit testing with zero config for TypeScript projects |
| Vercel | Zero-config deployment with automatic preview deployments per branch |
1. Next.js 16 breaking changes
Next.js 16 renamed middleware.ts to proxy.ts and the exported function from middleware to proxy. The Edge runtime is no longer supported in the proxy layer — only Node.js. Debugging this required reading the framework's own bundled docs rather than relying on community resources that hadn't yet caught up.
2. Client/server boundary with TanStack Query
Initially, the TanStack Query hooks called the Drizzle service functions directly as queryFn. This silently bundled the Neon client into the browser bundle, where process.env.DATABASE_URL is undefined. The fix was enforcing the HTTP boundary — hooks always fetch the API routes, and the database code never leaves the server.
3. Tailwind CSS v4 utility authoring
Dark mode shimmer animations required CSS custom properties (--shimmer-base, --shimmer-highlight) that switch under @variant dark, consumed by a single @utility animate-shimmer. Tailwind v4's @utility names must be purely alphanumeric — no dark: prefix variants — which required a different approach from v3.
4. TypeScript strictness on Web Crypto types
The toBase64url helper accepted ArrayBuffer, but TextEncoder.encode() returns Uint8Array<ArrayBuffer> and fromBase64url returned Uint8Array<ArrayBufferLike> — both incompatible with crypto.subtle.verify's BufferSource parameter in strict mode. Resolved by normalising all byte buffers through an explicit ArrayBuffer slice before passing them to the Web Crypto API.