For the complete documentation index, see llms.txt. Prefer markdown by appending.mdto documentation URLs or sendingAccept: text/markdown.
Integrations
Verify webhook signatures, keep storage buckets private with short-lived signed URLs, and never ship third-party API keys to the client.
Most breaches in SaaS kits do not come from exotic crypto - they come from trusting an unverified webhook, a public bucket, or a leaked provider key. TurboStarter wires the common integrations to fail closed.
Billing webhooks
Every supported web billing provider verifies the request signature before applying subscription changes.
| Provider | Secret variable | Verification |
|---|---|---|
| Stripe | STRIPE_WEBHOOK_SECRET | stripe.webhooks.constructEvent |
| Lemon Squeezy | LEMON_SQUEEZY_SIGNING_SECRET | HMAC + timing-safe compare |
| Polar | POLAR_WEBHOOK_SECRET | SDK unwrap / signature check |
| Dodo Payments | DODO_PAYMENTS_WEBHOOK_KEY | SDK webhooks.unwrap |
The API mounts the active provider webhook without authentication middleware on purpose - the signature is the credential:
.post(`/webhook/${provider}`, (c) => webhookHandler(c.req.raw))Rules when you extend webhooks:
- Read the raw body for signature checks (do not re-serialize JSON first)
- Reject missing or invalid signatures with an error - never “best effort” sync
- Use environment-specific secrets (test vs live)
- Keep custom side effects inside the verified handler callbacks
webhookHandler(c.req.raw, {
onSubscriptionUpdated: async (subscriptionId) => {
// Safe: only runs after signature verification
},
});Billing webhooks
Customize handlers and callbacks per provider.
Billing troubleshooting
Fix signature mismatches and missed events.
Background task signatures
If you use QStash (or similar), verify the Upstash signature on every task route the same way you verify billing webhooks. An open cron endpoint is a free remote code trigger.
File storage
Storage credentials stay in @workspace/storage/server. The browser never sees AWS keys - it receives short-lived presigned URLs from authenticated API routes.
| Endpoint | Auth | Purpose |
|---|---|---|
GET /storage/upload | enforceAuth | Presigned PUT (expires in 60s) |
GET /storage/signed | enforceAuth | Presigned GET (expires in 1h) |
GET /storage/delete | enforceAuth | Presigned DELETE (expires in 60s) |
GET /storage/public | public | Public object URL helper |
Recommendations:
- Keep buckets private by default
- Prefer path prefixes scoped to
userId/organizationId - Treat a presigned URL like a temporary password - anyone with the URL can use it until it expires
- Configure CORS narrowly for your app origin
- Enable provider-side encryption when storing sensitive files
Managing files
Upload flows, permissions, and signed URL usage.
Third-party API keys
OpenAI, Resend, Stripe, OAuth client secrets, analytics write keys that grant privileged access - all of these belong in server env presets.
Patterns that stay safe:
- Call providers from Hono routers, Server Components, or background workers
- Return only the UI-facing result to the client
- For browser uploads to third parties, use short-lived tokens or signed URLs issued by your API - not the master key
Patterns that leak:
NEXT_PUBLIC_on a secret key- Passing
process.env.OPENAI_API_KEYinto a Client Component - Logging full webhook payloads or Authorization headers in production
Email and OAuth
- OAuth client secrets are server-only; only client ids that providers document as public may appear in the browser
- Configure production callback URLs carefully - open redirects in auth flows are a common footgun
- Keep transactional email templates free of secrets; confirmation links should expire and be single-use (Better Auth handles this for built-in flows)
Monitoring without leaking data
Error trackers (for example Sentry) are valuable, but scrub:
- Cookies and
Authorizationheaders - Raw billing webhook bodies
- Password, OTP, and recovery code fields
Ship enough context to debug; never enough to impersonate a user.
How is this guide?
Last updated on