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 creating a new Vercel project, restoring a production environment, checking that preview deployments have the same provider wiring as production, or preparing the Docker self-host runtime.
Rules
- Never commit secrets to
.env,.env.production, or any tracked file. - Store Vercel production and preview secrets in Vercel project environment variables.
- Store self-host production secrets in the orchestrator secret store. For the
compose baseline, use an untracked
.env.self-hostfile copied from.env.self-host.example. - Store local secrets in
.env.localor in a pulled.env.localfile created byvercel env pull. - 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.
Vercel Workflow
Link the local checkout to the right Vercel project before pulling or pushing environment variables:
vercel link --yes --project <project-name-or-id> --scope <team-or-user>Add secrets from the Vercel dashboard or CLI. Scope production-only values to Production, preview values to Preview, and local development values to Development:
echo "<secret-value>" | vercel env add VARIABLE_NAME production
echo "<secret-value>" | vercel env add VARIABLE_NAME preview
echo "<secret-value>" | vercel env add VARIABLE_NAME developmentAfter adding or changing variables, redeploy the affected Vercel environment. Existing deployments do not automatically receive updated environment values.
Pull a local copy for development:
vercel env pull .env.local --environment=development --yesPull production values only for an explicit audit on a trusted machine:
vercel env pull .env.production.local --environment=production --yesCheck 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 production release on main while moving the expensive
Docker build away from the Mac mini VM and avoiding GitHub Actions artifact
storage quota.
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 runs drizzle-kit push schema sync plus the seed script.
Re-run it before starting a freshly provisioned self-host database and after
schema changes. It is not a versioned Drizzle migration runner.
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 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
maindeploy to theproductionCoolify application after the CI checks pass.
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 | 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. |
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, production | NEXT_PUBLIC_* build secrets | Public client bundle values baked into the Docker image. |
development, production | NEXT_BUILD_WORKERS variable | Build worker count. Defaults to 2 for GitHub-hosted 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 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 updates
the app's git_branch in Coolify and deploys that branch without forcing a
rebuild, so BuildKit can 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 | Vercel runtime | Usually set by the platform. Do not override unless debugging a non-Vercel runtime. |
NEXT_PUBLIC_BASE_URL | Vercel Domains | 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. |
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. |
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. |
VERCEL_URL | Vercel runtime | Set automatically by Vercel. Keep it out of local .env unless reproducing platform behavior. |
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. |
CMS_DASHBOARD_FULL_METRICS | Keep false unless the database has been tuned for the full CMS publication and activity aggregates. |
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 Vercel Production and Preview, or in the self-host secret store. - Use a separate development database for Vercel Development, local
.env.local, or.env.self-host.
If the provider exposes both direct and pooled URLs, prefer the pooled URL for Vercel functions. The app already defaults to a conservative production pool of three connections per runtime to avoid exhausting serverless database limits.
Canonical Host and Auth
Set:
NEXT_PUBLIC_BASE_URL=https://yayaw.app
BETTER_AUTH_TRUSTED_ORIGINS=https://*.yayaw.app,https://*.vercel.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 Vercel 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.
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:support@yayaw.app | 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 target environment. For Vercel:
vercel env add BILLING_CODE_ACCESS_GITHUB_APP_PRIVATE_KEY production
vercel env add BILLING_CODE_ACCESS_GITHUB_APP_PRIVATE_KEY previewThe Vercel dashboard is the safest place to paste a multiline PEM value for the
hosted provider. 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
team@yayaw.app. 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
These values are optional and only required when the matching provider is enabled:
| Provider | Variables | Where to get them |
|---|---|---|
| GitHub OAuth | GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET | GitHub OAuth App settings. This is separate from the GitHub App used for code access. |
| Google OAuth | GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET | Google Cloud Console OAuth client. |
| Facebook OAuth | FACEBOOK_CLIENT_ID, FACEBOOK_CLIENT_SECRET | Meta developer app settings. |
Use the production origin from NEXT_PUBLIC_BASE_URL when configuring OAuth
redirect URIs.
Object Storage
Media storage is optional until upload, thumbnail, logo, or generated-image
features are used. Use STORAGE_PROVIDER=supabase for the hosted Supabase path
or STORAGE_PROVIDER=s3 for MinIO, S3, R2, or another S3-compatible provider.
When the provider is unset, Supabase is selected if Supabase storage env is
present; otherwise S3 is selected if the S3 env set is complete.
Common variables:
| Variable | Where to get it |
|---|---|
STORAGE_PROVIDER | Set to supabase or s3; leave empty for auto-detection. |
STORAGE_MEDIA_BUCKET | Bucket name for media assets. Defaults to media. |
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. |
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.
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.
Supabase storage variables:
| Variable | Where to get it |
|---|---|
NEXT_PUBLIC_SUPABASE_URL | Supabase project API settings. |
NEXT_PUBLIC_SUPABASE_ANON_KEY | Supabase project API settings. Public browser key, not a secret. |
SUPABASE_SERVICE_ROLE_KEY | Supabase project API settings. Secret server-side key. |
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 SUPABASE_SERVICE_ROLE_KEY, S3_ACCESS_KEY_ID, or
S3_SECRET_ACCESS_KEY to the browser or a NEXT_PUBLIC_* variable.
Public Domains
Organization public domains use PUBLIC_DOMAIN_PROVIDER.
| Variable | Purpose |
|---|---|
PUBLIC_DOMAIN_PROVIDER | vercel for Vercel project-domain management or manual-dns for self-hosted DNS verification. |
APP_MANAGED_HOSTS | Comma-separated app-owned hosts that customers cannot claim. |
RESERVED_PUBLIC_DOMAIN_SUFFIXES | Additional suffixes that customers cannot claim. .vercel.app is always reserved. |
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. |
VERCEL_TOKEN | Required only for the Vercel provider. |
VERCEL_PROJECT_ID | Required only for the Vercel provider. |
VERCEL_TEAM_ID | Optional Vercel team scope. |
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, vercel-queue on Vercel, or db-worker for a long-lived worker. Production auto-detects Vercel and otherwise defaults to db-worker. |
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_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.
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
Vercel deployments get runtime metadata from Vercel automatically. Self-hosted deployments can provide static metadata:
| Variable | Purpose |
|---|---|
DEPLOYMENT_PROVIDER | vercel, static, or local; empty auto-detects. |
DEPLOYMENT_URL | Public deployment URL outside Vercel. |
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, Supabase/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 buildProvider References
- Vercel environment variables
- Vercel CLI environment commands
- Next.js self-hosting
- Next.js standalone output
- Docker Next.js guide
- Stripe API keys
- Stripe webhooks
- GitHub App registration
- GitHub App private keys
- GitHub repository collaborators API
- Resend API keys
- Supabase API keys
- Supabase self-hosting
- MinIO S3 compatibility
- OpenAI API authentication