What's inside the Web Kit
A full contents tour of the kit you are buying: the billing layer, auth, database, email, content pipeline, dashboard, tests and the agent-ready layer.
This page is the honest inventory. Every item below exists in the repository you get access to, and the file paths are real: you can check them against your own clone within a minute of accepting the invitation.
One thing to get straight first, because it confuses people who look around this site for it. The store you are reading is a copy of the kit with the accounts stripped out. It sells to anonymous buyers, so it has no sign-in page and no dashboard. Your copy is the full one, and it ships both.
Billing, which is why the kit exists
The billing layer is the largest and most carefully tested part of the kit, and it is the part most likely to lose you money if you write it yourself in an afternoon.
- Checkout.
GET /api/checkout?plan=<id>resolves the plan to a Dodo product id from an environment variable and redirects to Dodo's hosted checkout.src/lib/billing/checkout.tsis a pure function from a plan to the parameters, so it is unit-tested without a network call. - Customer portal.
/api/portalsends an existing customer to Dodo to manage or cancel their own subscription, so you are not building a billing settings screen. - Webhooks that verify.
POST /api/webhooks/dodoruns the request through three guards before anything writes: the signature is checked and a forged request gets a 401, the event id is checked against awebhook_eventstable and a replay gets a 200 without running your fulfilment twice, and the payload is normalised into one internal shape. Only then does the pipeline branch on what happened. - A subscription state machine. Every subscription resolves to one of
five statuses:
incomplete,active,past_due,cancelled,expired.hasAccess()returns true foractiveandpast_due, which is the detail that separates a working integration from one that logs out paying customers on the day their card fails and Dodo is still retrying. - Plan gating in two shapes.
requirePlan('pro')at the top of a page redirects when the plan is not met.meetsPlan()branches inline when only one panel on the page should change. Both are dunning-aware, so they agree with the state machine above rather than reimplementing it. - One-time purchase entitlements. A single payment grants the same entitlement a subscription does, resolved through the same gate. You can sell a lifetime deal and a monthly plan without writing a second code path. This store sells one-time purchases through exactly that path.
- A provider adapter, not a Dodo tangle. Nothing outside
src/lib/billing/providers/dodo/imports thedodopaymentsSDK or knows what a raw Dodo payload looks like. Everything else depends on theBillingProviderinterface. That seam is why the state machine and the webhook pipeline are testable against fakes with no live Dodo account, and it is where a second provider would slot in.
Auth and the gated dashboard
better-auth with two ways in: Google OAuth and
email magic links. Session helpers built for the App Router, so getUser()
works inside a server component and requireUser() redirects before anything
renders. An optimistic cookie-presence redirect in src/proxy.ts (Next 16's
replacement for middleware.ts) keeps signed-out visitors from seeing a
flash of the dashboard, backed by a real session check in the layout.
On top of that sits a dashboard shell: a plan badge, a live setup checklist that reads which environment variables you have filled in, a billing page that shows the current subscription and links to the portal, and a worked example rendering both gating shapes side by side with the code that produces them.
Database
Drizzle ORM against Neon Postgres. The schema lives in src/db/schema/,
generated SQL migrations live in drizzle/, and the client in
src/db/index.ts is constructed lazily so importing it is safe with no
DATABASE_URL set. The billing tables cover customers, subscriptions,
one-time purchases, processed webhook events and a waitlist.
Transactional email
Four React Email templates: welcome, magic link, receipt and subscription
status. They render through Resend, and they are wired to the events that
should trigger them rather than left as files you have to find a use for.
With no RESEND_API_KEY set, every send prints a readable preview to the
server log instead of silently vanishing, which is what you want in local
dev and in a zero-env preview deploy.
npm run email:dev opens a live preview server over the templates.
The receipt template carries a QR code rendered inline as an SVG data URI via
@kroszborg/rune, so there is no image host and no external request from
somebody's mail client.
Blog, docs and SEO
MDX content under content/blog/ and content/docs/, both behind one
frontmatter contract validated by a Zod schema. A generated sitemap.ts that
picks up new posts without being told, per-route metadata, robots.ts, and
Open Graph images rendered at request time by next/og, including a distinct
card per blog post. Docs read in a manual order you control rather than by
file date.
The pages you are reading right now are that pipeline.
Analytics
PostHog for product analytics and a Cloudflare Web Analytics beacon. Both are env-gated and both are genuinely silent until you add a key: no script loads and no request leaves the browser.
Tests
81 tests, Vitest, no live services required. Everything runs against
fakes and pure functions, so npm test works on a fresh clone with an empty
.env.local.
The suite is weighted toward the money path on purpose. The webhook handler tests cover bad signatures, duplicate event ids and every event kind the pipeline handles. Alongside them: the subscription state machine, Dodo payload normalisation, checkout parameter building, plan gating from both subscriptions and one-time purchases, and every email template rendering to non-empty HTML with the right content in it.
It boots with zero environment variables
Clone the repo, npm install, npm run dev, and the whole site renders with
nothing configured. Auth is off, billing is off, email prints to the console.
Nothing throws. npm run build succeeds the same way, which means a preview
deploy works before you have signed up for a single service.
The rule that keeps it true: no feature reads its environment variable at
module scope. Clients are constructed lazily inside functions, and every
gated surface checks a flag in src/lib/env.ts before doing the gated thing.
Importing a module is always safe. You turn features on one at a time, in
whatever order your product needs them.
Agent-ready
Point a coding agent at the repo and it reads AGENTS.md first (CLAUDE.md
just includes it): an architecture map of every directory, the conventions
that bind, and named recipes for the changes you will actually make. Add a
plan, handle a new webhook event, add an email, gate a feature, add an OAuth
provider, swap the accent colour, swap the font, add a blog post.
For UI work there is a bundled Claude Code skill at
.claude/skills/kit-design/, encoding the kit's design tokens, type scale,
spacing, motion and copy voice, so a screen your agent builds next still
looks like the rest of the product instead of drifting into generic output.
The stack, in one list
Next.js 16 (App Router), React 19, TypeScript in strict mode, Tailwind v4,
better-auth, Drizzle on Neon Postgres, Dodo Payments, React Email on Resend,
MDX via next-mdx-remote, Zod, PostHog, Vitest. No component library to
fight, and no service in that list you could not swap out.
What it is not
It is not a low-code product and it does not guess your business logic. It ships the parts that are the same for almost every paid product and gets out of the way for the parts that make yours yours.