Production Operations
Run preflight, migrations, scheduled credit grants, webhook replay, and upgrades safely.
Production safety depends on knowing what each command proves. Preflight, migration execution, application validation, and end-to-end acceptance are separate steps.
What preflight checks
Run the human-readable report with:
npm run preflightFor automation, request the same redacted report as JSON:
npm run --silent preflight -- --jsonPreflight checks:
DATABASE_URL,BETTER_AUTH_SECRET, and a non-localhostNEXT_PUBLIC_SITE_URL- completeness of each enabled capability, including payments, Google OAuth and One Tap, AI image generation, Turnstile, object storage, and email delivery
- a live PostgreSQL connection
- duplicate subscription, credit-source, and webhook-event rows that would block committed unique indexes
- full-repository TypeScript compilation
Secret values and credentials embedded in common connection strings are redacted from the report.
Preflight does not apply migrations, lint the source, run tests, or build the application. npm run validate is also narrower than a full gate: it runs the starter's current documentation validation, i18n validation, and source lint only.
Before promotion, use the repository's CI-equivalent checks appropriate to your changes, including type checking, unit and integration tests, lint, and a production build. Provider-backed payment acceptance additionally requires a real HTTPS webhook, a sandbox payment, and database read-back.
Migration-only database changes
Schema push is prohibited in every environment:
npm run db:pushThat command intentionally exits with an error. Use this workflow instead:
# Edit src/lib/db/schema.ts
npm run db:generate
# Review and commit the new drizzle/*.sql and drizzle/meta changes
npm run db:migrateThe production migration runner:
- creates an
app_migrationstracker table if needed; - reads committed
.sqlfiles directly fromdrizzle/; - applies unapplied files in lexical filename order;
- executes each SQL file and its tracker insert in one PostgreSQL transaction;
- stops immediately on tracker, SQL, or connection failure.
The runner does not provide down migrations or a distributed migration lock. Back up important data, test on a non-production database, run only one migration process against a database at a time, and use a forward-fix migration or a tested database restore if rollback is required. Never edit a migration after its filename has been recorded as applied.
The Drizzle journal is part of the generation and review history, but the production runner decides what to execute from SQL filenames and app_migrations.
Subscription credit scheduler
Vercel calls this route on the committed schedule:
5 0 * * *
GET /api/cron/subscription-creditsThe job runs daily at 00:05 UTC. It does not grant credits daily. It finds subscriptions whose independently stored monthly grant boundary is due and grants the configured monthly credits.
Set a strong server-only CRON_SECRET whenever subscription products are configured. Vercel sends it as:
Authorization: Bearer <CRON_SECRET>Route behavior:
| Result | Status |
|---|---|
CRON_SECRET is absent | 503 |
| Bearer token is absent or wrong | 401 |
| Grant processing fails | 500 |
| Run succeeds | 200 with grant and subscription counts |
Monthly grants are anchored to the subscription period, use a provider-neutral idempotency source, and expire at the next grant boundary or period end. A delayed run can catch up multiple boundaries, capped at 24 grants per subscription per run. Invoice webhooks update billing state; they do not directly issue these scheduled monthly grants.
After a run, inspect function logs and read back the affected subscriptions and credit_transactions rows. A 200 with zero grants can be correct when nothing is due.
Payment webhook operations
Stripe and Creem share one application endpoint:
POST /api/payments/webhookThe active PAYMENTS_PROVIDER selects the signature verifier for new webhook traffic. The handler verifies the untouched request body, normalizes provider events, and records delivery identity in webhook_inbox before applying billing changes.
Deliveries are deduplicated by provider plus event ID or payload hash. A successfully processed replay returns 200 with deduped: true. Processing uses a two-minute inbox lease; a failed handler releases the lease when possible, and an unprocessed expired lease can be reclaimed by a later delivery.
| Response | Meaning |
|---|---|
200, deduped: false | Delivery was accepted and processed |
200, deduped: true | Delivery was already processed or is currently owned by another attempt |
400 | Signature is missing or invalid |
503 | The selected provider is not completely configured |
500 | Verification succeeded but application processing failed |
When replaying a failed event:
- Confirm the endpoint and active provider match the sender.
- Confirm the signing secret belongs to that exact endpoint and test/live mode.
- Replay from the provider dashboard or development CLI without changing the payload.
- Check the HTTP response and application logs.
- Read back
webhook_inboxand the relevantsubscriptions,orders, orcredit_transactionsrows.
For local Stripe forwarding:
stripe listen --forward-to localhost:3000/api/payments/webhookUse the signing secret printed by that local listener only in the matching local environment.
Upgrading the starter
package.json is the canonical installed version. Upgrade customized projects deliberately:
- Read the changelog for every version being crossed.
- Commit or back up application code and database data.
- Apply the release changes on a separate branch instead of replacing customized files blindly.
- Run
npm ciwith the committed lockfile. - Review every new SQL migration and Drizzle journal entry.
- Run database changes only with
npm run db:migrate. - Run
npm run starter:checkand the tests that cover your modifications. - Deploy and verify a non-production environment before production.
Skipped major versions and buyer modifications can require manual integration. The starter does not automatically merge upstream changes into a customized application.