BunshipBunship

Environment and Configuration

Required environment variables for Bunship web + API runtime.

This page is the source of truth for required env vars in the current repo.

Runtime public config now uses PUBLIC_* names. The legacy NEXT_PUBLIC_* names are still accepted as fallbacks for compatibility, but only PUBLIC_AUTH_PROVIDER is a build-time variable used to choose the auth provider bundle.

Code references:

  • web schema: apps/ship/src/env.ts
  • auth runtime: packages/auth/src/server.ts, packages/auth-clerk/src/server.ts
  • api runtime: apps/ship-api/src/server.ts and apps/ship-api/src/index.ts
  • Trigger runtime sync: apps/ship-api/trigger.config.ts
  • Docker build: Dockerfile, .github/workflows/publish-ghcr.yml

Environment Ownership Matrix

Classify env vars by who reads them first. Do not mix build inputs, container runtime env, and Trigger runtime env.

ScenarioEnv ownershipStores business secretsNotes
Docker build / GHCR publishGIT_COMMIT_SHA, VERCEL_ENV, PUBLIC_AUTH_PROVIDER, plus the optional BuildKit secret file infisical_envNoOnly controls image version and auth-provider build variant. Dockerfile sets SKIP_ENV_VALIDATION=1, so the default build does not need business runtime env.
Infisical control vars for GHCR Docker buildINFISICAL_IDENTITY_ID, INFISICAL_PROJECT_SLUG, INFISICAL_ENV_SLUG, INFISICAL_DOMAIN, INFISICAL_SECRET_PATH, INFISICAL_RECURSIVE, INFISICAL_INCLUDE_IMPORTSNoOnly used by publish-ghcr.yml to locate the Infisical project/environment/folder through OIDC, write .infisical.docker.env, and pass it to Docker as a BuildKit secret.
Trigger deploy CITRIGGER_ACCESS_TOKEN, TRIGGER_PROJECT_IDTRIGGER_ACCESS_TOKEN is secretOnly used by deploy-trigger.yml to deploy tasks. It is not synced as a Trigger task runtime variable.
Web/API container runtimeSITE_URL, DATABASE_URL, auth, email, payment, storage, OAuth, public runtime config, etc.YesInjected by the deployment platform when the container starts. OAuth client IDs/secrets belong only here.
Web/API Trigger dispatchTRIGGER_SECRET_KEY, optional TRIGGER_PROJECT_IDYesLets Web/API select the Trigger adapter and enqueue runs on Trigger.dev. Not a Docker build variable.
Trigger cloud task runtimeDATABASE_URL, S3_*, PUBLIC_S3_URL_BASE, provider API keys, AI tuning varsYesTasks run on Trigger.dev cloud and need their own env sync/config. OAuth client IDs are not needed.
BullMQ runtimeREDIS_URLYesUsed by self-hosted persistent queue workers; choose BullMQ or Trigger, not both.

Minimum Required (Web + API)

Without these variables, startup or core auth/payment/storage flows will break.

VariableRequiredUsed byNotes
PUBLIC_AUTH_PROVIDERYes (default better-auth)web, apibetter-auth or clerk
DATABASE_URLYesweb, api, authPostgres DSN
SITE_URLYesweb, api, authCanonical site origin used for SEO, emails, auth links, and absolute URLs
TRUSTED_ORIGINSRecommendedapi, authComma-separated extra allowed origins for Better Auth and CORS
PUBLIC_SERVER_URLOptionalwebRuntime API origin override for browser requests. Set it only when the public API origin differs from SITE_URL; with VERCEL_ENV the target must expose /api/v1, otherwise it must expose /v1. Legacy fallback: NEXT_PUBLIC_SERVER_URL.
ADMIN_EMAIL_LISTYesweb, authComma-separated admin emails
EMAIL_FROMYesauthSender for verification/reset/OTP emails
RESEND_API_KEYYesweb/authEmail provider key
S3_ENDPOINTYesweb, apiStorage endpoint
S3_REGIONYesweb, apiStorage region
S3_ACCESS_KEYYesweb, apiStorage access key
S3_SECRET_KEYYesweb, apiStorage secret
S3_BUCKETYesweb, apiBucket name
PUBLIC_S3_URL_BASEYesweb, apiPublic object URL base. Legacy fallback: NEXT_PUBLIC_S3_URL_BASE.
STRIPE_SECRET_KEYYesweb, apiStripe API key
STRIPE_WEBHOOK_SECRETYesweb, apiStripe webhook signature secret
CREEM_API_KEYConditionalweb, apiCreem API key (required if using Creem)
CREEM_WEBHOOK_SECRETConditionalweb, apiCreem webhook signature secret
PAYMENT_PROVIDER_DEFAULTNo (default stripe)apiDefault payment provider when product has no active config
S_GITHUB_PERSONAL_ACCESS_TOKENYesweb, apiGitHub API access in admin/features
CLOUDFLARE_ACCOUNT_IDYes (in web schema)webRequired by current env validator

Upload Provider Selection

The shared S3_* variables above are the baseline storage config. Better Upload server routes can also select a provider dynamically from settings, then fall back to env. This is a server-side configuration mechanism, not an end-user provider switcher UI.

Provider resolution order:

  1. UPLOAD_PROVIDER_OVERRIDES[userId] in the settings table
  2. UPLOAD_PROVIDER_DEFAULT in the settings table
  3. BETTER_UPLOAD_PROVIDER
  4. cloudflare
VariableRequiredUsed byNotes
BETTER_UPLOAD_PROVIDERNo (default cloudflare)apiEnv fallback provider when no settings override applies. Supported values: cloudflare, aws, backblaze, tigris, digitalocean, minio, wasabi, custom. Invalid values are ignored and fall back to cloudflare.
BETTER_UPLOAD_<PROVIDER>_BUCKETNoapiProvider-specific bucket fallback, for example BETTER_UPLOAD_AWS_BUCKET or BETTER_UPLOAD_CUSTOM_BUCKET.
BETTER_UPLOAD_BUCKETNoapiShared bucket fallback before S3_BUCKET.

Credentials and client fields are resolved from the client object in settings first, then from env aliases such as AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY and S3_ACCESS_KEY / S3_SECRET_KEY. Configure path-style or provider-specific client behavior in the settings client object, not as a global env variable.

See S3-Compatible Storage for per-user provider examples and bucket resolution details.

Better Auth only (when PUBLIC_AUTH_PROVIDER=better-auth)

VariableRequiredUsed byNotes
BETTER_AUTH_SECRETYesauth/apiBetter Auth signing secret
BETTER_AUTH_URLNo (defaults to SITE_URL)auth/apiOverride auth/API public base URL when it differs from the site origin
AUTH_SECRETYesweb/authRequired by web env schema
OAUTH_GITHUB_CLIENT_IDYes (if GitHub OAuth enabled)web/authGitHub OAuth
OAUTH_GITHUB_CLIENT_SECRETYes (if GitHub OAuth enabled)web/authGitHub OAuth
OAUTH_GOOGLE_CLIENT_IDRecommendedauthGoogle OAuth server client ID
OAUTH_GOOGLE_CLIENT_SECRETRecommendedauthGoogle OAuth server secret
PUBLIC_OAUTH_GOOGLE_CLIENT_IDYes (if Google OAuth enabled)webGoogle One Tap / Sign-In UI. Legacy fallback: NEXT_PUBLIC_OAUTH_GOOGLE_CLIENT_ID.

Clerk only (when PUBLIC_AUTH_PROVIDER=clerk)

VariableRequiredUsed byNotes
PUBLIC_CLERK_PUBLISHABLE_KEYYeswebClerk public key. Legacy fallback: NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY.
CLERK_SECRET_KEYYesapi/authClerk server key
CLERK_WEBHOOK_SECRETYesapiUsed by /api/v1/webhook/clerk

API Runtime / Deployment Variables

The mounted API route prefix still depends on the entrypoint:

  • Next.js ship route surface: /api/v1/*
  • Standalone ship-api service: /v1/*

When PUBLIC_SERVER_URL is omitted, the web app talks to the same-origin /api/v1 surface under SITE_URL. Set it only when the browser-facing API origin differs from SITE_URL. With VERCEL_ENV present, browser requests use ${PUBLIC_SERVER_URL}/api/v1; without VERCEL_ENV, they use ${PUBLIC_SERVER_URL}/v1 for standalone ship-api or Worker-style API origins.

VariableRequiredNotes
PORTNo (default 9001)API listen port
APP_ENVRecommendeddevelopment / production style env flag
NODE_ENVRecommendedNode runtime env
JWT_SECRETRecommendedAPI JWT signing secret (fallback exists but not safe for production)
JWT_EXPIRATIONNo (default 7d)API JWT TTL
GIT_COMMIT_SHANoBuild metadata for logs
BUILD_TIMENoBuild metadata for logs
CROSS_SUB_DOMAINOptionalCross-subdomain cookie domain

Production Address Variables

Use this table when updating Infisical, container runtime env, or host-level env.

VariableRuntime actionRequiredNotes
SITE_URLKeep/addYesCanonical site origin, e.g. https://app.example.com
TRUSTED_ORIGINSKeep/addRecommendedComma-separated trusted origins for Better Auth and CORS
BETTER_AUTH_URLKeep only if neededNoDefaults to SITE_URL; set only when auth/API public origin differs
VERCEL_ENVKeep/defaultNoDocker build arg defaults to production; Cloudflare/Vercel runtime should explicitly stay production
APP_ENVKeepRecommendedBusiness/runtime environment flag
PUBLIC_SERVER_URLSet only if neededNoUse only when the browser-facing API origin differs from SITE_URL; with VERCEL_ENV the target must expose /api/v1, otherwise /v1; legacy NEXT_PUBLIC_SERVER_URL still works
NEXT_PUBLIC_SITE_URLDeleteNoReplaced by SITE_URL
NEXT_PUBLIC_API_PREFIXDeleteNoReplaced by shared constants
CORS_ORIGINDeleteNoReplaced by TRUSTED_ORIGINS
API_PREFIXDeleteNoPrefix is no longer env-driven

GitHub Variables for Infisical OIDC (GHCR Docker Build)

Add these values as GitHub Repository Variables, not source code. Entry: GitHub repo > Settings > Secrets and variables > Actions > Variables > New repository variable.

These variables are for the Load Infisical secrets step in publish-ghcr.yml. They only let GitHub Actions locate the Infisical project/env/folder through OIDC; they are not business runtime secrets. Trigger deploy does not need this Infisical OIDC set.

publish-ghcr.yml already declares permissions.id-token: write, so OIDC auth method not found for identity usually does not mean GitHub Actions cannot issue an OIDC token. It means the Infisical identity referenced by INFISICAL_IDENTITY_ID does not have OIDC Auth configured. A newly created machine identity may default to Universal Auth; for GitHub Actions, remove/replace Universal Auth with OIDC Auth.

GitHub VariableWhere to get it in InfisicalNotes
INFISICAL_IDENTITY_IDInfisical > Project > Access Control > Machine Identities, open the identity used by GitHub Actions; or find the same identity under Organization Settings > Access Control > IdentitiesThis is not a secret. Authentication must include OIDC Auth. For GitHub Actions, use https://token.actions.githubusercontent.com for both OIDC Discovery URL and Issuer. This workflow uses GitHub Environments, so use repo:<owner>/<repo>:environment:Production for main and repo:<owner>/<repo>:environment:staging for dev; only use repo:<owner>/<repo>:ref:refs/heads/main for workflows that do not reference a GitHub Environment.
INFISICAL_PROJECT_SLUGInfisical > Bunship project > Project Settings > General > Project SlugMust exactly match the Infisical project slug accessed by the workflow.
INFISICAL_ENV_SLUGInfisical > Bunship project > Project Settings > Environments, copy the production environment slugFor example prod / production; this is case-sensitive and must exactly match the Infisical environment slug.
INFISICAL_DOMAINThe current Infisical instance URLInfisical Cloud defaults to https://app.infisical.com, so this can be omitted unless using a self-hosted instance.
INFISICAL_SECRET_PATHInfisical project secret folder pathOptional. Defaults to /. If the folder is named env under the root, set /env. This is not the same as INFISICAL_ENV_SLUG.
INFISICAL_RECURSIVEGitHub Repository VariablesOptional. Defaults to false. Set true only when the Docker build inputs live in nested folders under INFISICAL_SECRET_PATH.

Troubleshooting order:

  1. OIDC auth method not found for identity: open the INFISICAL_IDENTITY_ID identity from Project > Access Control > Machine Identities and confirm Authentication includes OIDC Auth. If it still uses Universal Auth, remove/replace it with OIDC Auth.
  2. Subject mismatch: this workflow uses GitHub Environments, so the production branch subject is repo:<owner>/<repo>:environment:Production, not repo:<owner>/<repo>:ref:refs/heads/main.
  3. Folder with path '/' ... was not found: INFISICAL_ENV_SLUG or INFISICAL_SECRET_PATH does not match Infisical. If the folder is env under the root, set INFISICAL_SECRET_PATH=/env.

Workflow entry points:

FilePurpose
.github/workflows/publish-ghcr.ymlGHCR Docker image build/push. It runs Load Infisical secrets and passes the result to the Dockerfile as a BuildKit secret file.
.github/workflows/deploy-trigger.ymlTrigger.dev deploy. It does not use Infisical OIDC; it reads Trigger deploy/sync values from GitHub Secrets/Variables.

GHCR Build Inputs

The private GHCR image is built by .github/workflows/publish-ghcr.yml using Dockerfile. The workflow builds a Better Auth image under the default tags (latest, branch tag, <branch>-<sha>) and a Clerk image under clerk-latest / clerk-<sha>. Runtime env and runtime public config are injected by the running container, so the same provider-specific image can be reused across environments.

Build args:

VariableRequiredNotes
GIT_COMMIT_SHANoSet by the workflow to the GitHub SHA; defaults to local
VERCEL_ENVNoDefaults to production for image builds
PUBLIC_AUTH_PROVIDERProvided by workflow matrixbetter-auth for default tags, clerk for clerk-* tags

BuildKit secret file: the workflow writes the Infisical output to .infisical.docker.env and passes it to Docker build as infisical_env. The Dockerfile only reads that file during the build; it is not copied into image layers. Use it only for build-time/public config such as VERCEL_ENV or PUBLIC_*. The Docker build sets SKIP_ENV_VALIDATION=1 and must not require runtime-only values such as SITE_URL, DATABASE_URL, S3_*, auth secrets, Stripe keys, or email keys. Those belong to container runtime env.

OAuth variables are not Docker build inputs. OAUTH_GITHUB_CLIENT_ID / OAUTH_GOOGLE_CLIENT_ID are only Web/API auth runtime config when GitHub/Google login is enabled, and they should be injected at container runtime together with the matching *_CLIENT_SECRET. Trigger/AI runtime does not need these two variables.

Trigger dispatch variables are not Docker build inputs either. TRIGGER_SECRET_KEY belongs in Web/API container runtime only, where it selects the Trigger adapter and lets the app enqueue Trigger.dev runs. Do not put it in GHCR build secrets.

Container runtime public config:

VariableRequiredLegacy fallbackNotes
PUBLIC_S3_URL_BASEYesNEXT_PUBLIC_S3_URL_BASEPublic CDN/object URL base
PUBLIC_OAUTH_GOOGLE_CLIENT_IDIf Google OAuth enabledNEXT_PUBLIC_OAUTH_GOOGLE_CLIENT_IDPublic Google client ID for One Tap / Sign-In UI
PUBLIC_CLERK_PUBLISHABLE_KEYIf Clerk enabledNEXT_PUBLIC_CLERK_PUBLISHABLE_KEYClerk publishable key
PUBLIC_SERVER_URLNoNEXT_PUBLIC_SERVER_URLOptional API origin override when it differs from SITE_URL; with VERCEL_ENV target must expose /api/v1, otherwise /v1
PUBLIC_GA_IDNoNEXT_PUBLIC_GA_IDGoogle Analytics
PUBLIC_UMAMI_DATA_IDNoNEXT_PUBLIC_UMAMI_DATA_IDUmami analytics

Optional Feature Variables

VariableFeature
UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKENKV/Redis features
TRIGGER_SECRET_KEY, TRIGGER_PROJECT_IDWeb/API runtime dispatch to Trigger.dev; do not put these in Docker build
REDIS_URLBullMQ/CMS queues and AI admission/rate metrics; required when using BullMQ
CRON_SECRETAuth for the /api/cron/ai-cleanup cron route
PUBLIC_GA_ID, PUBLIC_UMAMI_DATA_IDAnalytics; legacy NEXT_PUBLIC_* names still work
NEXT_PUBLIC_APP_VERSION, VERCEL_GIT_COMMIT_SHAUI version display
BETTER_UPLOAD_PROVIDERUpload provider fallback; see the upload provider section above
OPENAI_API_KEY, OPENAI_API_BASEAdmin AI command/copilot
REPLICATE_API_TOKENReplicate provider
KIE_API_KEY, KIE_API_BASE_URLKIE provider
FAL_API_KEYFAL provider

Local .env Example (Better Auth)

PUBLIC_AUTH_PROVIDER=better-auth

DATABASE_URL=postgresql://postgres:postgres@localhost:5432/bunship
BETTER_AUTH_SECRET=replace-with-long-random-secret
AUTH_SECRET=replace-with-long-random-secret

SITE_URL=http://localhost:3001
TRUSTED_ORIGINS=http://localhost:3001,http://localhost:9001

ADMIN_EMAIL_LIST=admin@example.com
EMAIL_FROM=Bunship <noreply@example.com>
RESEND_API_KEY=re_xxx

OAUTH_GITHUB_CLIENT_ID=xxx
OAUTH_GITHUB_CLIENT_SECRET=xxx
OAUTH_GOOGLE_CLIENT_ID=xxx
OAUTH_GOOGLE_CLIENT_SECRET=xxx
PUBLIC_OAUTH_GOOGLE_CLIENT_ID=xxx

S3_ENDPOINT=https://s3.example.com
S3_REGION=auto
S3_ACCESS_KEY=xxx
S3_SECRET_KEY=xxx
S3_BUCKET=bunship
PUBLIC_S3_URL_BASE=https://cdn.example.com

STRIPE_SECRET_KEY=sk_test_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
CREEM_API_KEY=creem_xxx
CREEM_WEBHOOK_SECRET=creem_whsec_xxx
PAYMENT_PROVIDER_DEFAULT=stripe
S_GITHUB_PERSONAL_ACCESS_TOKEN=ghp_xxx
CLOUDFLARE_ACCOUNT_ID=xxx

Local .env Example (Clerk)

PUBLIC_AUTH_PROVIDER=clerk

DATABASE_URL=postgresql://postgres:postgres@localhost:5432/bunship

SITE_URL=http://localhost:3001
TRUSTED_ORIGINS=http://localhost:3001,http://localhost:9001

PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxx
CLERK_SECRET_KEY=sk_test_xxx
CLERK_WEBHOOK_SECRET=whsec_xxx

ADMIN_EMAIL_LIST=admin@example.com
EMAIL_FROM=Bunship <noreply@example.com>
RESEND_API_KEY=re_xxx

S3_ENDPOINT=https://s3.example.com
S3_REGION=auto
S3_ACCESS_KEY=xxx
S3_SECRET_KEY=xxx
S3_BUCKET=bunship
PUBLIC_S3_URL_BASE=https://cdn.example.com

STRIPE_SECRET_KEY=sk_test_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
S_GITHUB_PERSONAL_ACCESS_TOKEN=ghp_xxx
CLOUDFLARE_ACCOUNT_ID=xxx