Compose your own

Builder

Toggle modules, pick a design, preview live, submit for delivery.

CRMon
Bookingon
Invoicingon
Reviewsoff
Loyaltyoff
Open builder
A la carte

Modules

18 individual operational tools at $29-$49 each. Mix and match.

See all
Visual identity

Templates

11 aesthetics. Same kit, different look. Compose orthogonally.

See all
Free forever · MIT

The three free cores

Each is a Vercel-deployable Next.js project.Browse all cores →

The 12-Point Launch-Readiness Gate

The twelve backend checks that decide whether a launch holds under real traffic, drawn from shipped engineering: auth, RLS, webhooks, rate limits, error shape, email deliverability, and fulfillment retry.

12 min readFree, one email to unlock the full guide + download

Launch-readiness is not about whether the happy path works. On launch day the happy path always works, because that is the only path you tested. Readiness is about what happens when a webhook arrives twice, when two requests hit the same counter, when a token is forged, when the email provider bounces, when someone hammers an endpoint. These are not rare events. They are day one.

This gate is twelve backend checks drawn from real engineering, not a generic wishlist. Each one maps to code that exists in the LaunchKits core: the auth wrapper, the rate limiter, the error contract, the webhook idempotency layer. Run the gate before you launch and you close the failures that otherwise surface at the worst possible time, in production, in front of your first real users.

The gate, in one screen

  1. 1RLS is enabled on every tenant table, with org-scoped policies.
  2. 2Every API route verifies the auth token server-side and never trusts a client-sent identity.
  3. 3Rate limiting is in place, with a documented single-instance limitation if it is in-memory.
  4. 4Every response uses one structured error shape with a code and an HTTP status.
  5. 5Internal error messages are never leaked to clients in production.
  6. 6Webhook signatures are verified before the payload is trusted.
  7. 7Webhooks are idempotent: the same event id is never processed twice.
  8. 8Webhook failures are logged with enough context to replay them.
  9. 9Fulfillment is retryable and idempotent, tracked by an explicit status.
  10. 10Email sends from a verified domain, with delivery outcomes logged.
  11. 11Shared counters increment atomically, never read-modify-write.
  12. 12Secrets are server-only. The service key is never NEXT_PUBLIC_ or in the bundle.

The preview gives you the list. The full gate below gives you the how and the failure mode behind each item, with the real code patterns. Drop your email to unlock all twelve with the engineering detail, and download the gate as a Markdown checklist.

1 to 2: identity and the wall

The first two are the ones that turn a bug into a breach. RLS on every tenant table is covered in depth in the Multi-Tenant RLS Security Checklist; the short version is that a table without it is public through your anon key. The second is just as load-bearing and easier to get wrong: never trust a user id that arrived from the client.

// Resolve identity from the token, server-side, on every route.
const token = req.headers.get('authorization')?.replace('Bearer ', '')
if (!token) return errorResponse(ErrorCode.UNAUTHORIZED, 'Missing token')

const { data: { user }, error } = await supabase.auth.getUser(token)
if (error || !user) return errorResponse(ErrorCode.UNAUTHORIZED, 'Invalid token')
// user.id is now trustworthy. A client-sent userId in the body is not.
Trap: The trusted client id

The classic hole: the route reads userId from the request body and queries with it. An attacker sends someone else’s id and reads their data. The fix is to only ever derive identity from the verified token, and treat any id in the request body as a claim to be checked, never as fact.

3 to 5: the request contract

Three things make an API behave under load: it limits abuse, it fails in a shape clients can handle, and it never leaks its own internals. All three live in one wrapper around every route.

// Rate limit, then hand a structured 429 with Retry-After.
const limit = rateLimit(getRateLimitKey(req, user.id), 60, 60_000)
if (!limit.allowed) {
  return NextResponse.json(
    { error: { code: 'RATE_LIMITED', message: 'Too many requests.' } },
    { status: 429, headers: { 'Retry-After': String(retrySeconds) } },
  )
}
  • Rate limiting: a per-user or per-IP sliding window. If it is in-memory it works for a single instance only; write that limitation down so the day you scale out you know to move to a shared store.
  • Error shape: one contract, { error: { code, message } }, with an enum of codes mapped to HTTP statuses (UNAUTHORIZED to 401, LIMIT_EXCEEDED to 429, and so on). Clients can branch on code instead of parsing prose.
  • No leaked internals: return the raw error message only in development. In production, return a generic message and log the detail server-side. A stack trace in a client response is a gift to an attacker.
// Production hides the internals; development shows them.
return errorResponse(
  ErrorCode.INTERNAL,
  process.env.NODE_ENV === 'development' ? message : 'Internal server error',
)

6 to 8: webhooks, the part that gets skipped

Webhooks are where launches quietly break, because the failure only appears under the exact conditions you did not simulate: a retry, a replay, a forged call. Three checks cover it.

1Verify the signature first

Before you read a single field, verify the provider’s signature against the raw request body. An unverified webhook endpoint is an unauthenticated write endpoint that anyone on the internet can call with a fake payload.

2Dedupe by event id

Providers retry. The same event will arrive more than once. Record processed event ids and check before acting, so a retried checkout event does not fulfill the order twice or double-count usage.

3Log failures with the payload

When processing throws, write the event id, type, error, and raw payload to a failure log so you can replay it instead of losing it. A dropped webhook is a lost order with no trail.

// Idempotency guard: check, then process, then mark.
if (await isEventProcessed(event.id)) return ok() // already handled
try {
  await handleEvent(event)
  await markEventProcessed(event.id, event.type)
} catch (err) {
  await logWebhookFailure(event.id, event.type, String(err), event)
  throw err // let the provider retry; the failure is now recoverable
}
Trap: Process-then-mark, or mark-then-process?

Mark an event processed before the work succeeds and a mid-flight crash loses it forever, because the retry sees it as done. Process first, then mark, and log any failure so a crash leaves the event replayable. Order matters here: the mark is a commit, and you only commit after the work holds.

9 to 10: money and mail actually arriving

The two checks that decide whether a paying customer gets what they paid for. Both fail silently, which is why they need explicit tracking.

  • Fulfillment retry: the step that delivers the thing (grants access, provisions the account, sends the license) must be idempotent and retryable, tracked by an explicit delivery status. If it fails, it retries without double-delivering; if it is stuck, the status tells you which orders to chase.
  • Email deliverability: send from a verified domain with SPF, DKIM, and DMARC aligned, so your mail lands instead of spam-foldering. Log every send with its outcome (sent, failed, bounced) so a deliverability problem is visible in a table, not inferred from silence.
Note: Deliverability is DNS, not copy

A welcome email that never arrives is not a content problem, it is a DNS and sender-reputation problem. Verify the domain with your email provider and confirm SPF, DKIM, and DMARC before launch. Then send yourself a test to a Gmail, an Outlook, and a corporate inbox and confirm placement, because they judge you differently.

11 to 12: concurrency and secrets

  • Atomic counters: any shared counter (usage, seats, credits) must be incremented in a single UPDATE ... SET col = col + n or an RPC, never a read-then-write in application code, which races and undercounts under concurrent load.
  • Server-only secrets: the service_role key bypasses RLS entirely, so it must live in server-only env, never in a NEXT_PUBLIC_ variable, never in a client component, never in the shipped bundle. Grep the build output for it before you launch.
Fix: The launch-eve pass

The night before, do three concrete things. Log in as a second tenant and confirm you cannot read the first. Fire a duplicate webhook and confirm it processes once. Grep your client bundle for the service key and confirm it is absent. If those three hold, the gate holds.

Free · one email

Read the rest and take it with you

The full guide plus a downloadable Markdown copy. One email, no wall on the preview above, unsubscribe any time. You also get the occasional build note when we ship something worth your inbox.

Why LaunchKits wrote this

This is the standard our own kits ship against

The patterns in this guide are not theory. They are the org-scoped RLS, atomic counters, and webhook idempotency that ship in every LaunchKits module by default. If you would rather own that foundation than rebuild it, start with the free open-source cores.