All postsHome
Engineering notes · April 17, 2026

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.

By Layken Varholdt~10 min read

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

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:

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:

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:

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.

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:

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:

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:

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.