Environment variables
The API and Nuxt development commands load environment files from their workspaces. The Docusaurus configuration reads the process environment. Keep secrets out of version control and use the deployment platform's secret store for production values.
Configure the API
Create apps/api/.env for local development:
NODE_ENV=development
HOST=127.0.0.1
PORT=3000
LOG_LEVEL=info
DATABASE_URL=postgresql://app:app@localhost:5432/app
DIRECT_URL=postgresql://app:app@localhost:5432/app
REALTIME_PUBLISH_URL=http://127.0.0.1:54321/realtime/v1/api/broadcast
REALTIME_SECRET_KEY=replace_with_server_publish_secret
REALTIME_JWT_SECRET=private_p256_es256_jwk_json
AWS_REGION=eu-central-1
SQS_EMAIL_QUEUE_URL=https://sqs.eu-central-1.amazonaws.com/123456789012/email-jobs
SQS_PUSH_QUEUE_URL=https://sqs.eu-central-1.amazonaws.com/123456789012/push-jobs
SUPABASE_URL=http://127.0.0.1:54321
MEDIA_S3_BUCKET=private-media-bucket
MEDIA_UPLOAD_POLICIES_JSON={"avatar":{"maxSizeBytes":5242880,"contentTypes":["image/jpeg","image/png","image/webp"]}}
MEDIA_UPLOAD_TTL_SECONDS=300
MEDIA_DELIVERY_TTL_SECONDS=120
CLOUDFRONT_BASE_URL=https://media.example.com
CLOUDFRONT_KEY_PAIR_ID=K1234567890
CLOUDFRONT_PRIVATE_KEY_BASE64=base64_encoded_pkcs8_private_key
OTEL_EXPORT_ENABLED=false
OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4318
POSTHOG_PROJECT_TOKEN=phc_replace_me
POSTHOG_HOST=https://eu.i.posthog.com
The API validates its runtime configuration before it starts its service dependencies.
| Variable | Required | Accepted values and behavior |
|---|---|---|
NODE_ENV | Yes | development, test, or production |
HOST | Yes | Non-empty listener host; use 127.0.0.1 locally and 0.0.0.0 in a container |
PORT | Yes | Integer from 1 through 65535 |
LOG_LEVEL | Yes | fatal, error, warn, info, debug, trace, or silent |
DATABASE_URL | Yes | Absolute postgres: or postgresql: URL with a hostname and database name |
REALTIME_PUBLISH_URL | Yes | Absolute URL of the Realtime broadcast endpoint, with a hostname and no embedded credentials. A path is allowed. Production requires HTTPS. Development and test also accept HTTP, including the local Supabase endpoint. A missing hostname or userinfo is rejected |
REALTIME_SECRET_KEY | Yes | Non-empty server publish secret. It never belongs in a web variable, a log line, or a website credential |
REALTIME_JWT_SECRET | Yes | JSON private P-256 JWK with kty EC, crv P-256, alg ES256, and non-blank kid, d, x, and y. A legacy shared secret, a public-only key, and any other algorithm are rejected. Errors name the variable and do not include key material. Startup checks this shape only; the key must still be the project's current ES256 signing key or the first website subscription fails |
AWS_REGION | Yes | Non-empty AWS Region that contains both configured SQS queues, for example eu-central-1 |
SQS_EMAIL_QUEUE_URL | Yes | Absolute HTTPS URL for email jobs, with a hostname and queue path; query strings, fragments, and credentials are rejected |
SQS_PUSH_QUEUE_URL | Yes | Absolute HTTPS URL for push jobs, with a hostname and queue path; it must differ from the email queue URL |
SUPABASE_URL | Yes | Absolute origin of the Supabase project, with no credentials, path, query, or trailing slash. Production requires HTTPS; development and test also accept HTTP. The API derives the token issuer (<url>/auth/v1) and signing-key set from it. Access-token verification needs no anon key or service role key. The realtime publisher is configured separately and is probed before the API listens |
MEDIA_S3_BUCKET | Yes | Private S3 bucket name used for temporary and durable media objects |
MEDIA_UPLOAD_POLICIES_JSON | Yes | Non-empty JSON object keyed by purpose. Each value has a positive maxSizeBytes and non-empty unique contentTypes list selected from application/pdf, image/gif, image/jpeg, image/png, and image/webp |
MEDIA_UPLOAD_TTL_SECONDS | Yes | Integer from 60 through 3600; lifetime of a presigned S3 POST authorization and pending upload attempt |
MEDIA_DELIVERY_TTL_SECONDS | Yes | Integer from 30 through 3600; lifetime of an authorized CloudFront signed URL |
CLOUDFRONT_BASE_URL | Yes | HTTPS CloudFront delivery origin with no credentials, path, query, or fragment |
CLOUDFRONT_KEY_PAIR_ID | Yes | Non-empty CloudFront trusted-key-pair identifier |
CLOUDFRONT_PRIVATE_KEY_BASE64 | Yes | Base64-encoded PKCS#8 private key used only to sign delivery URLs; never put the decoded value in source control or a client-visible variable |
CORS_ORIGIN | Production only | One exact HTTP or HTTPS origin; credentials, *, paths, and trailing slashes are rejected |
OTEL_EXPORT_ENABLED | No | true or false; defaults to false in development, true in production, and is always disabled in tests |
OTEL_EXPORTER_OTLP_ENDPOINT | When OTLP export is enabled | Absolute HTTP or HTTPS base URL; the API appends the signal paths |
POSTHOG_PROJECT_TOKEN | Production only | PostHog project token used for explicit server-side product events; omit both PostHog values in development to use a no-op client, and never use a personal phx_ API key here |
POSTHOG_HOST | With POSTHOG_PROJECT_TOKEN in production | Absolute HTTP or HTTPS PostHog ingest host without credentials, such as https://eu.i.posthog.com or the host for a self-managed project |
DIRECT_URL | For Supabase CLI migrations | Direct, non-pooler PostgreSQL URL used by migration commands; it is not part of the running API configuration |
For production, inject variables into the process rather than relying on the
application to read a file. The API deliberately ignores dotenv files when
NODE_ENV is production or test. A production configuration with remote
telemetry disabled can start with:
NODE_ENV=production
HOST=0.0.0.0
PORT=3000
LOG_LEVEL=info
DATABASE_URL=postgresql://user:password@pooler.example.com/app
DIRECT_URL=postgresql://user:password@direct.example.com/app
REALTIME_PUBLISH_URL=https://project-ref.supabase.co/realtime/v1/api/broadcast
REALTIME_SECRET_KEY=replace_with_server_publish_secret
REALTIME_JWT_SECRET=private_p256_es256_jwk_json
AWS_REGION=eu-central-1
SQS_EMAIL_QUEUE_URL=https://sqs.eu-central-1.amazonaws.com/123456789012/email-jobs
SQS_PUSH_QUEUE_URL=https://sqs.eu-central-1.amazonaws.com/123456789012/push-jobs
SUPABASE_URL=https://project-ref.supabase.co
MEDIA_S3_BUCKET=private-media-bucket
MEDIA_UPLOAD_POLICIES_JSON={"avatar":{"maxSizeBytes":5242880,"contentTypes":["image/jpeg","image/png","image/webp"]}}
MEDIA_UPLOAD_TTL_SECONDS=300
MEDIA_DELIVERY_TTL_SECONDS=120
CLOUDFRONT_BASE_URL=https://media.example.com
CLOUDFRONT_KEY_PAIR_ID=K1234567890
CLOUDFRONT_PRIVATE_KEY_BASE64=base64_encoded_pkcs8_private_key
CORS_ORIGIN=https://app.example.com
OTEL_EXPORT_ENABLED=false
POSTHOG_PROJECT_TOKEN=phc_replace_me
POSTHOG_HOST=https://eu.i.posthog.com
See Database connection,
Realtime event channels, and
SQS job boundary,
Authenticated media uploads,
Authentication, and
Telemetry pipeline for the lifecycle of these
dependencies. The API workload requires sqs:SendMessage permission for the
configured email and push queues, plus S3 permissions sufficient to authorize
the configured direct-upload prefix and inspect, read, copy, and delete media
objects. CloudFront delivery signing uses the configured private key rather than
AWS credentials. Its shared AWS SDK clients use the default credential provider
chain, so production can use a workload IAM role without static credential
variables. Local development can use an AWS profile or temporary credentials;
when using temporary key credentials, provide AWS_ACCESS_KEY_ID,
AWS_SECRET_ACCESS_KEY, and AWS_SESSION_TOKEN through the shell or local
credential provider rather than treating them as required API settings.
This is a breaking configuration change. Deployments must replace the legacy
SQS_QUEUE_URL value with both semantic queue variables before starting the
updated API. There is no fallback to the legacy value, and the two new URLs
must identify different queues.
The API sends no automatic HTTP analytics. Future feature slices capture only the explicit product events they own. In production, both PostHog variables must be present in the process environment before the API listens; development without both values uses a no-op analytics client and tests never send events.
Configure the web application
Create apps/web/.env:
NUXT_POSTHOG_PROJECT_TOKEN=phc_replace_me
NUXT_POSTHOG_HOST=https://eu.i.posthog.com
NUXT_PUBLIC_SUPABASE_URL=https://project-ref.supabase.co
NUXT_PUBLIC_SUPABASE_ANON_KEY=replace_with_anon_key
NUXT_PUBLIC_API_BASE_URL=http://localhost:3000
NUXT_PUBLIC_REALTIME_URL=wss://project-ref.supabase.co/realtime/v1
| Variable | Visibility | Purpose |
|---|---|---|
NUXT_POSTHOG_PROJECT_TOKEN | Server only | PostHog project token for custom events emitted from Nitro server routes; required with its host in production, optional together in development, and never sent in tests |
NUXT_POSTHOG_HOST | Server only | Absolute HTTP or HTTPS ingest host without credentials, such as https://eu.i.posthog.com; select the host that matches the PostHog project region or self-managed deployment |
NUXT_PUBLIC_SUPABASE_URL | Public | Supabase project URL used by the browser to sign in with email and password |
NUXT_PUBLIC_SUPABASE_ANON_KEY | Public | Supabase anon (publishable) key for the browser auth client. Never put the service role key, publish secret, or signing key in any web variable |
NUXT_PUBLIC_API_BASE_URL | Public | Base URL for browser and server-rendered API requests |
NUXT_PUBLIC_REALTIME_URL | Public | Supabase Realtime websocket URL. It carries no secret. The API does not expose a local realtime listener, and the website does not open a socket from this URL by itself |
Every variable prefixed with NUXT_PUBLIC_ is serialized into browser-visible
runtime configuration. Do not put credentials or other secrets in those
values. Replace the realtime example with the project's Realtime websocket URL.
The two NUXT_POSTHOG_ values are private runtime configuration, so Nuxt does
not serialize them into rendered HTML or the browser payload. A production
Nitro server refuses to start unless both values are valid. Local development
without both values uses a no-op analytics client, and tests never send events.
Server routes capture only explicit events through their injected analytics
context; ordinary HTTP traffic and request bodies are not captured.
The repository's development and preview scripts currently pass
--port 3002, so that command-line setting takes precedence over environment
variables. To let apps/web/.env select the development port, remove the
--port argument from the relevant script and add:
NUXT_PORT=3002
NUXT_HOST=127.0.0.1
The built Nitro server uses PORT and HOST. The production image already
sets these values:
NODE_ENV=production
HOST=0.0.0.0
PORT=3000
When changing the container port, update the Dockerfile's EXPOSE instruction,
health check, and deployment routing together.
Configure channel authentication
The API's channel authentication reads the
settings below from the same apps/api/.env. Setting CHANNEL_AUTH_ISSUER_URL
enables it; production requires it. Once it is enabled, every other variable
marked required must be valid.
SUPABASE_PUBLISHABLE_KEY=<publishable-key>
CHANNEL_AUTH_ISSUER_URL=https://api.example.com
CHANNEL_AUTH_TELEGRAM_APPS_JSON={"professional":{"role":"professional","botToken":"<professional-bot-token>","webhookSecret":"<professional-webhook-secret>"},"client":{"role":"client","botToken":"<client-bot-token>","webhookSecret":"<client-webhook-secret>"}}
CHANNEL_AUTH_TELEGRAM_MAX_AGE_SECONDS=3600
CHANNEL_AUTH_ASSERTION_PRIVATE_KEY=<pkcs8-pem>
CHANNEL_AUTH_ASSERTION_RETIRED_PUBLIC_KEYS_JSON=
CHANNEL_AUTH_ASSERTION_AUDIENCE=bookygram-channels
CHANNEL_AUTH_SUPABASE_PROVIDER=custom:bookygram-channels
| Variable | Required | Accepted values and behavior |
|---|---|---|
CHANNEL_AUTH_ISSUER_URL | Production only | The API's own public HTTPS origin, with no path. Supabase Auth fetches discovery metadata and public keys from it, so HTTP is rejected in every environment |
SUPABASE_PUBLISHABLE_KEY | When enabled | The project's publishable key, sent as apikey on exchange, refresh and logout, and when the API reads a caller's confirmed phone for POST /client/account. Without it that route answers 503. Never the secret key |
CHANNEL_AUTH_TELEGRAM_APPS_JSON | Production only | JSON object mapping each Mini App identifier (lowercase, up to 32 characters) to {"role", "botToken", "webhookSecret"}. role is professional or client and decides which application a launch signs into. webhookSecret is 32 to 256 letters, digits, _ or -, and must match the secret_token set on that bot's webhook. Each bot and each webhook secret serves exactly one app; unknown fields are rejected |
CHANNEL_AUTH_TELEGRAM_MAX_AGE_SECONDS | No | Maximum launch-data age, from 60 through 86400; defaults to 3600. Launch data dated more than 30 seconds in the future is always rejected |
CHANNEL_AUTH_ASSERTION_PRIVATE_KEY | When enabled | PKCS#8 PEM of an RSA key of at least 2048 bits that signs identity assertions. A single line with \n escapes is accepted |
CHANNEL_AUTH_ASSERTION_RETIRED_PUBLIC_KEYS_JSON | No | JSON array of up to four RSA public JWKs still published during a key rotation. A JWK with private members is rejected |
CHANNEL_AUTH_ASSERTION_AUDIENCE | When enabled | The client ID registered for the provider in Supabase Auth |
CHANNEL_AUTH_SUPABASE_PROVIDER | When enabled | The provider identifier registered in Supabase Auth, such as custom:bookygram-channels |
Exchange, refresh and logout use the existing SUPABASE_URL. Configuration
errors list the invalid variable names without their values. Bot tokens,
webhook secrets, the assertion key and the publishable key belong only in the
API's environment, never in a Mini App variable. See
Rotate the channel assertion key for key
changes.
Configure the documentation site
The documentation site has local defaults. Set these variables in the process environment when the deployment URL differs from those defaults:
DOCS_URL=http://localhost:3003
DOCS_BASE_URL=/
| Variable | Default | Purpose |
|---|---|---|
DOCS_URL | http://localhost:3003 | Absolute origin used when Docusaurus generates canonical URLs and metadata |
DOCS_BASE_URL | / | Path from which the generated site is served |
For a site deployed at its own hostname, use:
DOCS_URL=https://docs.example.com
DOCS_BASE_URL=/
For a site deployed below a path, include both the leading and trailing slash:
DOCS_URL=https://example.com
DOCS_BASE_URL=/docs/
The current Docusaurus setup does not load apps/docs/.env automatically.
Provide the variables through the shell, CI, or deployment environment. For
example:
DOCS_URL=https://docs.example.com DOCS_BASE_URL=/ npm run build --workspace apps/docs
These values are consumed while Docusaurus builds the static site. Supplying
different values only after the build does not rewrite the generated output.
The local development, start, and serve scripts select port 3003 with a
command-line argument.
Verify the configuration
Start each workspace from the repository root in a separate terminal:
npm run dev --workspace api
npm run dev --workspace web
npm run dev --workspace apps/docs
The default local endpoints are:
| Application | URL |
|---|---|
| API | http://127.0.0.1:3000 |
| Web | http://localhost:3002 |
| Documentation | http://localhost:3003 |
An invalid API configuration reports the names of invalid variables without printing their values. Nuxt and Docusaurus configuration errors appear during their respective startup or build commands.
PostgreSQL Compose settings and the OpenTelemetry collector credentials are
infrastructure configuration, not API, web, or documentation application
variables. The API does not read REDIS_URL.