Runbooks
Operational runbooks for common maintenance tasks.
Regenerate DB Actions
bun run generate:actionsRun this after Drizzle schema changes. Commit generated files with the schema change so TypeScript and authz contract tests see the same database action surface as runtime.
Change Production Domain
Yayaw production is canonically served from https://yayaw.app.
Point the apex DNS records at the self-hosted Coolify/reverse-proxy ingress and verify TLS for
yayaw.app.Configure
www.yayaw.appat the reverse proxy to redirect toyayaw.appwith status308.Keep retired
.eudomains only as reverse-proxy redirects with status308: routeyayaw.euandwww.yayaw.eutoyayaw.app. Companion projects should follow the same pattern, for exampletable.yayaw.eutotable.yayaw.app.Set production environment variables:
NEXT_PUBLIC_BASE_URL=https://yayaw.app
BETTER_AUTH_TRUSTED_ORIGINS=https://*.yayaw.appKeep a durable
previewGit branch, routepreview.yayaw.appto its branch-backed Coolify application, and set previewNEXT_PUBLIC_BASE_URL:
NEXT_PUBLIC_BASE_URL=https://preview.yayaw.appDo not keep retired preview hosts as active preview domains after
preview.yayaw.appis verified.Redeploy the latest Coolify production and preview applications so builds receive the public base URL.
Update third-party callbacks and webhooks that store absolute origins, such as OAuth providers, Stripe webhooks, billing portal return URLs, email links, and analytics/site settings.
Deploy Self-Hosted Runtime
Copy the example file and fill production secrets outside git:
cp .env.self-host.example .env.self-host
perl -0pi -e "s/^BETTER_AUTH_SECRET=$/BETTER_AUTH_SECRET=$(openssl rand -hex 32)/m" .env.self-hostRun the schema sync and seed setup:
docker compose --env-file .env.self-host -f docker-compose.self-host.yml --profile setup run --rm migrateStart or update the runtime:
docker compose --env-file .env.self-host -f docker-compose.self-host.yml up --build -dConfirm Caddy can reach the app and that
Host/X-Forwarded-*headers are preserved by opening the configuredDEPLOYMENT_URL.Upload a media asset and confirm it is served from the configured
STORAGE_PUBLIC_BASE_URL.Confirm
bun run worker:page-aiis running through the composeworkerservice whenPAGE_AI_QUEUE_DRIVER=db-worker.Back up Postgres and object storage together before every risky release.
Cut Over an Existing Database to Better Auth 1.7
Migration 0050_better_auth_1_7_stable is additive for rollback, but it changes
account identity and adds uniqueness constraints. The migration process refuses
to run it against an existing database until the operator attests that auth
writes are actually paused. BETTER_AUTH_17_WRITES_PAUSED does not block a
single request by itself and must never be used as a substitute for ingress or
process isolation.
Rehearse on a recent production restore. First expose the additive preparation columns, then run the preflight and resolve every blocker before scheduling production:
DATABASE_URL=<restored-clone-url> \
bun src/lib/scripts/auth/better-auth-1-7-prepare.ts
DATABASE_URL=<restored-clone-url> \
bun src/lib/scripts/auth/better-auth-1-7-preflight.tsThe preparation command is idempotent and migration 0050 repeats the same
IF NOT EXISTS columns. Run it on production before the outage window so
every existing client_credentials client can receive an explicitly
reviewed client_credentials_scopes allowlist and, when ownership is
ambiguous, an organization reference_id. Allowed machine scopes are only
control-plane:read, control-plane:write, control-plane:publish, and
control-plane:admin; never copy openid, profile, email, offline access,
or another user-delegated scope. Existing client IDs and secrets remain
unchanged. The deployment migration path reruns preparation idempotently
before the blocking preflight.
Immediately before cutover, take a new Postgres snapshot or
pg_dumpand prove that it can be restored. Record the immutable app and worker image tags used for rollback.Block every external method on
/api/authat the ingress with a maintenance response. Ensure no alternate public origin can bypass that rule. On a single-host Compose deployment, stopping the oldappservice is an acceptable broader outage. Do not set the attestation yet.Verify the block from outside the deployment network. For an ingress rule, this harmless invalid request must return the maintenance status, normally
503, rather than a Better Auth2xxor validation4xx:
curl --include --request POST \
--header 'content-type: application/json' \
--data '{}' \
https://yayaw.app/api/auth/sign-in/emailOnly after the backup and external block are verified, set
BETTER_AUTH_17_WRITES_PAUSED=trueon the migration process. It is a short-lived, non-secret operator attestation:
# Self-host: set the value temporarily in the untracked .env.self-host file.
docker compose --env-file .env.self-host \
-f docker-compose.self-host.yml --profile setup run --rm migrateFor the Coolify rolling runner, set the GitHub production environment
variable BETTER_AUTH_17_WRITES_PAUSED to true, then run or rerun the
deployment. The runner transfers only the normalized boolean to its private,
mode-600 migration env file and does not print it. The guard distinguishes
a fresh database, a database where 0050 is already applied, and an existing
pending database. Both existing states execute the full Better Auth data
preflight before db:migrate. The pending state requires the attestation;
an applied database also requires it when the currently serving app does not
advertise the stable-1.7 readiness contract, which closes a retry after a
deliberate beta rollback. For the legacy
docker-compose.coolify.yml resource, set the same temporary non-secret
Compose variable in Coolify only after the external block is live, then
remove it after the migration service completes.
6. Deploy the Better Auth server and UI together, then run the critical auth,
OAuth, SCIM, Stripe, invitation, and passkey smoke tests while the external
write block remains in place for normal users. The rolling script verifies
the anonymous Better Auth session endpoint from inside the newly deployed
app container while paused; it never weakens the public /api/auth rule for
this smoke.
7. Immediately set BETTER_AUTH_17_WRITES_PAUSED=false or remove the variable
from the migration environment, then remove the ingress block and reopen
auth traffic. Leaving the attestation enabled is an operational error.
8. If application rollback is required, redeploy the old server and UI together
without reverting migration 0050. Disable the enable-scim-plugin flag
before the beta server boots: migration 0050 preserves the empty beta SCIM
tables under rollback-only names, while the stable tables at the public
names have an intentionally incompatible shape. Before another upgrade
attempt, pause auth writes again, replay the reviewed issuer/account-key
backfill from migration 0050, verify every machine OAuth client still has a
user owner and organization membership reference, and rerun the preflight.
The beta may have created accounts with a null issuer or OAuth clients with
incomplete additive metadata during the rollback window. Every later
deployment with 0050 already recorded still runs the preflight and fails
closed while repair work remains.
After migration 0050, the Coolify runner refuses to automatically boot a
previous image that does not advertise the stable Better Auth 1.7 readiness
contract. A failed first cutover therefore stops for operator intervention;
keep the external /api/auth block active. For a deliberate beta restore,
set enable-scim-plugin=false in both the managed database flag and any
remote flag provider, restart beta behind the ingress block, and verify from
the private network that
/api/auth/scim/v2/ServiceProviderConfig is unavailable before reopening
traffic. Automatic rollback becomes available again once the previous live
image advertises the stable-1.7 rollback contract.
Fresh databases and clean databases that already record migration 0050 while a
stable-1.7 app is serving do not require the one-time write-pause attestation.
An applied database behind a beta or unknown runtime requires the pause and the
external ingress proof again. Migration-history drift, partial schema state, or
rollback-created account repair work always fails closed and must be resolved
on a restored clone first.
Update Seeded Feature Flags
Edit
src/lib/scripts/seed.ts.Apply the seed:
bun run seedRe-run app quality checks:
bun run check
bunx tsc --noEmit
bun run buildRetire the legacy Table documentation mirror
Public documentation is served from the bilingual documentation-page catalog
at /[locale]/docs/*. The former Page-CMS pages under /table/docs are obsolete:
permanent redirects preserve incoming links. Never recreate or republish this
mirror with migrate-table-pages-to-cms.ts.
To retire an existing mirror safely:
Inventory only global pages whose path is
/table/docsor starts with/table/docs/. Keep the marketing/tableand example/table/examplepages.Verify every destination under
/en/docs/tableand/fr/docs/tableis published and that each old URL redirects to its replacement.Review each page's latest and published revision IDs. A newer editorial draft needs review before retirement; do not discard it automatically.
Archive the reviewed page through
yayaw_pages_archive, first withdryRun, then with its exactexpectedRevisionId,confirm: true, and a reason. Archiving retains the immutable revision history.Recheck redirects and the canonical documentation after archiving. Keep the archive audit receipts. Do not clear or delete canonical documentation entries.
For current documentation, use /dashboard/content/documentation to review
English and French together, resolve validation errors and publish the reviewed
revision. Draft status alone is not evidence that a page is obsolete.
Maintain the Yayaw Table Fumadocs Fallback Snapshot
Fumadocs renders the published bilingual Documentation catalog from Postgres at
/[locale]/docs/table/*. The files in content/docs/{en,fr}/table remain only
as the initial seed and explicit rollback/fallback snapshot; they are not the
routine editorial source after the database cutover. Super Admins manage live
English and French content from /dashboard/content/documentation.
When intentionally refreshing the fallback snapshot, update both directories,
use relative links such as ./setup, and keep the English and French
meta.json files in the same order. Run bun run docs:check-translations and
bun run docs:check-links, then use the explicit Documentation import command
only when the reviewed snapshot must replace untouched Git-imported revisions.
Validate Billing Webhooks in Staging
Better Auth Stripe plugin webhook:
stripe listen --forward-to https://<staging-domain>/api/auth/stripe/webhookCustom one-time webhook:
stripe listen --forward-to https://<staging-domain>/api/billing/stripe/webhookTrigger events:
stripe trigger checkout.session.completed
stripe trigger customer.subscription.updated
stripe trigger customer.subscription.deleted
stripe trigger invoice.payment_failed
stripe trigger invoice.paidinvoice.payment_failed and invoice.paid must reach the Better Auth Stripe
plugin endpoint at /api/auth/stripe/webhook. The custom one-time endpoint at
/api/billing/stripe/webhook intentionally processes
checkout.session.completed only.
Replay Failed One-Time Webhooks
When one-time webhook processing fails due to a transient issue, replay failed events:
bun run billing:replay-webhooksOptional limit:
bun run billing:replay-webhooks -- --limit=20Regenerate Fumadocs Source Artifacts
bun run docs:generateRegenerate LLM Assistant Files
bun run docs:llm:generateRun this after changing content/llm/llm-source.md. Do not edit AGENTS.md,
GEMINI.md, or .github/copilot-instructions.md manually.
Run a Full English Docs Pass
Use this when making broad documentation changes:
bun run docs:generate
bun run docs:check-links
bun run docs:check-translations
bun run docs:llm:generate
bun run docs:llm:check
bun run checkIf the docs change affects routes, imports, or MDX rendering, also run:
bunx tsc --noEmit
bun run buildKeep English docs canonical. When localized documentation is in scope, update
the French mirror in the same PR and keep docs:check-translations passing.
Manage Production MCP Keys
Issue a production MCP key for a user:
bun run mcp:key -- issue --email [email protected] --permissions read,write,publish,adminRevoke a production MCP key:
bun run mcp:key -- revoke --key-id <api-key-id>Rotate keys by issuing the replacement first, updating the client secret, verifying yayaw_status, then revoking the old key.
Validate Documentation Integrity
bun run docs:check-links
bun run docs:check-translations
bun run docs:llm:checkIf docs:llm:check fails, regenerate with bun run docs:llm:generate and
review the generated assistant-file diff before committing.
Verify GitHub Code Access
Confirm
/dashboard/admin/billing-settingshas GitHub repository access enabled.Confirm repository, GitHub App ID, and installation ID are present.
Confirm
BILLING_CODE_ACCESS_GITHUB_APP_PRIVATE_KEYexists in the target deployment environment.Use a paid test organization with
code-access:read.Submit a GitHub username from
/dashboard/organization/code-access.Confirm
code_access_github_accountsrecords the invitation or active access state.Confirm GitHub shows the repository invitation with read-only access.
For staging without a GitHub App, use BILLING_CODE_ACCESS_GITHUB_TOKEN only as
an explicit temporary fallback.
Verify Media Storage
Confirm
STORAGE_PROVIDER=s3is configured with S3/MinIO keys andSTORAGE_PUBLIC_BASE_URL.Upload an image from
/dashboard/content/media.Confirm a
media_assetsrow is created for the active organization.Confirm the public URL loads.
Open the page editor and bind the asset to an image field.
Publish the page and confirm the public page renders the selected asset.
If thumbnails are enabled for the asset type, confirm a thumbnail URL is created or a non-blocking thumbnail failure is recorded.
Verify Manual Public Domain DNS
Set
PUBLIC_DOMAIN_PROVIDER=manual-dns.Configure
PUBLIC_DOMAIN_CNAME_TARGETorPUBLIC_DOMAIN_IPV4_TARGETS.Add a domain from organization settings or
yayaw_org_domain_add.Publish the TXT challenge shown in the dashboard.
Point the hostname to the reverse proxy.
Run the check/verify action and confirm the domain becomes
verified.Open the custom host and confirm public pages render while
/dashboard,/auth,/api,/docs,/o, and/ingeststay blocked.
Reset Local Database
bun run db:resetTroubleshooting Build Issues
Regenerate artifacts:
bun run generate:actions
bun run docs:generateRe-run checks:
bun run check
bunx tsc --noEmit
bun run buildTroubleshoot esbuild / Drizzle Hangs
Symptoms:
bun run docs:generatedoes not finishbun run db:generateorbun run db:pushstays stuck
Actions:
Re-run the command once to confirm timeout output from safe scripts.
Reinstall dependencies:
rm -rf node_modules
bun installRetry:
bun run docs:generate
bun run db:generate
bun run db:push