All runtime configuration comes from the environment; nothing is read from disk at runtime, so the same image runs anywhere with a different .env. Two variables are required — DATABASE_URL and JWT_SECRET — and the process refuses to start without them.
Required
Variable
Notes
DATABASE_URL
The project database: your tables, auth, storage, and this project's Baselyra configuration. This is what /rest/v1 exposes.
JWT_SECRET
HS256 secret. Signs access tokens, the anon key, the service key, Studio tokens and signed storage URLs. At least 32 random bytes; scripts/setup.sh generates one.
Instance
Variable
Default
Effect
NODE_ENV
production
Anything but production turns per-request logging on.
PORT
3000
Port inside the container. Compose maps it with BASELYRA_PORT.
HOST
0.0.0.0
Bind address.
BASELYRA_PUBLIC_URL
http://localhost:3000
This instance's public origin. Goes into email links, signed storage URLs and the OAuth callback URL. A trailing slash is stripped.
BASELYRA_SITE_URL
http://localhost:3000
Your frontend. Where GET /auth/v1/verify redirects, and the default OAuth redirect target. Always this value, never anything from the request — which is what keeps those routes from being open redirects.
BASELYRA_VERSION
0.1.0
Reported by /health. The Docker build sets it.
CORS_ORIGINS
*
Comma-separated allowed origins. * means any page on the internet may call the API with a token the browser holds. Name your origins in production.
LOG_LEVEL
info
Fastify log level. authorization, apikey and cookie headers are redacted at every level.
Databases
Variable
Default
Effect
CONTROL_DATABASE_URL
DATABASE_URL with the database name swapped for baselyra_control
The control database: Studio accounts, audit log, import history, request metering, the project registry. Set it only to put the control database on another server. Pointing it at the same database as DATABASE_URL is refused at boot.
DATABASE_POOL_MAX
12
Connections per pool, per app process. Stay well under Postgres' max_connections.
DATABASE_STATEMENT_TIMEOUT_MS
15000
Kills a runaway API query. Exceeding it is a 408 statement_timeout. Bulk import copies raise it to unlimited for the duration of a table.
Replica settings
Four variables exist and are read, and the registry will open a pool per URL and
probe it every DATABASE_REPLICA_HEALTH_MS with
pg_is_in_recovery() and pg_last_xact_replay_timestamp().
Variable
Default
DATABASE_REPLICA_URLS
empty — comma separated
DATABASE_REPLICA_MAX_LAG_MS
5000
DATABASE_REPLICA_HEALTH_MS
10000
DATABASE_READ_AFTER_WRITE_MS
10000
Project settings
Every lookup of "which database is this project" reads control.projects rather
than a constant, so a switcher later is a change to one resolver. Today that table
holds a single row and nothing creates a second In progress.
Variable
Default
Effect
PROJECT_DEFAULT_SLUG
default
The project a request that names none is answered from.
PROJECT_DATABASE_PREFIX
baselyra_
Prefix for a created project's database name.
PROJECT_MAX_POOLS
8
Idle project pools held open at once; past this the least recently used is closed.
Tokens and sessions
Variable
Default
Effect
JWT_ACCESS_TTL
3600
Access token lifetime in seconds. An access token cannot be revoked before it expires, so keep this short — that is what the refresh token is for.
JWT_REFRESH_TTL
2592000
Refresh token lifetime in seconds (30 days).
JWT_ISSUER
baselyra
The iss claim.
Auth behaviour
Variable
Default
Effect
AUTH_CONFIRM_EMAIL
true
Require a confirmed address before the first sign-in. With it on, POST /auth/v1/signup answers with session: null.
AUTH_ALLOW_SIGNUPS
true
When false, /auth/v1/signup answers 403, third-party sign-in refuses to create a new account, and a phone code is only sent to a number that already has one.
AUTH_MIN_PASSWORD_LENGTH
8
The floor. There is no composition rule — length is the property that matters. The ceiling is 256 bytes, because scrypt is linear in input length.
AUTH_MAX_ATTEMPTS
8
Failed sign-ins per email+IP inside the window before a 429 with Retry-After.
AUTH_ATTEMPT_WINDOW
900
That window, in seconds. Nothing is locked permanently, so nobody can lock a competitor out of their own account.
AUTH_OTP_TTL
600
One-time code lifetime in seconds, for both email and phone codes.
Third-party sign-in
A provider is offered only when both halves are present. Half-configured is
indistinguishable from not configured, and a sign-in button that cannot work is
worse than no button.
The provider id is uppercased into the variable name:
google, github, linkedin, facebook, instagram,
tiktok, envato.
Variable
Default
Effect
BASELYRA_OAUTH_REDIRECT_ALLOWLIST
empty
Extra targets a sign-in may return the browser to, on top of BASELYRA_SITE_URL. Comma-separated absolute http(s) URLs; a path on one narrows the allowance to that path and below.
One directory per bucket. A Docker volume in the shipped compose file. Back it up or lose it.
STORAGE_MAX_FILE_BYTES
52428800
Global upload cap (50 MB), and the Fastify body limit. A bucket's own file_size_limit can be lower. Keep your proxy's body limit above this.
Email
Variable
Default
Effect
SMTP_HOST
empty
Empty means nothing is sent — the message, including confirmation and recovery links, is printed to the app log. Right for a laptop, never right in production.
SMTP_PORT
587
SMTP_SECURE
false
true only for implicit TLS on port 465.
SMTP_USER / SMTP_PASS
empty
SMTP_FROM
Baselyra <no-reply@localhost>
Phone and SMS
With SMS_PROVIDER unset the code is printed to the log instead of sent, exactly
as email is. A configured provider that refuses a message still throws.
Variable
Default
Effect
SMS_PROVIDER
empty
One of twilio, vonage, messagebird, sns, plivo, webhook.
SMS_DEFAULT_COUNTRY
empty
Country calling code for a number typed without one, e.g. 1 or 44. Unset means a number must arrive in international form to be accepted at all.
SMS_OTP_COOLDOWN
60
Seconds before the same number may be sent another code.
SMS_MAX_PER_NUMBER_PER_HOUR
5
Hard cap per number per hour. Every message is a charge on your account and the endpoint that causes one is unauthenticated, so this ceiling is what stops a stranger spending your money.
SMS_FROM
empty
Sender id or number, where the provider needs one.
Per-provider credentials — TWILIO_ACCOUNT_SID, VONAGE_API_KEY,
PLIVO_AUTH_ID and the rest — are listed on
Phone one-time codes.
Realtime
Variable
Default
Effect
REALTIME_MAX_CHANNELS
100
Channels one socket may hold.
REALTIME_HEARTBEAT_MS
30000
Server ping interval. A socket that misses two consecutive pongs is terminated.
AI
Entirely optional. Without a key every /ai/v1 route answers 503 ai_disabled,
the Studio hides its AI panels, and nothing else changes.
Variable
Default
Effect
DEEPSEEK_API_KEY
empty
The key stays on the server and never appears in a response.
DEEPSEEK_BASE_URL
https://api.deepseek.com
Any OpenAI-compatible /chat/completions endpoint, so a local Ollama or vLLM behind an OpenAI shim works.
DEEPSEEK_MODEL
deepseek-chat
DEEPSEEK_MAX_TOKENS
2048
Hard ceiling per completion. A caller may ask for fewer, never more.
Bootstrap and compose-only
Variable
Read by
Effect
BASELYRA_ADMIN_EMAIL
scripts/migrate.js
Creates the first Studio account in control.platform_users — never in your project's auth.users.
BASELYRA_ADMIN_PASSWORD
scripts/migrate.js
Its password on first boot only. A re-run promotes the account to owner but never overwrites the password.
BASELYRA_PORT
docker-compose.yml
Host port, bound to 127.0.0.1. Default 3130.
POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB
docker-compose.yml
Compose composes DATABASE_URL from them.
A production .env
# instance
BASELYRA_PUBLIC_URL=https://api.example.com
BASELYRA_SITE_URL=https://app.example.com
BASELYRA_PORT=3130
CORS_ORIGINS=https://app.example.com
LOG_LEVEL=info
# secrets — generated by ./scripts/setup.sh
JWT_SECRET=…48 random bytes…
POSTGRES_USER=baselyra
POSTGRES_PASSWORD=…32 random bytes…
POSTGRES_DB=baselyra
# auth
AUTH_CONFIRM_EMAIL=true
AUTH_ALLOW_SIGNUPS=true
AUTH_MIN_PASSWORD_LENGTH=8
# email — without this, recovery links land in your log file
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=apikey
SMTP_PASS=…
SMTP_FROM="Acme <no-reply@acme.com>"
# storage
STORAGE_ROOT=/var/lib/baselyra/storage
STORAGE_MAX_FILE_BYTES=52428800
# cleared after the first sign-in
BASELYRA_ADMIN_EMAIL=
BASELYRA_ADMIN_PASSWORD=
Changing a value
$EDITOR .env
docker compose up -d # recreates the app container with the new environment
Nothing is read from disk at runtime, so every change needs a restart. Changing
POSTGRES_PASSWORD also means changing it in Postgres
(alter role baselyra password '…') and restarting both containers.