Case study
A process-and-decisions walkthrough of the CRM we run the studio on.
Vetd CRM
Process notes on building a typed, server-first CRM end-to-end.
- Year
- 2026
- Duration
- Ongoing
- Role
- Design, build, ship
- Category
- SaaS Products
001
Overview
How we approached it.
We built the studio on Next.js 16, server-first by default, typed end-to-end from the Postgres schema to the UI. This is a walkthrough of the stack, the decisions that shaped the codebase, and the process we used to get from empty repo to running system, without stacking services we didn't understand.
002
Constraints
The four rules we wrote down before the repo existed.
Four constraints shaped every technical decision. We wrote them down before the repo existed and pulled them back out whenever a choice got noisy.
01
End-to-end type safety, no manual sync
If the database schema changes, TypeScript should complain before the browser does. That ruled out any layer that accepts untyped payloads and any client that hand-writes types for server responses. The schema is the contract.
02
Server-first by default
Most pages shouldn't ship JavaScript for content that doesn't need it. React Server Components had to be the default rendering mode, not an opt-in optimization we'd sprinkle in later. Client components exist where interaction actually lives.
03
One codebase, one deployment
Public marketing site and protected app live side-by-side. Route groups handle the split; a single build pipeline ships both. No microservices, no split repos, no shared packages to keep in lockstep, until something earns the split.
04
Maintainable by one developer
Every abstraction had to pull its weight. No state management library until server actions hit a wall. No ORM when generated types already covered it. No framework we didn't already know well enough to debug at 2am.
“If the schema changes, TypeScript complains before the browser does.”
003
Decisions
Five places where we picked the boring, well-typed option and moved on.
Five decisions that shaped the codebase. Each one a place where we picked the boring, well-typed option and moved on.
01
Next.js 16 App Router, RSC-first
Server Components render by default; client components are the opt-in. Pages read from the database in their own body via async functions. No data-fetching library, no hydration dance for static content. Mutations go through server actions, which revalidate by tag or path and let affected routes re-render themselves.
02
Three Supabase clients, different doors
A browser client for client components, a server client bound to next/headers cookies for RSC and server actions, a middleware client for request-scoped cookie refresh, and a service-role admin client marked 'server-only' so it can't be imported from a client file. Each has a single purpose; picking the wrong one is a type error, not a runtime bug.
03
Generated types, never hand-written
Supabase's CLI generates TypeScript from the live schema into lib/supabase/types.ts. Every query, every mutation, every component imports from that file. A column rename breaks the build across every consumer at once. No drift, no stale fixtures, no guess-and-check.
04
shadcn + Tailwind v4 as the only UI layer
We vendor shadcn components into the repo and edit them directly. Tailwind v4 tokens live in CSS variables (OKLCH) so the design system is themable without rebuilds. No CSS-in-JS, no component library dependency, no runtime style resolution. One PR changes the primary color across the entire app.
05
Vercel Fluid Compute over serverless
Fluid Compute keeps warm instances across requests, so middleware auth and server-rendered pages don't pay cold-start costs the way classic serverless would. Fresh Supabase clients per request (no module-level singletons) means concurrent request isolation still holds up.
004
The stack
Companies, frameworks, and services behind the build.
Next.js
App Router, server actions, middleware auth
React
RSC-first, Suspense, streaming
TypeScript
Strict mode, generated Supabase types
Tailwind CSS
OKLCH tokens, dark-canonical theming
shadcn/ui
Vendored primitives, edited in-repo
Supabase
Postgres, Auth, RLS, SSR cookie plumbing
Stripe
Hosted Checkout + idempotent webhooks
Cal.com
HMAC-signed booking webhooks
Vercel
Fluid Compute, edge hosting, image optimization
005
What we wrote
The engineering artifacts that came out of the build.
Proxy middleware that refreshes Supabase sessions on every request
Three single-purpose Supabase clients (browser, server, admin) with 'server-only' guards
Generated Postgres types imported across every query and form
Server actions for every mutation, revalidated by tag or path
Shared shadcn + Tailwind design system across marketing and app
Route groups splitting public marketing and protected CRM, one build
React 19 streaming with Suspense boundaries on the slow data paths
Dark-canonical OKLCH tokens in CSS variables, themable without rebuilds
006
By the numbers
A quick snapshot of the codebase shape.
Core tables
10
Migrations shipped
35+
Runtime deps
< 40
Monthly tooling
$0
Have a project like this?
Book a 20-minute discovery call. You'll leave with a concrete scope, a timeline, and a price.