Environment Variables
Runtime configuration variables used by the application.
This page lists the runtime variables consumed by the application. Use Deployment Environment Setup for the operational runbook that explains where to retrieve each value, how to scope hosted provider environments, and how to validate a deployment environment before promotion.
Core
NODE_ENV=development
NEXT_PUBLIC_BASE_URL=http://localhost:3080
DATABASE_URL=postgresql://user:password@localhost:5432/yayaw
DATABASE_POOL_MAX=
DATABASE_IDLE_TIMEOUT_SECONDS=
DATABASE_MAX_LIFETIME_SECONDS=
DATABASE_CONNECT_TIMEOUT_SECONDS=
DATABASE_STATEMENT_TIMEOUT_SECONDS=
DATABASE_PREPARE_STATEMENTS=
CMS_DASHBOARD_FULL_METRICS=false
BETTER_AUTH_SECRET=
BETTER_AUTH_TRUSTED_ORIGINS=http://localhost:3080
BETTER_AUTH_DEVICE_CLIENT_IDS=kyber-desktop,kyber-web
BETTER_AUTH_DEVICE_TRUSTED_ORIGINS=http://localhost:5080,tauri://localhost,http://tauri.localhost- The
DATABASE_*pool knobs are optional. Runtime DB pools default to10connections in development and3in production/serverless. Production also defaults to10seconds connect timeout,20seconds idle timeout,300seconds max lifetime,20seconds statement timeout, and disabled prepared statements for pooler compatibility. RaiseDATABASE_POOL_MAXor the lifecycle values only when the deployment target can support the extra per-runtime connections. BETTER_AUTH_SECRETmust be a stable, high-entropy server-only secret in production and self-hosted runtimes.BETTER_AUTH_DEVICE_CLIENT_IDSis optional and controls the Better Auth Device Authorization client IDs allowed to request device codes. It defaults tokyber-desktop,kyber-web.BETTER_AUTH_DEVICE_TRUSTED_ORIGINSis optional and adds origins that can call only the Device Authorization and bearer-token auth endpoints. Use it for local/native Kyber clients instead of broadening credentialed cookie origins.DATABASE_STATEMENT_TIMEOUT_SECONDSapplies a per-statement timeout when the database client opens a connection. Leave it empty unless the target Postgres provider or self-hosted database needs an explicit guardrail.CMS_DASHBOARD_FULL_METRICS=trueenables the full CMS publication and activity query set on/dashboard/content. Leave it false by default so the CMS dashboard uses fast indexed counters and never blocks the dashboard shell on expensive aggregate reads.
Build-Time Knobs
NEXT_BUILD_WORKERS=1
NEXT_STATIC_PAGE_GENERATION_TIMEOUT=180NEXT_BUILD_WORKERScontrols the number of Next.js workers used duringnext build. Self-hosted Docker builds default to1for stability on smaller hosts.NEXT_STATIC_PAGE_GENERATION_TIMEOUTraises Next.js' static generation timeout for slower builders. Increase it only when legitimate static pages time out.
Production Host
Yayaw production uses https://yayaw.app as the canonical public origin.
Preview deployments use https://preview.yayaw.app for the branch-backed
preview domain.
NEXT_PUBLIC_BASE_URL=https://yayaw.app
BETTER_AUTH_TRUSTED_ORIGINS=https://*.yayaw.app,https://*.vercel.appNEXT_PUBLIC_BASE_URLis the application source of truth for canonical URLs, Better Auth base URL, OAuth metadata, sitemap/robots links, and generated absolute asset URLs.NEXT_PUBLIC_SITE_URLis not read by the application.BETTER_AUTH_URLis not required by the current runtime because Better Auth receivesbaseURLfromNEXT_PUBLIC_BASE_URL. If a legacy deployment still defines it, keep it aligned withhttps://yayaw.app.preview.yayaw.appis the branch-backed preview domain and tracks the durablepreviewGit branch.- Keep
www.yayaw.appas a Vercel project-domain redirect toyayaw.app; the app also normalizeswwwpage requests before i18n routing. - Keep retired
.euhosts only as Vercel308redirects to their.appreplacements, such asyayaw.euandwww.yayaw.eutoyayaw.app. Do not add retired hosts toBETTER_AUTH_TRUSTED_ORIGINS. - Organization public domains are separate from the canonical app host. They
are verified through the configured public-domain provider and then mapped
through
organization_public_domains; they do not belong inBETTER_AUTH_TRUSTED_ORIGINSbecause dashboard/auth are not served from those hosts.
Billing and Stripe
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
STRIPE_ONE_TIME_WEBHOOK_SECRET=
BILLING_GRACE_PERIOD_DAYS=7
BILLING_PRO_SEAT_LIMIT=10
BILLING_BUSINESS_SEAT_LIMIT=100
BILLING_CODE_ACCESS_REPOSITORY_URL=
BILLING_CODE_ACCESS_DOWNLOAD_URL=
BILLING_CODE_ACCESS_DOCUMENTATION_URL=
BILLING_CODE_ACCESS_SUPPORT_URL=
BILLING_CODE_ACCESS_GITHUB_REPOSITORY=
BILLING_CODE_ACCESS_GITHUB_APP_ID=
BILLING_CODE_ACCESS_GITHUB_APP_INSTALLATION_ID=
BILLING_CODE_ACCESS_GITHUB_APP_PRIVATE_KEY=
BILLING_CODE_ACCESS_GITHUB_TOKEN=STRIPE_WEBHOOK_SECRETis used by the Better Auth Stripe plugin webhook endpoint.STRIPE_ONE_TIME_WEBHOOK_SECRETis used by the custom one-time webhook endpoint.- Billing product prices are managed from admin or MCP and synced to Stripe; resulting Stripe price IDs are stored internally in
billing_products. BILLING_CODE_ACCESS_*URLs are optional non-secret deliverable links shown on/dashboard/organization/code-accessafter an eligible purchase or active subscription. Leave a value empty when that deliverable requires manual provisioning.- GitHub repository access for paid code access is configured from
/dashboard/admin/billing-settings. Non-secret values can also be supplied as environment fallbacks withBILLING_CODE_ACCESS_GITHUB_REPOSITORY,BILLING_CODE_ACCESS_GITHUB_APP_ID, andBILLING_CODE_ACCESS_GITHUB_APP_INSTALLATION_ID. - Keep GitHub secrets in environment variables only:
BILLING_CODE_ACCESS_GITHUB_APP_PRIVATE_KEYfor production GitHub App provisioning, orBILLING_CODE_ACCESS_GITHUB_TOKENas an optional local/staging fallback. - See Deployment Environment Setup for the GitHub App creation, installation ID, private key, and token fallback steps.
Canonical Host and Auth Sessions
- Set
NEXT_PUBLIC_BASE_URLto the canonical production host. - Set preview
NEXT_PUBLIC_BASE_URLto the branch-backed preview host when a stable preview domain is configured. - Do not mix
wwwand non-wwwhosts for authenticated sessions. - Keep
BETTER_AUTH_TRUSTED_ORIGINSfor required preview/local hosts. The canonical host and itswwwvariant are already derived fromNEXT_PUBLIC_BASE_URL. - For locale-prefixed routes, prefer
@/i18n/navigation(Link,useRouter,usePathname) instead ofnext/linkandnext/navigationin app navigation components.
OAuth (Optional)
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
FACEBOOK_CLIENT_ID=
FACEBOOK_CLIENT_SECRET=Email (Optional but required for invitation/reset/magic-link features)
RESEND_API_KEY=
EMAIL_SENDER=team@yayaw.app
EMAIL_SUPPORT=support@yayaw.app
EMAIL_USERNAME=Yayaw TeamEMAIL_SENDER is the verified Resend sender address used in From headers.
EMAIL_SUPPORT is shown in templates and support copy. EMAIL_USERNAME is the
display name paired with the sender address. These variables are bootstrap
defaults and fallbacks; runtime values saved in Admin > Site Settings > Email
take priority after setup.
Storage (Optional for media features)
Media storage uses an S3-compatible API such as MinIO, AWS S3, or R2.
STORAGE_PROVIDER should remain s3.
STORAGE_PROVIDER=s3
STORAGE_MEDIA_BUCKET=media
STORAGE_MEDIA_STAGING_BUCKET=media-staging
STORAGE_CMS_FORM_BUCKET=cms-form-submissions
STORAGE_PUBLIC_BASE_URL=
CMS_MEDIA_STAGED_UPLOADS_ENABLED=false
STORAGE_TRANSFER_SIGNING_SECRET=
CMS_PERSONALIZATION_RATE_LIMIT_SECRET=
CMS_PERSONALIZATION_FORM_CONTEXT_SECRET=
CMS_PERSONALIZATION_CLIENT_IP_HEADER=x-real-ip
S3_ENDPOINT=
S3_SIGNED_PUBLIC_ENDPOINT=
S3_REGION=us-east-1
S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=
S3_FORCE_PATH_STYLE=trueSTORAGE_MEDIA_BUCKETdefaults tomedia.CMS_MEDIA_STAGED_UPLOADS_ENABLEDdefaults tofalse. Enable it only after every application instance runs the staged-upload-compatible release.STORAGE_MEDIA_STAGING_BUCKETselects the private bucket for signed client uploads. Development can fall back toSTORAGE_MEDIA_BUCKET, but production staged uploads fail closed unless this value names a distinct bucket. Keep that bucket private and configure a provider lifecycle rule to remove abandoned staging objects after the upload-session retention window.STORAGE_CMS_FORM_BUCKETselects the durable private bucket for customer attachments submitted through registered public CMS forms. It can fall back toSTORAGE_MEDIA_STAGING_BUCKET, but it must never equal the public media bucket. Prefer a dedicated bucket. When both flows share one private bucket, scope staging lifecycle rules to their staging object prefix so they do not remove durable form attachments.STORAGE_TRANSFER_SIGNING_SECREToptionally separates opaque, short-lived same-origin storage grants from other signatures. It falls back toCMS_PREVIEW_SIGNING_SECRET, thenBETTER_AUTH_SECRET, and the effective production secret must contain at least 32 characters. The application encrypts and authenticates the bucket, object key, operation, MIME type, byte bound, and expiry; client URLs useNEXT_PUBLIC_BASE_URLand never exposeS3_ENDPOINT. Keep private buckets non-anonymous. By default, staged uploads stream through the application with an exact 250 MB ceiling, conditional no-overwrite storage, and a deadline no later than the grant expiry.S3_SIGNED_PUBLIC_ENDPOINTis an explicit opt-in public S3 API origin for direct signed uploads when the application host has a lower request-body limit than the 250 MB CMS media contract. It never falls back toS3_ENDPOINT; production accepts only a safe HTTPS hostname. The returned direct PUT signsIf-None-Match: *, and clients must send every returned header. Its bucket CORS policy must allow the application origin,PUT, and theContent-Type,Content-Length, andIf-None-Matchrequest headers. Downloads, including personalization photos, always use the opaque application proxy. Leave this blank for Coolify and self-hosted deployments where the app can stream the request to the internal S3 endpoint. Personalization photo downloads use an audience-bound, durable one-use grant with a maximum two-minute TTL. The proxy atomically consumes the current grant before reading storage, and issuing another URL invalidates the prior one.CMS_PERSONALIZATION_RATE_LIMIT_SECRETis an optional dedicated HMAC secret for the public personalization-request abuse bucket. It must contain at least 32 characters and falls back toCMS_PREVIEW_SIGNING_SECRET, thenBETTER_AUTH_SECRET. The abuse bucket stores only the HMAC subject, never the raw client IP, and the customer request row is not linked to that subject.CMS_PERSONALIZATION_FORM_CONTEXT_SECRETis required and must contain at least 32 characters. It signs the public runtime's short-lived form context, binding submissions to the registered handler and action, page identity, immutable revision, scope, organization, locale, path, and origin. Keep it distinct so it can be rotated independently.CMS_PERSONALIZATION_CLIENT_IP_HEADERis required in production and names one proxy-owned header containing exactly one canonical client IP. Configure the ingress to remove any client-supplied value and set the trusted value.x-forwarded-foris rejected because its first hop is not an authenticated client identity. The bundled Caddy configurations overwritex-real-ip, so that is the self-host default; choose the equivalent canonical header for another ingress only after verifying that it overwrites client input. The endpoint atomically enforces six submissions per trusted IP and 1,200 submissions globally per hour, then the worker prunes expired abuse buckets.STORAGE_PUBLIC_BASE_URLis required for S3-compatible storage and must be a public URL that can serve public/<bucket>/<object-key>paths. It is never used for private staging or CMS form buckets.- If
STORAGE_PUBLIC_BASE_URLuses the app or CDN origin, route each public bucket prefix to the object store before the app fallback. The built-in public prefixes are/media/*and/organization-logos/*. - S3-compatible storage requires endpoint, region, access key, secret key, and
public base URL. Keep
S3_FORCE_PATH_STYLE=truefor MinIO and most local S3 compatible endpoints.S3_ENDPOINTmay remain an internal Docker or private network hostname because browsers never receive it. A successfully promoted staged object remains as a private conditional-write tombstone through grant expiry and a 24-hour settlement window. The post-expiry reaper repeats deletion and records cleanup only after its final delete. This prevents a valid bearer grant or a late direct PUT from recreating durable staging after promotion, including across app instances. bun run seeduploads default global site-variable assets intoSTORAGE_MEDIA_BUCKETwhen storage is configured. Without storage credentials, the seed keeps local public-asset fallbacks so local setup can still complete.- Existing media rows store absolute public URLs, so changing providers later requires either keeping old URLs reachable or running a deliberate media URL migration.
OpenAI (Optional for AI builder features)
OPENAI_API_KEY=
OPENAI_COMPONENTS_AI_FALLBACK=true
OPENAI_IMAGE_GENERATION_ENABLED=true
OPENAI_IMAGE_MODEL=gpt-image-1.5
PAGE_AI_QUEUE_DRIVER=direct
PAGE_AI_DEEP_REFINEMENT=false
PAGE_AI_WORKER_POLL_MS=1500
PAGE_AI_WORKER_MAINTENANCE_MS=60000
PAGE_AI_WORKER_ID=OPENAI_COMPONENTS_AI_FALLBACKcontrols text/object AI fallbacks for component and page builder flows.OPENAI_IMAGE_GENERATION_ENABLEDcontrols page-builder image generation.- Runtime site settings can also disable these AI features through
ai-components-enabledandmedia-image-generation-enabledwithout changing environment variables. - Generated page-builder images use the configured OpenAI image model, default
to
gpt-image-1.5, and are stored aswebpmedia assets through the organization media library. PAGE_AI_QUEUE_DRIVERcontrols the durable Page AI wake-up transport:directfor local development,vercel-queuefor Vercel, anddb-workerfor a long-lived worker process. In production, the default isvercel-queueonly when Vercel runtime variables are present; otherwise it isdb-worker.PAGE_AI_DEEP_REFINEMENTis an internal environment-only quality toggle and is not exposed as an admin site setting.PAGE_AI_WORKER_POLL_MS,PAGE_AI_WORKER_MAINTENANCE_MS, andPAGE_AI_WORKER_IDare only used bybun run worker:page-aiwhenPAGE_AI_QUEUE_DRIVER=db-worker. The maintenance interval defaults to one minute and drives durable personalization retention, orphan-photo recovery, and expired abuse-bucket pruning on an independent non-overlapping timer, including while a Page AI run is long-lived or blocked. Personalization PUTs are aborted after two minutes; ambiguous uploads keep a PII-free cleanup tombstone until a second object delete runs after a three-minute settle window.
PostHog (Optional for analytics and flags)
NEXT_PUBLIC_ANALYTICS_PROVIDER=posthog
NEXT_PUBLIC_ANALYTICS_CAPTURE_MODE=hybrid
NEXT_PUBLIC_POSTHOG_KEY=
NEXT_PUBLIC_POSTHOG_HOST=https://eu.i.posthog.com
NEXT_PUBLIC_POSTHOG_ENABLE_LOCAL=false
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=NEXT_PUBLIC_POSTHOG_* values are used for capture, feature flags, and client
identification. POSTHOG_PERSONAL_API_KEY, POSTHOG_PROJECT_ID, and
POSTHOG_API_HOST are private server-only values used by /dashboard analytics
to query PostHog through the private Query API. POSTHOG_ORG_ID_PROPERTY
defaults to organization_id and must match the event property registered for
organization-scoped dashboard metrics. POSTHOG_FLAG_LOOKUP_TIMEOUT_MS
optionally shortens or lengthens the server-side PostHog feature-flag lookup
timeout; it defaults to 1500.
NEXT_PUBLIC_ANALYTICS_CAPTURE_MODE controls where events are captured:
hybrid keeps browser analytics for product behavior and sends server-side
billing/auth events, server disables browser analytics scripts and tracks CMS
page views plus conversions from the server, and client preserves legacy
browser-only capture. Server-side billing events include conversion properties
such as revenue, value, currency, plan, product_key, and Stripe IDs.
Umami (Optional Analytics Provider)
NEXT_PUBLIC_ANALYTICS_PROVIDER=umami
NEXT_PUBLIC_ANALYTICS_CAPTURE_MODE=hybrid
NEXT_PUBLIC_UMAMI_HOST_URL=
NEXT_PUBLIC_UMAMI_SCRIPT_URL=
NEXT_PUBLIC_UMAMI_WEBSITE_ID=
NEXT_PUBLIC_UMAMI_DOMAINS=
NEXT_PUBLIC_UMAMI_AUTO_TRACK=true
UMAMI_API_URL=
UMAMI_WEBSITE_ID=
UMAMI_API_TOKEN=
UMAMI_API_KEY=
UMAMI_USERNAME=
UMAMI_PASSWORD=
UMAMI_CMS_EVENT_PAGE_SIZE=1000NEXT_PUBLIC_UMAMI_* values configure the browser tracking script and are
baked into the client bundle at build time. UMAMI_API_URL, UMAMI_WEBSITE_ID,
and either UMAMI_API_TOKEN (or the legacy alias UMAMI_API_KEY) or
UMAMI_USERNAME plus UMAMI_PASSWORD are server-only values used by dashboard
analytics data providers.
For a no-browser-analytics setup, set NEXT_PUBLIC_ANALYTICS_CAPTURE_MODE=server.
The app will not render the Umami script, and server events are sent directly to
Umami /api/send with a server User-Agent. This improves privacy and avoids
client cookies, but session, unique visitor, device, and referrer quality is less
precise than browser capture.
Control Plane MCP (Optional)
YAYAW_MCP_API_KEY=
YAYAW_MCP_LOCAL_USER_ID=
CMS_PREVIEW_SIGNING_SECRET=
CMS_LEGACY_PROTOTYPE_WRITES_ENABLED=false
CMS_PROTOTYPE_RUNTIME_V2_WRITES_ENABLED=false
CMS_PROTOTYPE_ROLLOUT_OPERATION_ID=
CMS_LEGACY_PAGE_DESIGN_ADMIN_BYPASS_ENABLED=false
CMS_LOW_LEVEL_PAGE_ADMIN_BYPASS_ENABLED=falseYAYAW_MCP_API_KEYis used by the local stdio MCP launcher when you want local development to verify a real Better Auth API key.YAYAW_MCP_LOCAL_USER_IDis only used by the local stdio launcher when no API key is provided.- Production MCP clients connect to
/api/mcpwithAuthorization: Bearer <Yayaw API key or OAuth access token>and should store secrets outside the repository. - OAuth MCP metadata is derived from
NEXT_PUBLIC_BASE_URL; production ChatGPT/App clients need that value to be the public HTTPS origin so issuer, resource, JWKS, and redirect metadata are stable. CMS_PREVIEW_SIGNING_SECREToptionally separates short-lived immutable CMS draft preview signatures from the authentication secret. Use a stable high-entropy server-only value in production; the runtime falls back toBETTER_AUTH_SECRETwhen it is empty.- Production prototype writers use three explicit states. Reader-first uses
CMS_LEGACY_PROTOTYPE_WRITES_ENABLED=trueandCMS_PROTOTYPE_RUNTIME_V2_WRITES_ENABLED=false. The deployment freeze and every later rolling renderer deployment use both values asfalse. Exact runtime activation uses legacyfalseand v2true, after private staged uploads are verified. Bothtruefails closed. Never re-enable legacy writes after the first v2 activation. CMS_PROTOTYPE_ROLLOUT_OPERATION_IDis managed by the trusted manual rollout. It is empty in reader/freeze and identifies the activating, eventually consumed durable receipt in exact mode. Do not set or rotate it independently ofCMS Prototype Runtime Rollout.CMS_LEGACY_PAGE_DESIGN_ADMIN_BYPASS_ENABLEDdefaults tofalse. Set it totrueonly for a time-bounded migration or incident response by a client withcontrol-plane:admin; normal page design must use the prepared contextual workflow.CMS_LOW_LEVEL_PAGE_ADMIN_BYPASS_ENABLEDdefaults tofalse. Enable it only for a time-bounded, audited maintenance operation that must callyayaw_pages_create_draftoryayaw_pages_save_draft; normal creation and rework use the contextual design tools.
Dynamic Data Runtime Transforms (Optional)
DYNAMIC_DATA_RUNTIME_TRANSFORM_SECRET=DYNAMIC_DATA_RUNTIME_TRANSFORM_SECRETis required only when a deployed dynamic runtime route declares anhmac_sha256value transform. Generate a stable high-entropy server-only value and rotate it deliberately, because existing transformed lookup values depend on it.
Maintenance Mode (Optional)
MAINTENANCE_MODE=false
MAINTENANCE_MODE_END_DATE=Deployment Runtime
DEPLOYMENT_PROVIDER=
DEPLOYMENT_URL=
DEPLOYMENT_ENV=
DEPLOYMENT_GIT_COMMIT_SHA=
DEPLOYMENT_GIT_COMMIT_REF=
PUBLIC_DOMAIN_PROVIDER=
APP_MANAGED_HOSTS=
RESERVED_PUBLIC_DOMAIN_SUFFIXES=
PUBLIC_DOMAIN_CNAME_TARGET=
PUBLIC_DOMAIN_IPV4_TARGETS=
PUBLIC_DOMAIN_TXT_PREFIX=_yayaw
VERCEL_URL=
VERCEL_TOKEN=
VERCEL_PROJECT_ID=
VERCEL_TEAM_ID=DEPLOYMENT_PROVIDERcan bevercel,static, orlocal. Leave it empty to auto-detect Vercel fromVERCEL/VERCEL_URL, static deployments fromDEPLOYMENT_URL, and local otherwise.DEPLOYMENT_URL,DEPLOYMENT_ENV,DEPLOYMENT_GIT_COMMIT_SHA, andDEPLOYMENT_GIT_COMMIT_REFprovide dashboard/control-plane deployment metadata for Docker or other non-Vercel runtimes.PUBLIC_DOMAIN_PROVIDERcan bevercelormanual-dns. When unset, Vercel is selected only ifVERCEL_PROJECT_IDandVERCEL_TOKENare configured; otherwise manual DNS verification is used.APP_MANAGED_HOSTSadds comma-separated app-owned hosts that organization public domains may not claim.RESERVED_PUBLIC_DOMAIN_SUFFIXESadds suffixes, such as.preview.example, that custom public domains may not claim..vercel.appis always reserved.PUBLIC_DOMAIN_CNAME_TARGET,PUBLIC_DOMAIN_IPV4_TARGETS, andPUBLIC_DOMAIN_TXT_PREFIXdrive the manual DNS provider's hints and ownership TXT challenge.VERCEL_URLis supplied by Vercel and lets the app recognize deployment hosts as managed app hosts.VERCEL_TOKEN,VERCEL_PROJECT_ID, and optionalVERCEL_TEAM_IDare server-only values used to add, inspect, and verify organization public domains through the Vercel project-domain API.
Source of Truth
Keep .env.example in sync with real usage in source code when adding or removing variables.
Document retrieval steps in
Deployment Environment Setup whenever a
deployment operator needs to collect the value from an external provider.
Local Tooling Troubleshooting
Some local machines may hit an esbuild service hang when running Drizzle or docs generators.
If a command appears blocked:
- Use the safe scripts with timeouts:
bun run docs:generate
bun run db:generate
bun run db:push- If it still fails, reinstall dependencies:
rm -rf node_modules
bun install- Retry the command and verify with:
bun run check
bun run build