The stack I’m building everything on now.
TanStack Start, Convex, and Clerk — plus the specific architectural patterns from Theo Browne’s open-source Lawn repo that make them work together.
Picking a default
I’ve shipped enough side projects on enough different stacks at this point to have an opinion. Every new app starts with the same setup tax: routing decisions, auth wiring, data layer, deployment shape, where the design system lives. By the time the actual product logic shows up, I’ve already burned half a weekend on plumbing.
So I’m committing. Going forward, every new app I build starts on the same stack: TanStack Start + Convex + Clerk, deployed on Vercel, with the architectural patterns lifted almost wholesale from Theo Browne’s open-source Lawn repo.
This post is half “here’s the stack” and half “here are the specific patterns worth copying.” If you want a working reference instead of a starter template, Lawn is the best one I’ve read in a while.
The stack at a glance
TanStack Start
App framework + file-based router on Vite
Convex
Schema, auth-aware queries, actions, realtime, webhooks
Clerk
Identity, sessions, auth UI
Tailwind v4 + Radix
Styling and low-level UI primitives
Bun
Package manager + local task runner
Vercel
Static hosting for the SPA + prerendered marketing pages
Why this combination works: every layer has a clear job. Convex isn’t trying to be a frontend framework. TanStack Start isn’t trying to be a backend. Clerk isn’t trying to be an authorization layer. When the boundaries are clean, the integration stays clean too.
TanStack Start gives me file-based routing and Vite-based prerendering without dragging along a Next-sized framework. Convex gives me the entire backend in one place: schema, auth-aware queries, mutations, actions for external side effects, HTTP routes for webhooks, scheduled functions, and realtime subscriptions all behind the same client. Clerk handles identity well so I never need to build a sign-in page from scratch.
Where the credit goes
Most of what I’m describing here isn’t original. The architectural patterns are from Lawn, a fully working TanStack Start + Convex + Clerk app that Theo Browne and the Ping team published as open source. Lawn isn’t a starter template. It’s a real product they open-sourced as a reference implementation, and it’s rare to get a production app published with this much architectural care.
I’ve been studying it for the last few weeks and pulling the patterns I want to keep into a personal blueprint. This post is my notes on what’s worth lifting.
Route file conventions
Lawn enforces a discipline on route files that I’ve come around on. Each route is three files instead of one:
- The route entry (e.g.
app/routes/dashboard/$teamSlug.index.tsx) stays thin. It defines the path, sets head metadata, and hands off to an implementation component. - A hyphen-prefixed implementation file (e.g.
-team.tsx) holds the actual UI. It lives next to the route but isn’t itself a route, because TanStack Router ignores hyphen-prefixed siblings. - A
.data.tsfile (e.g.-team.data.ts) holds the route’s data contract.
Why this is worth doing: route files stop becoming 400-line dumping grounds. Big implementation code lives next to the route without polluting the file tree. The data contract is its own file, which makes prewarming and testing both clean.
The route data contract
This is probably the pattern I was most skeptical of going in and most converted on coming out.
Each major route gets a .data.ts file with three exports:
getEssentialSpecs()— the query specs the route needs to render.useData()— the hook the component uses to consume those queries.prewarm()— the function called when a user signals intent to navigate (hover, focus, touch).
What this gives you: navigation prewarming and route rendering share the same source of truth. You hover a link and the route’s data starts loading before the click fires. When the page mounts, the query is often already in cache, so the page renders instantly.
I’ve shipped apps before with manual prewarm calls scattered across components. It always rotted. The .data.ts pattern keeps the contract in one file, so the prewarm logic can’t drift from what the page actually needs.
Intent-based prewarming
The prewarm system is more sophisticated than the simple “prefetch on hover” pattern most teams use:
- Intent triggers come from
onMouseEnter,onFocus, andonTouchStart, with cancelation ononMouseLeaveandonBlur. - It’s debounced. A pointer brushing across links shouldn’t fire a query.
- It’s deduped. A short-term memory of recently prewarmed query keys prevents the app from resubscribing to the same data over and over.
- It’s a navigation primitive, not a card-grid optimization. Lawn prewarms breadcrumbs, headers, and shell elements, not just content cards.
The result is that navigation feels closer to native app speed than web app speed. Most clicks don’t have a loading state because the data was already on its way.
Convex as the domain boundary
Lawn doesn’t sprinkle business logic across server files. Everything authoritative lives in Convex.
- Authorization — reusable helpers like
requireUser,requireTeamAccess,requireProjectAccess. - Data access — Convex queries with auth checks built in.
- External integrations — Convex actions, isolated from queries so external failures don’t corrupt reads.
- Webhooks — a tiny
http.tsadapter layer that does signature verification and hands off to internal mutations.
Clerk owns identity. Convex owns authorization. The client never becomes the source of truth for either.
The reason this matters: when you keep authorization in Convex, you can’t accidentally ship a permission bug because someone forgot a check on a new route. The check is on the data, not on the page that displays the data.
Backend organized by domain, not function type
Convex modules get grouped by what they own:
- Domain modules — one file per core entity in your product, holding its queries and mutations together.
- Cross-cutting helpers —
auth.ts,security.ts, anything that’s shared between domains. - Integration modules — one file per external system, holding all the actions that talk to it.
http.ts— the webhook adapter, deliberately small. Verify the signature, hand off to an internal mutation, return a 200.
This sounds obvious but it’s the opposite of what most Convex examples do. The examples usually have one big functions.ts or split things into queries.ts / mutations.ts / actions.ts. That stops scaling around the third entity.
Why the combination, not any one piece
Take any one of these decisions in isolation and it’s defensible but not transformative. Static marketing pages are a known practice. Convex subscriptions are well-documented. Intent-based prewarming exists in plenty of stacks.
What makes Lawn (and now my new default) feel different is that all of these decisions agree with each other:
- Static marketing pages mean you don’t pay for SSR on routes that don’t benefit.
- A SPA product surface means Clerk auth and Convex subscriptions are first-class, instead of bolted on around an SSR layer.
- Direct Convex subscriptions mean you don’t need a query cache between the data and the UI.
- Route data contracts mean prewarming has somewhere to live that won’t rot.
- Domain-first Convex organization means the backend stays legible when you have eight tables instead of two.
Pick any one and it’s fine. Combine them with intent and the architecture compounds.
The blueprint, in one list
The patterns above are what every new project I start will share, no matter what it does:
app/+src/+convex/repo split.- Prerendered marketing surface, SPA product surface.
- Thin route files, hyphen-prefixed implementation files, companion
.data.tscontracts per major route. - Debounced, deduped, intent-based prewarming as a navigation primitive.
- Centralized Convex auth helpers; authorization on the data, not on the page.
- Domain-organized Convex modules + a tiny
http.tswebhook layer. - Plain
convex/reactfor reads, no extra cache layer.
Everything else — the schema, the visual language, the integrations — is per-project. The shape above is what stays constant.
Credit + resources
Massive credit to Theo Browne and the Ping team for publishing Lawn as open source. Most starter templates demonstrate one or two patterns. Lawn demonstrates a coherent set of decisions that work together, on a real product instead of a contrived demo.
If you’re starting a new app and any of this resonates, clone the repo and read it cover to cover. It’s the best modern web architecture reference I’ve found.
And if you build something with these patterns, I’d genuinely love to see it. Send it my way.