Skip to content
Docs

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.ts is a pure function from a plan to the parameters, so it is unit-tested without a network call.
  • Customer portal. /api/portal sends 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/dodo runs 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 a webhook_events table 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 for active and past_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 the dodopayments SDK or knows what a raw Dodo payload looks like. Everything else depends on the BillingProvider interface. 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.