Motherbase reads configuration from .env. The supported setup path is ./setup.sh, which writes the project and Console environment files and generates strong secrets.
For manual development, copy apps/server/.env.example to apps/server/.env and fill the required values. The Console has its own file, deploy/console/.env.example.
For the deployment-side view of the same variables — which file writes them, which stack consumes them — see self-hosting environment variables.
Generate secrets
# JWT secret
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
# Config encryption key, base64-encoded 32 bytes
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"Required to boot
| Variable | Description |
|---|---|
DATABASE_URL | PostgreSQL connection string |
JWT_SECRET | Secret for signing JWT access tokens |
CONFIG_ENCRYPTION_KEY is not required to boot, but it is required to save Studio-managed secrets such as edge secrets and encrypted provider settings. setup.sh generates it.
Server
| Variable | Default | Description |
|---|---|---|
PORT | 5001 | HTTP API server port |
NODE_ENV | development | development, test, or production |
BASE_HOST | localhost:5001 | Public host used to generate file/API URLs |
CORS_ORIGINS | empty | Comma-separated allowed origins. Empty is dev-only; production must use an explicit allowlist. |
APP_NAME | Motherbase | Product name in transactional email subjects/bodies and the default TOTP issuer shown in an authenticator app |
BIND_HOST | prod 0.0.0.0, otherwise 127.0.0.1 | Interface to bind. Outside production, error responses carry stack traces, so the default keeps them off the network. Set it explicitly only for a VM or a remote dev box — doing so logs a warning |
TRUST_PROXY | empty (false) | Express trust proxy. Required behind Traefik or any reverse proxy, otherwise every client shares the proxy IP in one rate-limit bucket. Accepts a hop count (1), loopback, or a comma-separated IP/CIDR list. Leave unset with no proxy in front — trusting X-Forwarded-For then lets clients forge their IP. |
Database
| Variable | Default | Description |
|---|---|---|
DATABASE_URL | required | Full PostgreSQL connection string |
POSTGRES_PORT | 55432 | PostgreSQL port used by Docker Compose |
POSTGRES_USER | postgres | Database user |
POSTGRES_PASSWORD | generated by setup | Database password |
POSTGRES_DB | app | Database name |
Auth
| Variable | Default | Description |
|---|---|---|
JWT_SECRET | required | JWT signing secret |
ACCESS_TOKEN_EXP_MIN | 15 | Access token TTL in minutes |
REFRESH_TOKEN_EXP_DAYS | 30 | Refresh token TTL in days |
Passwords are hashed with argon2. Imported Supabase bcrypt hashes are accepted and migrated to argon2 after the user's first successful password login.
Google, Apple, OTP, SMTP, and password reset behavior are configured per project in Project Studio -> Settings with environment fallbacks for SMTP.
Rate limits
| Variable | Default | Description |
|---|---|---|
RATE_LIMIT_WINDOW_MIN | 15 | Rate limit window in minutes |
RATE_LIMIT_GLOBAL_MAX | 1000 | Global request limit per window |
RATE_LIMIT_AUTH_MAX | 20 | Auth route limit per window |
RATE_LIMIT_REST_READ_MAX | 300 | REST read limit per window |
RATE_LIMIT_REST_WRITE_MAX | 120 | REST write limit per window |
RATE_LIMIT_EDGE_MAX | 60 | Edge invocation limit per window |
REST API
| Variable | Default | Description |
|---|---|---|
REST_MAX_ROWS | 1000 | Seed for the api.rest → maxRows setting. The query parser enforces a hard 1000 ceiling regardless |
REST_RPC_SECURITY_DEFINER | empty | Comma-separated names of SECURITY DEFINER functions deliberately exposed over /rest/v1/rpc/. Empty by default — a definer function runs as its owner and bypasses the caller's RLS, so exposing one is always an explicit act |
This seeds the api.rest → rpcSecurityDefiner setting while no row has been saved for
that group. Once it is saved, the stored value wins and this variable is no longer read —
which is what makes clearing the allowlist from Studio actually clear it. Use
Reset to .env on the group to hand control back to the environment.
Email / SMTP
Required for email OTP, password reset, and email verification.
| Variable | Default | Description |
|---|---|---|
EMAIL_SMTP_HOST | empty | SMTP server hostname |
EMAIL_SMTP_USERNAME | empty | SMTP login username |
EMAIL_SMTP_PASSWORD | empty | SMTP login password |
EMAIL_CODE_LENGTH | 6 | Number of digits in OTP codes |
EMAIL_CODE_EXP_TIME | 2 | OTP expiry in hours |
Storage
| Variable | Default | Description |
|---|---|---|
STORAGE_DRIVER | local | local or s3 |
STATIC_PATH | uploads | Local storage root directory |
USER_PATH | users | Legacy user-scoped subdirectory |
MAX_UPLOAD_MB | 25 | Max multipart upload size in MB |
MAX_BACKUP_UPLOAD_MB | 2048 | Separate ceiling for uploading a backup file into BACKUP_DIR. A database dump is routinely far larger than a tenant upload, and raising MAX_UPLOAD_MB to fit one would widen the surface on every public upload route |
S3 / MinIO / R2
| Variable | Default | Description |
|---|---|---|
S3_ENDPOINT | empty | S3 endpoint URL, for example MinIO or R2 |
S3_REGION | us-east-1 | S3 region |
S3_ACCESS_KEY_ID | empty | S3 access key ID |
S3_SECRET_ACCESS_KEY | empty | S3 secret access key |
S3_BUCKET | uploads | Private physical bucket |
S3_PUBLIC_BUCKET | public | Public physical bucket |
S3_PUBLIC_URL | empty | Public base URL for served files |
MINIO_PORT | 59000 | Host port for the MinIO S3 API in Docker Compose |
MINIO_CONSOLE_PORT | 59001 | Host port for the MinIO web console in Docker Compose |
Logical buckets are configured in Project Studio and map onto the physical public/private buckets.
Backups and imports
| Variable | Default | Description |
|---|---|---|
BACKUP_DIR | backups | Local directory for database dumps and temporary Supabase import files |
BACKUP_ENABLED | false | Enable scheduled automatic database backups |
BACKUP_CRON | 0 3 * * * | Schedule for automatic backups |
BACKUP_RETENTION | 7 | Number of local backups to retain |
BACKUP_OFFSITE | false | Upload scheduled backups to S3-compatible storage |
BACKUP_BUCKET | backups | Dedicated offsite backup bucket |
BACKUP_S3_ENDPOINT | empty | Optional offsite S3 endpoint override |
BACKUP_S3_REGION | empty | Optional offsite S3 region override |
BACKUP_S3_ACCESS_KEY_ID | empty | Optional offsite S3 access key override |
BACKUP_S3_SECRET_ACCESS_KEY | empty | Optional offsite S3 secret key override |
PG_BIN_DIR | empty | Directory containing pg_dump / psql when they are not on PATH (common on native Windows/macOS dev machines) |
The Supabase importer writes a temporary dump file under BACKUP_DIR and removes it after the import.
Offsite upload happens only when STORAGE_DRIVER=s3 and offsite is enabled. See backups and restore.
Settings seeds
These are fallbacks, not live configuration. Each seeds a field in a settings group while no row has been saved for that group; once the group is saved in Studio the stored value wins and the variable is no longer read. Use Reset to .env on a group to hand control back. See settings system.
| Variable | Default | Seeds |
|---|---|---|
AUTH_ALLOW_SIGNUP | true | auth.signup → allowNewUsers (any value but false is true) |
AUTH_REQUIRE_EMAIL_VERIFICATION | false | auth.signup → requireEmailVerification |
AUTH_AUTO_SIGNIN | true | auth.signup → autoSignInAfterVerify |
AUTH_MIN_PASSWORD_LENGTH | 8 | auth.signup → minPasswordLength |
AUTH_MAX_PASSWORD_LENGTH | 128 | auth.signup → maxPasswordLength |
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET | empty | auth.social → Google provider (present ⇒ enabled) |
APPLE_CLIENT_ID / APPLE_CLIENT_SECRET / APPLE_BUNDLE_ID | empty | auth.social → Apple provider |
TWILIO_ACCOUNT_SID / TWILIO_AUTH_TOKEN / TWILIO_FROM_NUMBER | empty | phone.sms |
GITHUB_OAUTH_CLIENT_ID / GITHUB_OAUTH_CLIENT_SECRET / GITHUB_OAUTH_CALLBACK_URL | empty | git.github |
STORAGE_RLS_POLICIES | false | storage.policies → enabled |
REST_REQUIRE_API_KEY | false | api.rest → requireApiKey |
DATA_RETENTION_ENABLED | true | data.retention → enabled |
DATA_RETENTION_CRON | 15 4 * * * | data.retention → cron |
WEBHOOK_DELIVERY_RETENTION_DAYS | 30 | data.retention → webhookDeliveryDays |
DATA_RETENTION_BATCH_SIZE | 500 | data.retention → batchSize |
DATA_RETENTION_MAX_BATCHES | 20 | data.retention → maxBatchesPerRun |
REALTIME_RETENTION_ENABLED | true | realtime.retention → enabled |
REALTIME_OUTBOX_COMPLETED_DAYS | 7 | realtime.retention → completedDays |
REALTIME_OUTBOX_FAILED_DAYS | 30 | realtime.retention → failedDays |
JOB_RUN_RETENTION_DAYS | 30 | data.retention → jobRunDays |
AUTH_AUDIT_LOG_RETENTION_DAYS | 90 | data.retention → authAuditLogDays |
LOG_PERSISTENCE_ENABLED | true | logs.retention → enabled (any value but false is true). Off keeps the in-memory buffer only |
LOG_RETENTION_DAYS | 7 | logs.retention → retentionDays |
LOG_MAX_ROWS | 2000000 | logs.retention → maxRows, a ceiling regardless of age. 0 disables the cap |
AUTH_SITE_URL | empty | auth.urls → siteUrl, the fallback redirect for an auth flow |
AUTH_REDIRECT_URLS | empty | auth.urls → redirectUrls, comma or whitespace separated, each with a scheme |
REST_MAX_ROWS, REST_RPC_SECURITY_DEFINER and EDGE_FUNCTION_LOG_RETENTION_DAYS
above work the same way. Every field, with its full description, is in the
settings reference.
Push notifications
Firebase
| Variable | Description |
|---|---|
FIREBASE_PROJECT_ID | Firebase project ID |
FIREBASE_CLIENT_EMAIL | Service account client email |
FIREBASE_PRIVATE_KEY | Service account private key, PEM with \n newlines |
APNs
| Variable | Default | Description |
|---|---|---|
APNS_KEY | empty | .p8 private key contents |
APNS_KEY_ID | empty | Apple Developer key ID |
APNS_TEAM_ID | empty | Apple Developer Team ID |
APNS_BUNDLE_ID | empty | App bundle identifier |
APNS_PRODUCTION | false | true for production APNs gateway |
Project Studio and admin
| Variable | Default | Description |
|---|---|---|
CONSOLE_SQL_ENABLED | false | Enable the raw SQL editor in Project Studio |
CONSOLE_LOG_BUFFER_SIZE | 500 | Number of log lines kept in memory |
CONFIG_ENCRYPTION_KEY | empty | Encrypts Studio-managed secrets at rest. Base64, 32 bytes. Absent is allowed — the server boots and non-secret settings work, but saving a secret is refused with a clear message |
Edge functions
| Variable | Default | Description |
|---|---|---|
EDGE_PUBLIC_INVOKE_ENABLED | false | Allow anonymous /api/edge/:route invocation |
EDGE_INVOKE_TOKEN | empty | Optional server-to-server token for x-motherbase-edge-token |
EDGE_SECRETS_ENABLED | true | Inject Studio-managed edge secrets into a function's env. Set to false to withhold them; Shield reports when it is on and escalates to critical alongside EDGE_PUBLIC_INVOKE_ENABLED |
EDGE_MAX_MEMORY_MB | 512 | Ceiling for a function's memory limit, enforced on the worker process (default per function: 128) |
EDGE_MAX_TIMEOUT_MS | 60000 | Ceiling for a function's wall-clock timeout (default per function: 5000) |
EDGE_FUNCTION_LOG_RETENTION_DAYS | 30 | Seeds data.retention → edgeFunctionLogDays |
The runtime sidecar
Functions execute in a separate Deno service (deploy/edge-runtime), reached over HTTP.
These configure the API's half of that link.
| Variable | Default | Description |
|---|---|---|
EDGE_RUNTIME_URL | http://edge-runtime:9000 | Where the API reaches the sidecar |
EDGE_RUNTIME_TOKEN | empty | Shared secret, sent as x-edge-runtime-token. Must match the one the sidecar was started with |
EDGE_RUNTIME_CALLBACK_URL | http://api:5001 | This API as the sidecar reaches it, for the sendMail() callback |
And these are read by the sidecar process itself, not by the API:
| Variable | Default | Description |
|---|---|---|
EDGE_RUNTIME_PORT | 9000 | Port the sidecar listens on |
EDGE_ALLOWED_INTERNAL_ORIGINS | empty | Comma-separated exact origins (http://api:5001) a function's fetch may call despite resolving to a private address. Matched on scheme + host + port, never by suffix |
EDGE_MAX_WORKERS | 16 | Soft cap on cached worker processes. A burst goes over it rather than failing a call |
EDGE_WORKER_IDLE_TTL_MS | 300000 | Idle time before a worker is retired |
EDGE_WORKER_MAX_INVOCATIONS | 500 | Calls a worker serves before it is recycled |
EDGE_WORKER_BASE_RSS_MB | 96 | Allowance above a function's memory limit for Deno itself, used by the resident-size watchdog. A calibration knob, not a precise number |
DENO_DIR | /tmp/deno-dir | Module cache directory for the worker processes |
See edge runtime for what each one changes.
AI assistant
| Variable | Default | Description |
|---|---|---|
AI_DATA_DIR | data | Directory for the assistant's SQLite thread store (assistant.db). Kept separate from BACKUP_DIR so restoring a database backup never touches conversations. |
AI_ASSISTANT_ENABLED | false | Turn the assistant on with the environment-provided key |
AI_PROVIDER | empty | Provider id, one of the ids @motherbase/ai declares |
AI_MODEL | empty | Model id for that provider |
AI_API_KEY | empty | The key. Nothing is configured until both this and AI_PROVIDER are set |
AI_BASE_URL | empty | Override the provider's base URL (a gateway, a self-hosted endpoint) |
These five are the organization-wide default, shaped exactly like a saved
ai.assistant settings group and reported to Studio as source: 'org'. A project that
saves its own ai.assistant overrides them entirely. A half-filled environment — a
provider with no key, or a key with no provider — is treated as not configured rather
than as a broken configuration. In a standalone deployment with no Platform Console,
these are how an operator configures the assistant. See
AI assistant.
Platform Console
Only needed when running the multi-project Platform Console.
| Variable | Default | Description |
|---|---|---|
CONSOLE_MODE | prod | dev or prod |
PLATFORM_SERVICE_TOKEN | generated by setup | Shared secret for Console to project API calls |
PROJECTS_DIR | dev: .dev-projects, prod: /srv/projects | Directory where project stacks are stored |
COMPOSE_FILE | mode-specific deploy file | Optional project Compose file override |
TRAEFIK_NETWORK | traefik_proxy | Docker network used by Traefik |
UPDATE_CHECK_URL | empty | Optional release-channel URL used by the Console update status card |
CONSOLE_PORT | 5500 | Console HTTP port |
CONSOLE_JWT_SECRET | dev fallback only | Signs Console operator sessions. Required in prod mode, minimum 32 characters — the Console refuses to boot otherwise |
CONSOLE_ADMIN_EMAIL | admin@example.com | Seeded first owner |
CONSOLE_ADMIN_PASSWORD_HASH_B64 | empty | Base64 of the owner's bcrypt hash. Base64 because Compose mangles $ through ${} interpolation |
CONSOLE_ADMIN_PASSWORD_HASH | empty | Raw hash fallback for non-Compose setups |
CONSOLE_ADMIN_PASSWORD_TEMPORARY | false | Force a password change on first login |
CONSOLE_BASE_URL | <scheme>://localhost:<port> | Public base URL trusted for auth |
CONSOLE_TRUSTED_ORIGINS | empty | Extra comma-separated origins to trust (proxies that rewrite Host) |
CONSOLE_RATE_LIMIT_WINDOW_MIN | 15 | Console auth rate-limit window |
CONSOLE_RATE_LIMIT_AUTH_MAX | 20 | Console auth attempts per window |
CONSOLE_RATE_LIMIT_PROXY_MAX | 600 | Requests per window through the Console project proxy |
CONSOLE_AUDIT_MAX_ENTRIES | 10000 | Audit log retention, clamped to [1, 1000000] |
CONSOLE_BILLING_ENABLED | false | Show the Billing tab |
CONSOLE_STACK_COMPOSE | deploy/console/docker-compose.yml | Stack the self-update targets |
CONSOLE_STACK_SERVICES | console orchestrator | Services in that stack |
PLATFORM_DOMAIN_SUFFIX | dev: localhost | Base domain new project hostnames are derived from |
API_IMAGE | motherbase-api:1.0.0 | Image used for new project stacks |
CERT_RESOLVER | letsencrypt | Traefik certificate resolver for new projects |
ORCHESTRATION_URL | http://orchestrator:5501 | Orchestration daemon address |
ORCHESTRATION_SECRET | — | Shared bearer secret, minimum 32 characters |
ORCHESTRATION_PORT | 5501 | Port the daemon listens on |
PREVIEW_ENABLED | false | Preview projects per pull request. Needs PREVIEW_GITHUB_SECRET set as well, or it stays off |
PREVIEW_GITHUB_SECRET | empty | Secret the GitHub webhook is signed with |
PREVIEW_GITHUB_TOKEN | empty | Token used to post the preview URL back on the PR |
PREVIEW_REPO_ALLOWLIST | empty | Comma-separated owner/repo list allowed to open previews |
PREVIEW_TTL_HOURS | 72 | Hours a preview stack lives before it is reaped |
DB_PORT | allocated | Host port for a project's Postgres, assigned by the Console from 54320-54419 and written into the project's .env.prod. Not something you set by hand |
UPDATE_CHECK_URL may return { "version": "1.2.3" }, { "tags": [...] }, or a Docker Hub-style { "results": [{ "name": "1.2.3" }] } payload. The Console only checks and displays availability; updates still use the supported setup path. See Updates.
setup.sh and the Console deployment files also use install-time values such as DOMAIN, CONSOLE_DOMAIN, ACME_EMAIL, and Traefik DNS challenge credentials.
CLI
Used by @motherbase/cli for non-interactive runs and CI.
| Variable | Description |
|---|---|
MOTHERBASE_CONSOLE_URL | Platform Console URL for Console mode |
MOTHERBASE_TOKEN | Console session token, equivalent to --token |
MOTHERBASE_PROJECT | Project id/name to operate through the Console proxy |
MOTHERBASE_URL | Direct project API URL for single-project direct mode |
MOTHERBASE_ADMIN_TOKEN | Admin bearer token for direct project mode |
MOTHERBASE_DEBUG | 1 logs one line per HTTP request to stderr, equivalent to --debug. Never the request body |
SEED_ADMIN_EMAIL and SEED_ADMIN_PASSWORD are read by the project seed script
(npm run db:seed), not by the running server.
Console mode uses MOTHERBASE_CONSOLE_URL + MOTHERBASE_TOKEN + MOTHERBASE_PROJECT. Direct mode uses MOTHERBASE_URL + MOTHERBASE_ADMIN_TOKEN.