Deployment Environment Setup
How to collect, scope, and verify the environment variables required for deployment.
This page is the deployment operator guide for environment variables. Use it when restoring a production environment, checking that the Coolify preview has the same provider wiring as production, or preparing the portable Docker runtime.
Rules
Never commit secrets to
.env,.env.production, or any tracked file.Store production and preview secrets in the Coolify/orchestrator secret store. For the compose baseline, use an untracked
.env.self-hostfile copied from.env.self-host.example.Store local secrets in an untracked
.env.localpopulated from the trusted secret source.Keep
NEXT_PUBLIC_*values non-secret because they are exposed to the browser bundle.Prefer admin/runtime settings for operational non-secrets when the dashboard supports them. For example, GitHub code-access repository settings are managed from
/dashboard/admin/billing-settings; environment values are only fallbacks.
Deployment Target
Yayaw production and preview are self-hosted through Docker/Coolify. Vercel is
not a deployment target. Do not run the Vercel CLI, link this checkout, add a
Vercel deployment workflow, or create a local .vercel directory. The tracked
vercel.json is retained only to keep git.deploymentEnabled: false while any
legacy Git integration is disconnected.
Use the GitHub development and production environments for image-build and
deployment inputs, and the Coolify/orchestrator secret store for runtime
secrets. Copy required development values into .env.local only on a trusted
machine.
Check that every key listed in .env.example is present locally:
while IFS='=' read -r key _; do
[[ -z "$key" || "$key" == \#* ]] && continue
grep -q "^${key}=" .env.local || echo "Missing in .env.local: $key"
done < .env.exampleSelf-Hosted Docker Workflow
The portable baseline uses Docker Compose with Postgres, MinIO/S3, the Next.js standalone app server, a Page AI worker, and Caddy:
cp .env.self-host.example .env.self-host
docker compose --env-file .env.self-host -f docker-compose.self-host.yml --profile setup run --rm migrate
docker compose --env-file .env.self-host -f docker-compose.self-host.yml up --buildDocker builds use NEXT_BUILD_WORKERS=1 by default for stable Next.js static
generation on small hosts. On larger machines you can raise it for faster builds,
for example NEXT_BUILD_WORKERS=2 docker compose --env-file .env.self-host -f
docker-compose.self-host.yml up --build.
Self-hosted Docker builds also default NEXT_STATIC_PAGE_GENERATION_TIMEOUT to
180 seconds. This gives slower hosts enough time to prerender large docs pages
without tripping Next.js' default 60 second static generation timeout. Lower it
on faster CI builders or raise it only if the build logs show legitimate static
pages timing out.
Self-hosted Docker builds render Fumadocs pages dynamically instead of pre-generating every docs page during image builds. This keeps small Coolify or Docker hosts from spending most of their deployment budget on documentation pages while still serving docs normally at runtime.
The Compose files tag the app and worker build targets with YAYAW_APP_IMAGE
and YAYAW_WORKER_IMAGE. Keep the same worker image tag for the migrate and
worker services so Docker or Coolify can reuse the worker target instead of
building it twice.
The Yayaw-managed Coolify deployment uses prebuilt images instead of building on
the production VM. GitHub Actions builds the app and worker images on
GitHub-hosted ubuntu-24.04-arm runners for the Mac mini VM's linux/arm64
platform, pushes the images to GHCR, makes the Coolify server pull them through
the private self-hosted runner, updates YAYAW_APP_IMAGE and
YAYAW_WORKER_IMAGE, and then asks Coolify to run the compose deployment. This
keeps the automatic reader/freeze production release path on main while
moving the expensive Docker build away from the Mac mini VM and avoiding GitHub
Actions artifact storage quota. Once production is in protected exact, the
same trigger performs a read-only evaluation and defers rollout to
CMS Production Promotion.
Coolify must store those image tag variables as build-time and runtime values
because Docker Compose interpolates image: entries while preparing the
deployment.
The setup profile bootstraps migration history, runs versioned Drizzle
migrations, then runs the seed script and
bun run dynamic-data:repair-deployed-storage. Re-run it before starting a
freshly provisioned self-host database and after schema changes. Setup never
pushes the schema. A database with zero public tables receives, in one
transaction, the prerequisite receipt-validation function, the frozen schema of
migration 0050 (src/lib/db/baseline/0050_better_auth_1_7_stable.sql), the
migration history from 0000 to 0050, and every later migration; any error exits
non-zero and leaves the database empty. Migration 0051 recreates and verifies
the complete set of migration-only functions and triggers after reconciling
publication provenance, revision heads, and the runtime-floor singleton. A
push-managed legacy database is accepted only when its full pre-0050
table/column shape matches that baseline, it has no table that only a later
migration creates, the Better Auth preflight passes, and the write-pause
attestation is active; setup records 0049 before applying 0050, the same 0051
reconciliation, and every later migration. The guard reruns the preflight on an
already-migrated database to detect
account rows created by a beta rollback. For an existing database with
migration 0050 pending, it requires BETTER_AUTH_17_WRITES_PAUSED=true before
db:migrate. The Compose wrapper also resolves the currently served
/api/ready contract through its private BETTER_AUTH_RUNTIME_PROBE_URL: only
exact stable-1.7 evidence bypasses a new pause on an already-applied database;
an unavailable, beta, or unknown runtime requires the attestation again. A
static contract value is not trusted. A zero-table bootstrap is the sole
internal exception.
The variable is an operator attestation, not a runtime lock: first
take and verify a backup and block /api/auth writes at the ingress or by
stopping the old app, then remove the attestation immediately after cutover.
The dynamic-data repair step covers MCP-deployed native ydm_* tables that are
intentionally not part of the static Drizzle schema.
Do not commit .env.self-host. For production, move the same values to the
target host's secret manager or compose environment. Generate
BETTER_AUTH_SECRET before the first Docker build, for example with
openssl rand -hex 32; the Dockerfile fails fast when it is empty. The
--env-file flag makes the same public values available to Docker build args.
BETTER_AUTH_SECRET is also passed to next build so Better Auth route
evaluation never falls back to a development secret during image builds. Rebuild
the image when NEXT_PUBLIC_* values change because Next.js exposes them in the
browser bundle at build time.
Coolify Rolling Migration
docker-compose.coolify.yml remains the legacy deployment unit while the rolling
path is validated. It replaces migrate, app, worker, and Caddy together, so it
cannot provide a zero-downtime release. Do not point the automatic development or
production workflows at the new resources until the manual rolling workflow has
passed its rollback checks.
Migrate development first:
Back up Postgres and MinIO. Keep the existing Coolify Compose resource UUID so its
postgres_dataandminio_datavolumes are reused.Enable Connect to Predefined Network on the existing infrastructure resource. Record the resolved
postgres-<resource-uuid>andminio-<resource-uuid>hostnames.Create a Coolify Docker Image Application from the immutable
ghcr.io/<owner>/yayaw-app:<sha>image. Expose container port3000, do not map a host port, do not set a container name, and connect it to the predefined network. Configure the GHCR registry credentials in Coolify when the package is private; the workflow's temporary pull credentials are not a registry configuration for the Application.Copy the app runtime environment to that Application. Change
DATABASE_URLandS3_ENDPOINTto the resolved infrastructure hostnames. Initially use a temporary development domain.Keep health checks enabled. The Docker image checks
/api/ready, which returns200only when Postgres is reachable and the immutable image SHA matches the requested SHA./api/healthremains liveness-only.Configure a 20-30 second stop grace period in Coolify. Next.js handles
SIGTERMand finishes in-flight work during that window.Move the Page AI worker to a separate resource based on
ghcr.io/<owner>/yayaw-worker:<sha>. It must not remain in the infrastructure stack that survives web releases.On a database clone, verify that
drizzle.__drizzle_migrationsaccurately represents the already-applied SQL migrations. The rolling runner refuses to migrate a non-empty database with a missing or empty journal. Do not fabricate a baseline directly in production.Add
COOLIFY_ROLLING_APPLICATION_UUIDandNEXT_SERVER_ACTIONS_ENCRYPTION_KEYto the GitHubdevelopmentenvironment. Generate the latter once withopenssl rand -base64 32and keep the same value across builds.Run
Coolify Rolling Deployfordevelopment. It pulls both images with retry, runs the Better Auth cutover guard, versioned Drizzle migrations, schema verification, seed, and dynamic-data repair, then updates the Docker Image Application. For the one-time Better Auth 1.7 cutover of an existing database, do not leave the old app serving auth writes: first block every/api/authwrite externally, then set the non-secret GitHub environment variableBETTER_AUTH_17_WRITES_PAUSED=true. Remove it after migration. A failed deployment or public SHA smoke test restores the previous image tag automatically; database changes are not rolled back.After the web rollout succeeds, promote the separate worker resource to the emitted immutable
yayaw-worker:<sha>tag. Keep the previous worker running until the replacement process is healthy; all worker changes must remain compatible with the migrated schema during this handoff.Validate an intentionally bad image or readiness failure and confirm that the old public SHA remains available. Then repeat the procedure for production.
After the new app and worker are healthy, change the existing Compose resource to
docker-compose.coolify.infrastructure.yml. Set MINIO_MEDIA_FQDN to the public
origin plus /media and MINIO_ORGANIZATION_LOGOS_FQDN to the same origin plus
/organization-logos; Coolify creates the stable path routers to MinIO. Remove
the old Caddy domain and assign the root domain to the Docker Image Application
during the same handoff. The infrastructure file has no host port mappings and
must not be redeployed for normal application releases.
Every migration used by this path must follow expand/contract: add compatible schema first, deploy code that tolerates both versions, and remove old schema only after all older containers and workers are gone. Automatic image rollback cannot undo a destructive database migration.
Public compression through chained Caddy proxies
When the legacy Compose Caddy remains behind the Mac Mini public Caddy, both
layers must preserve encoding negotiation. The public yayaw.app fallback must
not force header_up Accept-Encoding identity: that disables the internal
proxy's gzip/zstd response even when its direct probe succeeds. Finding encode
elsewhere in the front Caddyfile does not prove that the production host uses it.
The rolling workflow validates a narrowly scoped candidate before reloading the front proxy. Its adapted JSON must differ only by removal of that production upstream override; other hosts, authentication, headers, and routes are retained. Public HTML compression is then required before replacing the application, and the application smoke verifies HTML/CSS/JavaScript compression and byte budgets. The original front configuration is restored if the full-chain check fails.
The asset smoke follows Caddy's default 512-byte compression minimum: smaller chunks may remain uncompressed, but every byte still counts toward the existing transfer budgets. Larger uncompressed assets fail with their path and size. Rollback verifies the previous image's exact SHA, database readiness, public page, and anonymous auth response without imposing the new release's performance budgets on that older image. When auth ingress is paused, the private auth probe must select the actual rollback image rather than the failed replacement.
On the ingress runner, bash .github/scripts/inspect-public-caddy-compression.sh
compares the on-disk and active configurations, validates a candidate without
installing it, and probes the public/front responses. Its logs include only
allowlisted routing and compression settings, never auth or TLS material.
Coolify CI Environments
Self-hosted Coolify deployments are driven from GitHub Actions:
Pull requests deploy to a shared
developmentCoolify application after the CI checks pass.Pushes to
mainevaluate the production release after the CI checks pass. Reader/freeze fleets roll automatically; an exact fleet produces a successful promotion-required summary without changing production.
Configure two GitHub environments with separate Coolify application UUIDs:
| GitHub environment | Required secret or variable | Purpose |
|---|---|---|
development | COOLIFY_URL | Coolify API origin. |
development | COOLIFY_API_TOKEN | Token allowed to update and deploy the development app. |
development | COOLIFY_APPLICATION_UUID | Shared development app UUID. |
development | COOLIFY_ROLLING_APPLICATION_UUID | Docker Image Application UUID used by the manual rolling validation workflow. |
development | DEVELOPMENT_BASIC_AUTH_USERNAME | HTTP Basic username for the protected development app. |
development | DEVELOPMENT_BASIC_AUTH_PASSWORD | HTTP Basic password for the protected development app. |
development | DEVELOPMENT_URL variable | Optional smoke-test URL. Defaults to https://dev.yayaw.app; use an internal runner-reachable URL when the public DNS is not routed yet. |
production | COOLIFY_URL | Coolify API origin. |
production | COOLIFY_API_TOKEN | Token allowed to deploy the production app. |
production | COOLIFY_APPLICATION_UUID | Production app UUID. |
production | COOLIFY_ROLLING_APPLICATION_UUID | Production Docker Image Application UUID after development validation. |
development, production | BETTER_AUTH_SECRET | Build-time secret required by the Next.js Docker image build. |
development, production | DATABASE_URL | Build-time database URL for server route evaluation. |
development | POSTGRES_ROTATION_DATABASE_URL | Temporary candidate URL used only by credential rotation and runtime synchronization. Keep DATABASE_URL on the currently working credential until the first rotated deployment succeeds. |
development, production | NEXT_PUBLIC_* build secrets | Public client bundle values baked into the Docker image. |
development, production | NEXT_SERVER_ACTIONS_ENCRYPTION_KEY | Stable base64 AES key embedded during next build so overlapping instances accept the same Server Actions payloads. |
development, production | NEXT_BUILD_WORKERS variable | Build worker count. Defaults to 1 for stable self-hosted Docker builds. |
development, production | NEXT_STATIC_PAGE_GENERATION_TIMEOUT variable | Static generation timeout. Defaults to 180. |
development, production | COOLIFY_SERVER_SSH_HOST variable | Optional host override for loading images onto the Coolify VM. Defaults to 127.0.0.1 from the Mac mini runner. |
development, production | COOLIFY_SERVER_SSH_PORT variable | Optional SSH port override. Defaults to 2222. |
development, production | COOLIFY_SERVER_SSH_USER variable | Optional SSH user override. Defaults to yannis. |
development, production | COOLIFY_SERVER_SSH_KEY_PATH variable or COOLIFY_SERVER_SSH_KEY secret | Optional SSH key override. The Mac mini runner defaults to /Users/yannis/.ssh/coolify_vm_ed25519. |
development, production | COOLIFY_SERVER_KNOWN_HOSTS_PATH variable | Optional known-hosts override. Defaults to /Users/yannis/.ssh/known_hosts_coolify. |
The CMS release preflight additionally synchronizes the database, private storage, signed-preview, personalization, and worker-maintenance contract to the Coolify app and worker. Configure these GitHub environment secrets:
S3_ACCESS_KEY_ID,S3_ENDPOINT, andS3_SECRET_ACCESS_KEYCMS_PREVIEW_SIGNING_SECRET,CMS_PERSONALIZATION_RATE_LIMIT_SECRET, andCMS_PERSONALIZATION_FORM_CONTEXT_SECRET, each at least 32 characters; keep the form-context secret dedicatedoptional
STORAGE_TRANSFER_SIGNING_SECRETwhen storage transfer tokens should not share the preview-signing secretDATABASE_URLin both environmentsMINIO_ROOT_PASSWORD,POSTGRES_PASSWORD, and the temporaryPOSTGRES_ROTATION_DATABASE_URLandPOSTGRES_ROTATION_PREVIOUS_PASSWORDindevelopment
Configure STORAGE_PROVIDER=s3, STORAGE_MEDIA_BUCKET=media,
STORAGE_MEDIA_STAGING_BUCKET=media-staging,
STORAGE_CMS_FORM_BUCKET=cms-form-submissions,
CMS_PERSONALIZATION_CLIENT_IP_HEADER=x-real-ip,
PAGE_AI_WORKER_MAINTENANCE_MS=60000, S3_REGION=us-east-1, and
S3_FORCE_PATH_STYLE=true as GitHub environment variables.
STORAGE_PUBLIC_BASE_URL is optional when NEXT_PUBLIC_BASE_URL already names
the same public origin. S3_SIGNED_PUBLIC_ENDPOINT is an optional variable for
an explicitly configured direct-upload origin. Do not persist a rollout phase
as a GitHub variable: the deployment and rollout workflows derive and validate
the safe reader, freeze, or exact phase from the live fleet before
synchronizing the corresponding runtime flags.
Prepare the development credentials once, outside the deployment:
database_password="$(openssl rand -hex 32)"
storage_password="$(openssl rand -hex 32)"
database_fingerprint="$(
printf '%s' "$database_password" | shasum -a 256 | awk '{print $1}'
)"
rotation_database_url="$(
printf 'postgresql://yayaw:%s@postgres:5432/yayaw' "$database_password"
)"
printf '%s' "$database_password" | gh secret set POSTGRES_PASSWORD --env development
printf '%s' "$rotation_database_url" \
| gh secret set POSTGRES_ROTATION_DATABASE_URL --env development
printf '%s' "$storage_password" | gh secret set MINIO_ROOT_PASSWORD --env development
printf '%s' "$storage_password" | gh secret set S3_SECRET_ACCESS_KEY --env development
gh variable set CMS_DATABASE_CREDENTIALS_FINGERPRINT \
--env development \
--body "$database_fingerprint"The database password is deliberately a raw 32-byte hexadecimal value so it
does not require URL encoding. For an existing development volume, leave
DATABASE_URL on the currently working credential while the PR images build.
The workflow passes that active URL to the rotation step, which derives its raw
password in memory without exposing it in logs. Set
POSTGRES_ROTATION_PREVIOUS_PASSWORD explicitly only when the active URL uses
an unsupported password encoding. The deployment job uses
POSTGRES_ROTATION_DATABASE_URL only after the images exist: it tests the
candidate credential over the private yayaw-postgres-development alias; when
needed, it uses a permission-0600 remote env file to alter the role, retests
the new credential, and synchronizes that candidate URL to Coolify. It stops
before Coolify env mutation when neither credential works.
The legacy development Compose resource defaults its shared-network aliases to
yayaw-postgres-development and yayaw-minio-development. Do not reuse either
alias for production on the same Docker network: duplicate aliases make Docker
DNS select an infrastructure container nondeterministically. If the legacy
Compose file is intentionally used for production, set
POSTGRES_COOLIFY_ALIAS=yayaw-postgres-production and
MINIO_COOLIFY_ALIAS=yayaw-minio-production explicitly. The stable rolling
infrastructure should continue using its Coolify-resolved service hostnames.
Within the legacy development Compose resource, keep S3_ENDPOINT on
http://minio:9000; the migration service waits for minio-init before the
database seed uploads CMS assets. One-shot migration and storage-preflight
containers run on the shared Coolify network instead; the rollout workflow
supplies them with the unique http://yayaw-minio-development:9000 endpoint
without changing the Compose runtime endpoint.
After the first rotated development deployment succeeds, promote the retained candidate URL and remove the one-time candidate:
printf '%s' "$rotation_database_url" \
| gh secret set DATABASE_URL --env development
gh secret delete POSTGRES_ROTATION_DATABASE_URL --env developmentAlso delete POSTGRES_ROTATION_PREVIOUS_PASSWORD if the explicit fallback was
configured.
For a fresh volume with no running app, set DATABASE_URL to the same candidate
URL before the first build as well. Never replace only DATABASE_URL and
POSTGRES_PASSWORD on an existing volume before the rotation workflow has
completed.
The GitHub workflows also need repository package permissions: the image-build
jobs use packages: write to publish ghcr.io/<owner>/yayaw-app:<sha> and
ghcr.io/<owner>/yayaw-worker:<sha>, while the Coolify deploy jobs use
packages: read to pull those images from the self-hosted runner.
The shared development app is intentionally mutable: each pull request
force-updates the repository-managed coolify/development branch to the PR head
SHA, updates the app's git_branch in Coolify to that stable branch, and
deploys without forcing a rebuild. This avoids failures when a merged PR branch
is deleted before Coolify clones the repository, while still letting BuildKit
reuse layers across development and production deployments. GitHub Actions
serializes development deploys through a single development-coolify-deploy
concurrency group, so the shared environment stays predictable while new runs
wait for the current deployment to finish.
Keep the development origin private. Prefer a Tailscale-only hostname or enable
HTTP Basic authentication at the Coolify app level while still returning
X-Robots-Tag: noindex, nofollow, noarchive. The smoke test runs from the
self-hosted runner with the development Basic Auth secrets, so the runner must
be able to reach the protected development URL.
Production Baseline
These values are required before a production deployment can serve the app correctly:
| Variable | Where to get it | Value guidance |
|---|---|---|
NODE_ENV | Coolify application env | Set to production for production containers. |
NEXT_PUBLIC_BASE_URL | Reverse proxy/DNS configuration | Set to the canonical public origin, for example https://yayaw.app. |
BETTER_AUTH_SECRET | Secret manager | Set a stable long random value for Better Auth cookies and tokens. |
SCIM_CREDENTIAL_HASH_SECRET | Secret manager | Set a distinct stable random value of at least 32 characters for managed SCIM credential HMAC digests. |
DATABASE_URL | Postgres provider | Use the production pooled connection string when the provider exposes one. |
BETTER_AUTH_TRUSTED_ORIGINS | Deployment domains | Add extra preview/local origins as a comma-separated list. The canonical host and www variant are derived from NEXT_PUBLIC_BASE_URL. |
BETTER_AUTH_DEVICE_CLIENT_IDS | Deployment env | Optional comma-separated Better Auth Device Authorization client IDs. Defaults to kyber-desktop,kyber-web. |
BETTER_AUTH_DEVICE_TRUSTED_ORIGINS | Deployment env | Optional comma-separated local/native origins allowed to use only the Device Authorization and bearer-token auth endpoints. |
CADDY_HOST_PORT | Coolify application env | Host port for the compose Caddy container. Production defaults to 3080; set a different value such as 3081 for the shared development app. |
MINIO_API_HOST_PORT | Coolify application env | Host port for MinIO's S3-compatible API. Production defaults to 9000; set a different value such as 9100 for the shared development app. |
MINIO_CONSOLE_HOST_PORT | Coolify application env | Host port for the MinIO console. Production defaults to 9001; set a different value such as 9101 for the shared development app. |
Optional database pool knobs:
| Variable | When to set it |
|---|---|
DATABASE_POOL_MAX | Raise only when the database provider can support more per-runtime connections. |
DATABASE_IDLE_TIMEOUT_SECONDS | Tune idle lifecycle for non-serverless deployments. |
DATABASE_MAX_LIFETIME_SECONDS | Tune connection rotation for long-lived runtimes. |
DATABASE_CONNECT_TIMEOUT_SECONDS | Tune startup behavior for slow private networks. |
DATABASE_STATEMENT_TIMEOUT_SECONDS | Add a server-side statement timeout for each database connection. |
DATABASE_PREPARE_STATEMENTS | Keep false for pooler/serverless production connections unless the provider supports prepared statements. |
Database
DATABASE_URL is required by the application runtime, setup scripts, seeds, and
Drizzle operations.
For the Coolify self-host deployment, schema sync and seeds run inside the
compose migrate service before the app and worker start. Keep the production
DATABASE_URL in Coolify or the orchestrator secret store so the setup step can
resolve internal Docker service names such as postgres.
For hosted providers that do not run the self-host compose stack, execute the chosen Drizzle schema operation from a runner that can reach the production database before promoting the new app version.
For hosted Postgres providers:
Create the production database.
Open the provider connection string panel.
Copy the pooled connection string when available.
Store it as
DATABASE_URLin the Coolify/orchestrator secret store.Use a separate development database in local
.env.localor the development Coolify application.
If the provider exposes both direct and pooled URLs, use the connection mode recommended for long-lived containers and size the application pool for the database limit.
Canonical Host and Auth
Set:
NEXT_PUBLIC_BASE_URL=https://yayaw.app
BETTER_AUTH_TRUSTED_ORIGINS=https://*.yayaw.appNEXT_PUBLIC_BASE_URL drives canonical URLs, Better Auth base URL, OAuth
metadata, sitemap/robots links, and generated absolute asset URLs.
Do not set NEXT_PUBLIC_SITE_URL; the application does not read it. Do not set
BETTER_AUTH_URL for new deployments; Better Auth receives its base URL from
NEXT_PUBLIC_BASE_URL.
The branch-backed preview domain is https://preview.yayaw.app and tracks the
durable preview Git branch.
Retired .eu hosts may stay attached only as reverse-proxy 308 redirects to
their .app replacements, such as yayaw.eu and www.yayaw.eu to
yayaw.app. They must not serve dashboard/auth traffic or appear in
BETTER_AUTH_TRUSTED_ORIGINS.
Better Auth 1.7 Cutover Preflight
Before applying the Better Auth 1.7 migration to any restored production copy or live cutover database, stop authentication writes and run:
bun run ba:1-7:preflightThe read-only preflight inventories account providers and their future issuers,
OAuth clients/resources, Device Authorization codes, and legacy SCIM rows. It
blocks unknown account issuers, identity collisions, unsafe OAuth metadata,
machine clients without a durable user owner and reviewed organization
membership reference, duplicate device codes, orphan resource links, or
non-empty legacy SCIM data. A machine client whose owner belongs to exactly one
organization is backfilled deterministically; zero or multiple memberships
require an explicit oauth_client.reference_id choice and never require client
re-registration.
Run it again immediately before migration after the maintenance window begins.
Better Auth 1.7.3 restores account identity to (provider_id, account_id) and
stops writing the temporary issuer field introduced by 1.7.0 through 1.7.2.
Migration 0052 rejects duplicate provider/account pairs, replaces the temporary
issuer-based unique index, and leaves account.issuer nullable for rollback
compatibility. Do not make that column required again or use it for account
lookups.
Managed SCIM Provisioning
Managed SCIM is enabled by the seeded enable-scim-plugin flag. Set a distinct,
stable SCIM_CREDENTIAL_HASH_SECRET in every enabled environment before the
application starts:
openssl rand -hex 32Store the result in the deployment orchestrator's secret store and pass it to the app runtime. The Coolify and portable self-host Compose definitions forward this value. Do not expose it as a public or build-time variable. Better Auth stores versioned HMAC-SHA256 digests of issued credentials, so changing the secret without a data-aware migration invalidates every managed token.
An administrator with organization-settings:manage creates and rotates
connections from /dashboard/organization/settings. Configure the identity
provider with the displayed one-time bearer token and:
https://yayaw.app/api/auth/scim/v2The raw token is returned only by creation or rotation and must be copied before the dashboard is refreshed. Each credential expires after 365 days by default, grants SCIM Users/Groups read and write operations, and can be revoked while an overlapping rotated credential remains active. Decommissioning is irreversible. SCIM Groups remain provisioning data only: no group name, membership, or role attribute creates a Yayaw role, binding, or permission.
Stripe Billing
Stripe has three required secrets when billing is enabled:
| Variable | Where to get it | Notes |
|---|---|---|
STRIPE_SECRET_KEY | Stripe Dashboard, Developers, API keys | Use sk_live_... in production and sk_test_... in staging/local test environments. |
STRIPE_WEBHOOK_SECRET | Stripe Dashboard, Developers, Webhooks, subscription endpoint signing secret | Used by the Better Auth Stripe webhook endpoint. |
STRIPE_ONE_TIME_WEBHOOK_SECRET | Stripe Dashboard, Developers, Webhooks, one-time endpoint signing secret | Used by the custom one-time checkout webhook endpoint. |
Create two Stripe webhook endpoints for production:
https://yayaw.app/api/auth/stripe/webhook
https://yayaw.app/api/billing/stripe/webhookUse the signing secret from the first endpoint for STRIPE_WEBHOOK_SECRET and
the signing secret from the second endpoint for
STRIPE_ONE_TIME_WEBHOOK_SECRET. Do not reuse one endpoint secret for the other
endpoint.
The subscription endpoint should receive subscription checkout and billing
lifecycle events. The one-time endpoint only needs checkout.session.completed
for lifetime purchases.
Billing product prices are managed from /dashboard/admin/billing-products or
through the control plane MCP. Stripe price IDs are stored in the database after
catalog sync; they are not deployment secrets.
Code-Access Deliverable Links
These values are optional, non-secret URLs shown on
/dashboard/organization/code-access after a paid purchase unlocks code
access:
| Variable | Example | When to use it |
|---|---|---|
BILLING_CODE_ACCESS_REPOSITORY_URL | https://github.com/Yayaw-eu/yayaw | Link to the private repository after GitHub access is granted. |
BILLING_CODE_ACCESS_DOWNLOAD_URL | https://github.com/Yayaw-eu/yayaw/releases | Link to release artifacts or archive downloads. |
BILLING_CODE_ACCESS_DOCUMENTATION_URL | https://docs.yayaw.app | Link to setup or usage documentation. |
BILLING_CODE_ACCESS_SUPPORT_URL | mailto:[email protected] | Link to support for access problems. |
Leave a value empty when that deliverable requires manual provisioning.
GitHub Code Access
Production repository access should use a GitHub App, not a personal access
token. The app lets Yayaw invite paid customers to the configured repository
with read-only pull access without storing a human user's GitHub credentials.
Create the GitHub App
Create the app from the account that owns the private repository:
GitHub organization -> Settings -> Developer settings -> GitHub Apps -> New GitHub AppRecommended app values:
| Field | Value |
|---|---|
| GitHub App name | Yayaw Code Access |
| Homepage URL | The production app URL, for example https://yayaw.app |
| Webhook | Disabled unless a future feature needs GitHub callbacks |
| Where can this GitHub App be installed | Only on this account |
Repository permissions:
| Permission | Access |
|---|---|
| Administration | Read and write |
| Metadata | Read-only, automatically granted by GitHub |
The Administration: Read and write permission is required because Yayaw calls
the GitHub collaborator API to invite the requested username to the repository.
The app does not need Contents permissions for the current flow because the
customer receives normal repository access through their own GitHub account.
Install the GitHub App
After creating the app:
Generate a private key from the app settings and download the
.pemfile.Install the app on the owner account or organization.
Select only the repository that paid customers should access.
Copy the App ID from the app settings.
Copy the installation ID from the installation URL.
The installation URL usually looks like this for an organization install:
https://github.com/organizations/Yayaw-eu/settings/installations/12345678The final numeric segment is the GitHub App installation ID.
Store the GitHub App secret
Store the private key in the Coolify/orchestrator secret store for each target
environment. Use its multiline secret editor so the PEM is never passed on a
command line. For a local .env.local or self-host secret file, either paste
the multiline key in quotes or escape newlines on one line:
printf 'BILLING_CODE_ACCESS_GITHUB_APP_PRIVATE_KEY="%s"\n' "$(perl -0pe 's/\n/\\n/g' yayaw-code-access.pem)" >> .env.localThe runtime accepts escaped \n sequences and converts them back to real
newlines before signing the GitHub App JWT.
If a deployment platform flattens the PEM into one line with spaces between the
header, body, and footer, Yayaw normalizes it back into PEM format at runtime.
Configure non-secret GitHub values
Prefer the admin UI for non-secret values:
/dashboard/admin/billing-settingsSet:
| Admin setting | Value |
|---|---|
| Enabled | On |
| Repository | Yayaw-eu/yayaw or the exact target repository |
| GitHub App ID | The numeric App ID from the GitHub App settings |
| GitHub App installation ID | The numeric installation ID from the installation URL |
Environment fallbacks are available for deployments that need boot-time config:
BILLING_CODE_ACCESS_GITHUB_REPOSITORY=Yayaw-eu/yayaw
BILLING_CODE_ACCESS_GITHUB_APP_ID=123456
BILLING_CODE_ACCESS_GITHUB_APP_INSTALLATION_ID=12345678
BILLING_CODE_ACCESS_GITHUB_APP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"Local or staging token fallback
Use BILLING_CODE_ACCESS_GITHUB_TOKEN only for local or staging environments
where a GitHub App is not available:
BILLING_CODE_ACCESS_GITHUB_REPOSITORY=Yayaw-eu/yayaw
BILLING_CODE_ACCESS_GITHUB_TOKEN=github_pat_...The token owner must be allowed to add collaborators to the repository. For a fine-grained token, grant access only to the target repository and include the repository administration permission needed to invite collaborators. Do not use the token fallback for production unless there is an incident workaround and a rotation plan.
RESEND_API_KEY enables invitation, reset, magic-link, and billing
transactional emails.
To retrieve it:
Verify the sending domain in Resend for production email.
Create a server-side API key in Resend.
Store it as
RESEND_API_KEYin the target deployment environment.Send a test email from the transactional email admin surface after deploy.
Set EMAIL_SENDER to a verified sender on the Resend domain, for example
[email protected]. Set EMAIL_SUPPORT to the support address shown inside email
templates, and EMAIL_USERNAME to the sender display name. These variables are
bootstrap defaults and fallbacks; after the first admin user can sign in,
Admin > Site Settings > Email takes priority at runtime.
Missing RESEND_API_KEY does not break billing webhook processing, but it does
skip transactional emails.
OAuth Providers
Set OAUTH_PROVIDER_ENCRYPTION_KEYS in the Coolify runtime secret store on every
app instance before configuring providers. Use a dedicated key generated with
openssl rand -base64 32, prefixed by a key ID (key1:<value>). This is not a build
secret and must not be shared with BETTER_AUTH_SECRET.
Google, GitHub and Microsoft client credentials are managed in Admin > Site
Settings with global auth-provider:manage and TOTP. Save a draft, register the
shown callback at the provider, test it and activate. No provider edit requires
a rebuild or restart. Initial encryption-key setup or rotation does require a
runtime restart. The GitHub OAuth App is separate from the code-access GitHub App.
Use separate provider applications for preview and production. Keep encryption
keys in secret-manager backups separately from the database. Rotation keeps both
keys (new:<value>,old:<value>) until every provider has been saved, tested and
activated again; preserve keys needed to restore older backups.
Object Storage
Media storage is optional until upload, thumbnail, logo, or generated-image
features are used. Set STORAGE_PROVIDER=s3 for MinIO, S3, R2, or another
S3-compatible provider.
Common variables:
| Variable | Where to get it |
|---|---|
STORAGE_PROVIDER | Set to s3. |
STORAGE_MEDIA_BUCKET | Bucket name for media assets. Defaults to media. |
STORAGE_MEDIA_STAGING_BUCKET | Private bucket for staged direct uploads. The bundled self-host setup uses media-staging. |
STORAGE_CMS_FORM_BUCKET | Durable private bucket for customer form attachments. The bundled self-host setup uses cms-form-submissions. |
STORAGE_PUBLIC_BASE_URL | Public object origin for S3-compatible storage, serving /<bucket>/<key>. Use the app or CDN origin only when those bucket prefixes are routed to the object store. |
STORAGE_TRANSFER_SIGNING_SECRET | Optional dedicated secret for opaque same-origin private transfer grants. It falls back to the preview or auth secret and must resolve to at least 32 characters in production. |
CMS_PERSONALIZATION_FORM_CONTEXT_SECRET | Required dedicated secret for expiring public CMS form provenance. Generate at least 32 random characters and never expose it to the browser or CMS content. |
S3_SIGNED_PUBLIC_ENDPOINT | Optional explicit public S3 API origin for direct signed uploads when the app host cannot accept the 250 MB CMS request limit. Never set the internal Docker endpoint. |
For same-origin or CDN-fronted S3 storage, proxy every public bucket prefix to
the object store before the app fallback. The built-in public buckets are
/media/* for uploaded media and /organization-logos/* for organization
logos. If STORAGE_PUBLIC_BASE_URL points directly to a dedicated object-store
origin, that origin must serve the same /<bucket>/<key> paths.
Create the staging and form buckets before enabling their writers and do not
grant them anonymous read access. Limit provider lifecycle cleanup to the
staging bucket or staging object prefix; the Page AI worker owns the durable
180-day form-photo retention ledger and retries interrupted deletions. Set
CMS_PERSONALIZATION_CLIENT_IP_HEADER=x-real-ip for the bundled Caddy ingress,
which overwrites that header, or use another proxy-owned single-value header
only after verifying the ingress strips client input. Keep the worker running
with PAGE_AI_QUEUE_DRIVER=db-worker in self-hosted environments.
Private transfers use an opaque same-origin application grant by default, so
S3_ENDPOINT can remain private. On a platform with a lower inbound-body limit,
set S3_SIGNED_PUBLIC_ENDPOINT to a dedicated safe HTTPS S3 API origin; only
uploads switch to a conditional direct PUT and private downloads remain behind
the app proxy. Allow the app origin, PUT, Content-Type, Content-Length,
and If-None-Match in that bucket's CORS policy. Promoted staging bytes remain
as a no-overwrite tombstone through grant expiry and a 24-hour settlement
window. The reaper repeats deletion and records cleanup only after its final
delete.
Roll out exact prototype writers in this order:
Deploy the compatible reader with
CMS_LEGACY_PROTOTYPE_WRITES_ENABLED=trueandCMS_PROTOTYPE_RUNTIME_V2_WRITES_ENABLED=false.Optionally run the manual
CMS Prototype Runtime Rolloutworkflow withreaderand the full SHA already deployed to prove the reader fleet.Run the same trusted workflow with
exact. It verifies/creates the private buckets, synchronizesfreeze, redeploys every typed app/worker resource on the requested full SHA, and records the live resource UUID, deployment UUID, image digest, image name, commit, and complete active container-ID set in a version-1 durable receipt. Every active container must independently expose the expected SHA plus the legacy, v2, staged-upload, and rollout-operation environment values.After revalidating that no newer deployment replaced the drain evidence, the workflow moves the receipt from
issuedtoactivating, synchronizes legacyfalse, v2true, staged uploadstrue, andCMS_PROTOTYPE_ROLLOUT_OPERATION_ID, then redeploys and revalidates every resource again. The exact deployment UUID must be new while its typed resource UUID, image digest, image name, and full SHA remain identical.The workflow verifies the final runtime state, reinspects every exact container, and passes that fresh JSON directly to receipt consumption in the same shell step. Consuming the receipt activates the database floor before any design gate is written. Only the current consumed operation may authorize a new
prototypeGateV2.
The workflow checks out its control-plane scripts at an immutable workflow SHA:
the SHA captured by GitHub when a manual run is dispatched, or the immutable
control-plane SHA supplied by the authorizing parent for a reusable child. The
authorization step proves that this SHA remains an ancestor of the fetched
current main; advancing main cannot change the scripts in an authorized run.
The requested deployment SHA is evidence, never executable workflow code. The
durable-receipt dispatcher is copied from that pinned checkout, verified by
checksum, and executed in a hardened one-shot container attached to the private
Coolify Docker network using the worker image built from that same immutable
control-plane SHA. Its entrypoint is overridden and its image ID is pinned
before database access. Keep Postgres private; the rollout does not require a
public database URL or port. If a run fails after activating, the failure
handler can only restore and redeploy freeze. Resume the same operation
explicitly with resume_operation_id and its confirmation string. A resume
requires the same environment, repository, deployment SHA, typed resources, and
images, plus a fresh live drain attempt with its own expiry. The immutable
parent keeps the control-plane SHA that issued it; a resumed child attempt
records and validates the immutable workflow/control-plane SHA captured for its
own authorization. It cannot issue a competing active receipt or fall back to
reader.
A standalone production exact dispatch is accepted only when its deployed SHA
matches the immutable workflow/control-plane SHA captured for that run. This
guard executes before any writer mutation, so a stale recovery request does not
force the live fleet into freeze. If main has advanced beyond an activating
receipt's deployed SHA, deploy the strict descendant while preserving freeze,
then use the explicit supersession path below instead of resuming the stale SHA.
If the fleet has already advanced to a strict descendant SHA while an older
receipt remains activating, do not resume or roll back the old target. Run
exact on the live descendant with both superseded_operation_id and
superseded_deploy_sha, plus
exact:<new-sha>:supersede:<old-sha>:<old-operation-id> as confirmation. The
workflow proves the Git ancestry, redeploys and revalidates the descendant in
freeze, then atomically creates and activates the replacement receipt while
linking the old one as terminal superseded. The replacement must preserve
the typed resource UUIDs and prove new deployment UUIDs. Supersession never
advances the runtime floor; only consumption of the replacement does.
Automatic production delivery starts on a push to main and reuses the
successful latest CI Quality result from the merged PR's explicit head SHA.
The complete head and merged Git trees must agree. There is no second lint,
TypeScript, audit, or test run on main. Images are built before any CMS freeze;
a failed build changes neither the served version nor CMS write availability.
After build, the workflow skips obsolete automatic targets, verifies the live
fleet, freezes exact writers, migrates/rolls out the target, and activates exact
through the existing receipt protocol. An already live-frozen fleet keeps its
freeze. Reader bootstrap requires the explicit runtime rollout first.
Shared development previews are requested directly on a validated PR using the
preview label (build requests images only). Remove and re-add the label for
a new request. Preview retains reader/freeze rules and never activates exact
implicitly. Development images now use the yayaw-development GHCR prefix;
request a new preview before a development exact operation with this control
plane. Production and receipt-control-plane images retain the yayaw prefix.
For a later renderer upgrade in development:
Run
CMS Prototype Runtime Rolloutwithfreezeagainst the currently deployed full SHA.Re-run the target PR deployment. Under the shared
development-coolify-deploylock, it preserves freeze, deploys the PR's new immutable SHA, and then proves that every live app/worker container exposes that SHA and the freeze flags. A single development Compose resource is proven as one deployment containing both expected images, with a stable aggregate image digest and the complete app/worker container-ID set.Run
CMS Prototype Runtime Rolloutwithexacton that same PR SHA. It creates and consumes the next receipt, advancing the current rollout operation while retaining the first floor operation.
For manual production recovery, dispatch CMS Production Promotion from
main with the currently deployed full SHA, the immutable main workflow SHA,
and promote-exact:<current-sha>:<target-sha>. It holds
rolling-coolify-production for authorization, image build, fleet proof, freeze,
migrations/rolling, verification, and exact activation. A failure stops the
chain; an exact activation failure restores freeze without creating a second
receipt. The parent proves the captured SHA remains in main history and pins
it through every child, including the receipt worker image. Receipt resume and
supersession remain explicit operations in CMS Prototype Runtime Rollout;
the rolling workflow itself is reusable only.
The normal job never simulates this transition. Once the floor is active, never use reader as a reset or re-enable legacy writers.
Migrations 0047_cms_prototype_runtime_floor and
0049_cms_prototype_rollout_receipt_supersession make that boundary durable.
They persist immutable parent receipts and run attempts, constrain each one-way
issued → activating → consumed state or the explicit terminal recovery
branch activating → superseded, and lock design-session writers while safely
backfilling any pre-existing exact gate with an irreversible synthetic
first/current operation. Receipt consumption activates the independent
singleton. Its first activation timestamp and first rollout
operation are immutable; its first design-context ID remains null until the
first new exact gate binds it and is immutable thereafter. Later consumed
receipts may only advance current_v2_rollout_operation_id. Existing exact
sessions can continue ordinary updates without replaying the transaction-local
operation, while a newly added exact gate must carry the current operation.
The synthetic backfill string is never sufficient authorization: the current
operation must also resolve to a real consumed receipt before another exact
gate can be added.
The floor cannot be deleted, truncated, reset, or reversed by expiring/deleting
the originating session. Database triggers and
same-transaction writer guards serialize legacy session, component, page,
review, publication, and media writes against that singleton. Resetting an
environment flag therefore cannot reopen legacy mutations. Legacy reads, pure
dry-runs, completed-upload idempotent reads, cancellation/expiration, and media
cleanup remain available. A floor-rejected media commit removes its promoted
object, while prepared-upload cleanup remains retryable by the reaper.
The manual workflow is also the rolling-binary safety boundary: every resource
must run the immutable SHA in freeze before any resource enters exact.
Do not edit these flags directly or skip the workflow, because older binaries
do not contain every application-level floor guard.
The worker-image storage preflight needs HeadBucket, optional
CreateBucket, GetBucketPolicy, GetBucketAcl, PutObject, GetObject,
and DeleteObject on the staging and form buckets. It never prints storage
credentials or full provider errors.
During setup, bun run seed uploads the default global site-variable assets to
the configured media bucket under global/site-variables/* and writes those
public URLs into the site-variables/main global-data entry. Existing custom
site-variable asset URLs are preserved when the seed is re-run.
S3-compatible storage variables:
| Variable | Where to get it |
|---|---|
S3_ENDPOINT | Provider endpoint, for example http://minio:9000. |
S3_REGION | Provider region. Use us-east-1 for MinIO unless configured otherwise. |
S3_ACCESS_KEY_ID | Server-side storage credential. |
S3_SECRET_ACCESS_KEY | Server-side storage credential. |
S3_FORCE_PATH_STYLE | Set true for MinIO and most local S3-compatible endpoints. |
Never expose S3_ACCESS_KEY_ID or S3_SECRET_ACCESS_KEY to the browser or a
NEXT_PUBLIC_* variable.
S3_SIGNED_PUBLIC_ENDPOINT is optional and is never inferred from
S3_ENDPOINT. When set in production, it must use HTTPS with a public,
multi-label DNS hostname and no credentials, query, or fragment. It enables
only signed direct PUT uploads; downloads continue through the same-origin app
proxy. Configure bucket CORS for the app origin, PUT, and the
Content-Type, Content-Length, and If-None-Match headers before setting
this variable. STORAGE_TRANSFER_SIGNING_SECRET optionally gives opaque
transfer tokens a dedicated secret; otherwise the runtime deliberately falls
back to CMS_PREVIEW_SIGNING_SECRET, then BETTER_AUTH_SECRET. The effective
production secret must contain at least 32 characters.
Public Domains
Organization public domains use PUBLIC_DOMAIN_PROVIDER.
| Variable | Purpose |
|---|---|
PUBLIC_DOMAIN_PROVIDER | Set to manual-dns for the supported self-hosted DNS verification path. |
APP_MANAGED_HOSTS | Comma-separated app-owned hosts that customers cannot claim. |
RESERVED_PUBLIC_DOMAIN_SUFFIXES | Additional suffixes that customers cannot claim. |
PUBLIC_DOMAIN_CNAME_TARGET | Manual DNS CNAME target shown to operators. |
PUBLIC_DOMAIN_IPV4_TARGETS | Comma-separated manual DNS A-record targets. |
PUBLIC_DOMAIN_TXT_PREFIX | TXT verification prefix, default _yayaw. |
manual-dns generates a TXT ownership challenge and verifies public DNS. It
does not automate DNS-provider changes or TLS certificate issuance.
OpenAI
AI generation features require:
OPENAI_API_KEY=Create the key from the OpenAI platform API key page and store it server-side in the target deployment environment. The key enables component AI fallback, page AI generation, and image generation when the corresponding feature flags and runtime settings are on.
Optional AI runtime toggles:
| Variable | Default | Purpose |
|---|---|---|
OPENAI_COMPONENTS_AI_FALLBACK | true | Enables AI fallback for component inference and classification. |
OPENAI_IMAGE_GENERATION_ENABLED | true | Enables page-builder image generation when an API key exists. |
OPENAI_IMAGE_MODEL | gpt-image-1.5 | Image generation model. |
PAGE_AI_QUEUE_DRIVER | Auto | Use direct locally or db-worker with the long-lived Coolify worker. Production defaults to db-worker; the legacy vercel-queue driver requires an explicit value and is never inferred from stale VERCEL_* variables. |
PAGE_AI_DEEP_REFINEMENT | false | Internal quality toggle, not exposed in admin settings. |
PAGE_AI_WORKER_POLL_MS | 1500 | Poll interval for the database worker driver. |
PAGE_AI_WORKER_MAINTENANCE_MS | 60000 | Interval for durable personalization retention, orphan cleanup, and abuse-bucket pruning. |
PAGE_AI_WORKER_ID | Empty | Optional identifier for a long-lived worker. |
Analytics
Choose one analytics provider and one capture mode:
NEXT_PUBLIC_ANALYTICS_PROVIDER=umami
NEXT_PUBLIC_ANALYTICS_CAPTURE_MODE=hybridSupported providers are posthog, umami, and none. Capture mode can be
hybrid, server, or client. hybrid is the default and records reliable
server-side billing/auth events while keeping browser product analytics. server
does not render browser analytics scripts; it sends CMS page views and
conversion events from the app server instead. client keeps browser-only
capture.
Server-side conversion events include checkout_started, purchase_completed,
subscription_started, subscription_updated, subscription_cancel_scheduled,
subscription_canceled, and payment_failed. Billing events include revenue,
value, currency, plan, product_key, and Stripe IDs for reconciliation.
Stripe test-mode billing events are still tracked with billing_mode=test and
is_test_data=true, but they do not count as conversion, revenue, or
value unless the Track Stripe Test Billing Analytics admin setting is
enabled. The original test amount is kept in test_amount_cents and
test_revenue for debugging.
PostHog
Client capture and feature flags use public PostHog values:
NEXT_PUBLIC_POSTHOG_KEY=
NEXT_PUBLIC_POSTHOG_HOST=https://eu.i.posthog.com
NEXT_PUBLIC_POSTHOG_ENABLE_LOCAL=falseServer-side dashboard analytics use private PostHog values:
POSTHOG_PERSONAL_API_KEY=
POSTHOG_PROJECT_ID=
POSTHOG_API_HOST=https://eu.posthog.com
POSTHOG_ORG_ID_PROPERTY=organization_id
POSTHOG_FLAG_LOOKUP_TIMEOUT_MS=Retrieve the public project key and project ID from the PostHog project
settings. Create a personal API key with enough access for the Query API and
store it as POSTHOG_PERSONAL_API_KEY. Keep
POSTHOG_ORG_ID_PROPERTY=organization_id unless the analytics event property
changes in the product. Set POSTHOG_FLAG_LOOKUP_TIMEOUT_MS only when
server-side PostHog flag lookups need a timeout other than the default 1500
milliseconds.
Umami
Client capture uses public Umami values:
NEXT_PUBLIC_UMAMI_HOST_URL=https://analytics.example.com
NEXT_PUBLIC_UMAMI_SCRIPT_URL=
NEXT_PUBLIC_UMAMI_WEBSITE_ID=
NEXT_PUBLIC_UMAMI_DOMAINS=
NEXT_PUBLIC_UMAMI_AUTO_TRACK=trueServer-side dashboard reads and server capture use private Umami values:
UMAMI_API_URL=https://analytics.example.com
UMAMI_WEBSITE_ID=
UMAMI_API_TOKEN=
UMAMI_API_KEY=
UMAMI_USERNAME=
UMAMI_PASSWORD=
UMAMI_CMS_EVENT_PAGE_SIZE=1000Use either UMAMI_API_TOKEN (or the legacy alias UMAMI_API_KEY) or
UMAMI_USERNAME plus UMAMI_PASSWORD for dashboard data reads. Server event
capture uses Umami /api/send, so it only needs UMAMI_API_URL and
UMAMI_WEBSITE_ID.
Control Plane MCP
Production MCP does not require an environment variable inside the app runtime.
Clients connect to /api/mcp with a Better Auth API key or OAuth access token.
Local stdio testing can use:
YAYAW_MCP_API_KEY=
YAYAW_MCP_LOCAL_USER_ID=Create, inspect, and revoke MCP keys with:
bun run mcp:keyStore trusted client keys outside the repository and pass them to the client as
YAYAW_MCP_API_KEY.
Dynamic Data Runtime Transforms
Set DYNAMIC_DATA_RUNTIME_TRANSFORM_SECRET only when deployed dynamic runtime
routes use hmac_sha256 value transforms, such as a pairing code transformed
into a stored code_hash.
DYNAMIC_DATA_RUNTIME_TRANSFORM_SECRET=$(openssl rand -hex 32)Store the value as a server-only secret in the orchestrator. Rotating it changes future HMAC output, so rotate alongside a product-specific data migration or pairing-session expiry window.
Maintenance Mode
Maintenance mode is optional:
MAINTENANCE_MODE=false
MAINTENANCE_MODE_END_DATE=Set MAINTENANCE_MODE=true only when the app should serve the maintenance
surface. Use MAINTENANCE_MODE_END_DATE for operator-facing status context.
Deployment Metadata
Coolify/self-hosted deployments provide static metadata:
| Variable | Purpose |
|---|---|
DEPLOYMENT_PROVIDER | Set static in Coolify and local for local development. |
DEPLOYMENT_URL | Public deployment URL. |
DEPLOYMENT_ENV | Environment label, such as production or preview. |
DEPLOYMENT_GIT_COMMIT_SHA | Commit SHA for dashboard/control-plane status. |
DEPLOYMENT_GIT_COMMIT_REF | Commit ref or branch name. |
Deployment Checklist
Before promoting a deployment:
The target environment has every required variable from
.env.exampleor.env.self-host.example.Preview has either production-equivalent provider values or explicit staging provider values.
NEXT_PUBLIC_BASE_URLmatches the public HTTPS origin for the environment.Stripe has two webhook endpoints and each endpoint secret is mapped to the matching environment variable.
GitHub code access has either admin settings plus
BILLING_CODE_ACCESS_GITHUB_APP_PRIVATE_KEY, or explicit environment fallbacks for repository, App ID, installation ID, and private key.Resend, PostHog, S3, OpenAI, and OAuth variables are present only when those features are enabled.
Coolify or the orchestrator has the runtime
DATABASE_URLused by the composemigrateservice, and GitHub environments have the build-timeDATABASE_URLvalue needed bynext build.A fresh deployment or container restart was triggered after the last environment variable change.
Run the local quality gates before merging deployment config changes:
bun run check
bun run docs:generate
bun run docs:check-links
bun run docs:check-translations
bun run docs:llm:check
bunx tsc --noEmit
bun run build