Environment & Configuration
Environment Variables
Section titled “Environment Variables”All configuration is done via environment variables in your .env file at the project root.
Important: Rebase validates environment variables with Zod at startup. If anything required is missing or malformed (a URL that is not a URL, a port that is not a number), the server refuses to boot and names the variable.
Where the schema lives depends on how you run the backend. A project booted by the runtime —
rebase dev,rebase start, the published image — uses the schema the runtime owns (loadBootEnvin@rebasepro/server), which is the union of every table below. A project that has runrebase ejectowns abackend/src/env.tscallingloadEnv({ extend }), and can add its own typed variables there.
Required
Section titled “Required”| Variable | Description | Example |
|---|---|---|
DATABASE_URL |
PostgreSQL connection string | postgresql://user:pass@localhost:5432/mydb |
JWT_SECRET |
Secret key for signing JWT tokens. Use a strong random string (min 32 chars). Required in production (auto-generated in development). | a1b2c3d4e5... |
sslmode=no-verifyis a node-postgres spelling, not a libpq one.Rebase and the Node driver accept it — encrypt, but do not check the certificate.
psql,pg_dump,pg_restoreand Atlas do not, and they do not degrade: they refuse to start withinvalid sslmode value: "no-verify".Rebase’s own commands (
rebase db push,rebase db backup,rebase db restore) rewrite it to the equivalentsslmode=requirebefore shelling out, so they work with the URL as configured. Reaching forpsqlby hand does not — swap insslmode=requirethere, which encrypts without verifying in exactly the same way.
Frontend
Section titled “Frontend”| Variable | Description | Default |
|---|---|---|
VITE_API_URL |
Backend API URL for the client SDK. Set this in development only — see below. | page origin |
VITE_GOOGLE_CLIENT_ID |
Google OAuth client ID. Enables “Sign in with Google”. | — |
Leave
VITE_API_URLunset in production builds.In development the frontend and backend are separate origins, so the dev server injects this. In production the Rebase backend serves the SPA, so the API is the page’s own origin and the client resolves it that way on its own.
Baking an absolute URL into a production bundle works right up until a second hostname points at the same app: a custom domain then loads the page from
example.comand calls the API onexample.rebase.website, which is cross-origin, so every request fails preflight. Allowing the origin in CORS does not fix it either — the refresh cookie isSameSite=Laxand is not sent cross-site, so you would clear the console errors and still have broken auth. Unset, every domain pointing at the app works with no CORS configuration at all.
Backend
Section titled “Backend”| Variable | Description | Default |
|---|---|---|
PORT |
Port for the backend HTTP server. Read by rebase start; rebase dev ignores it and binds a port derived from the project path so several projects can run at once — use rebase dev --port to pin one. |
3001 |
LOG_LEVEL |
Logging verbosity: error, warn, info, debug |
info |
NODE_ENV |
Environment: development, production, or test |
development |
CORS_ORIGINS |
Comma-separated list of allowed origins. Required in production if different from backend domain. | — |
FRONTEND_URL |
URL of the frontend app. Used as an alternative to CORS_ORIGINS. | — |
ADMIN_CONNECTION_STRING |
Admin-level database connection string (used for schema introspection and admin operations). | DATABASE_URL |
DISABLE_DB_ROLE_SWITCHING |
Disable PostgreSQL role-switching in SQL Editor (useful for custom authentication where DB roles are not mapped). | false |
Authentication
Section titled “Authentication”| Variable | Description | Default |
|---|---|---|
JWT_SECRET |
Secret for JWT signing (required in production, auto-generated in development) | — |
JWT_PRIVATE_KEY |
PEM private key for signing access tokens asymmetrically (RS256), so anything holding the JWKS can verify a session without being able to mint one. Accepts a PEM with real newlines, a PEM with \n escapes, or base64 of the whole PEM. Without it tokens stay HS256. |
— |
JWT_KEY_ID |
Names JWT_PRIVATE_KEY in the token header and in the JWKS. Change it whenever the key changes — rotation depends on old and new being distinguishable. |
default |
JWT_ACCESS_EXPIRES_IN |
Access token lifetime | 1h |
JWT_REFRESH_EXPIRES_IN |
Refresh token lifetime. Sliding — every rotation re-ups it, so this governs how long a session survives inactivity. | 400d |
ALLOW_REGISTRATION |
Allow new users to register (true/false). The first user can always register, whatever this says — an empty user table has to admit somebody. The scaffold’s .env.example sets it to true; the framework default is off. |
false |
DISABLE_SELF_REGISTRATION |
Kill switch. Closes the first-user bootstrap window that ALLOW_REGISTRATION=false deliberately leaves open, so registration is shut even against an empty database. |
— |
ALLOW_ANONYMOUS |
Enable anonymous sign-in (POST /api/auth/anonymous). Opt-in, and deliberately not gated by ALLOW_REGISTRATION. |
false |
AUTH_REQUIRE |
Require authentication for the data API. Set false for a fully public read surface — RLS still applies. |
true |
AUTH_DEFAULT_ROLE |
Role assigned to a newly registered user when none is given. | — |
AUTH_ALLOW_USER_LOOKUP |
Mount POST /api/auth/find-user, which resolves an email to a minimal public profile (uid, displayName, photoURL) for invite-by-email flows. Authenticated callers only, and it never returns the email, roles or metadata of the user it found. Off by default: it is an enumeration surface. |
false |
AUTH_COOKIE_SAME_SITE |
SameSite on the refresh cookie: Strict, Lax or None. None requires HTTPS and is only for a genuinely cross-site frontend. |
Lax |
GOOGLE_CLIENT_ID |
Google OAuth client ID (backend validation) | — |
GOOGLE_CLIENT_SECRET |
Google OAuth client secret | — |
GITHUB_CLIENT_ID |
GitHub OAuth client ID | — |
GITHUB_CLIENT_SECRET |
GitHub OAuth client secret | — |
MICROSOFT_CLIENT_ID |
Microsoft OAuth client ID | — |
MICROSOFT_CLIENT_SECRET |
Microsoft OAuth client secret | — |
REBASE_SERVICE_KEY |
Static admin API key. Bypasses normal JWT auth for server-to-server calls when passed as Authorization: Bearer <key>. (Auto-generated in development). |
— |
REBASE_RATE_LIMIT_STORE |
Where auth rate-limit counters live: memory (per-process) or sql (shared across replicas). A process cannot see its own replica count, so a deployment with peers has to say so — three replicas on the default enforce three times the limit. Any other value refuses to boot rather than falling back, postgres included. |
memory |
Storage
Section titled “Storage”| Variable | Description | Default |
|---|---|---|
STORAGE_TYPE |
Storage backend: local, s3 or gcs. In production local disables storage unless FORCE_LOCAL_STORAGE=true |
local |
STORAGE_PATH |
Base path for local storage | ./uploads |
FORCE_LOCAL_STORAGE |
Allow local storage in production — only with a durable volume mounted at STORAGE_PATH |
false |
S3_BUCKET |
S3 bucket name (when STORAGE_TYPE=s3) |
— |
S3_REGION |
AWS region | — |
S3_ACCESS_KEY_ID |
AWS access key | — |
S3_SECRET_ACCESS_KEY |
AWS secret key | — |
S3_ENDPOINT |
Custom S3 endpoint (for MinIO, Cloudflare R2, etc.) | — |
S3_FORCE_PATH_STYLE |
Force path-style URLs for S3 bucket (true/false) |
false |
Email (Optional)
Section titled “Email (Optional)”| Variable | Description |
|---|---|
SMTP_HOST |
SMTP server host |
SMTP_PORT |
SMTP server port |
SMTP_SECURE |
Enable secure connection (true/false) |
SMTP_USER |
SMTP username |
SMTP_PASS |
SMTP password |
SMTP_FROM |
Sender address for system emails |
SMTP_NAME |
Display name on the sender address |
APP_NAME |
Product name used in email subjects and bodies (default: Rebase) |
EMAIL_LOGO_URL |
Logo shown atop the default email templates. Absolute http(s) PNG or JPG — clients strip SVG and block data: URIs. Unset, an app still named Rebase gets the Rebase mark and a renamed one gets none |
Database connection pool
Section titled “Database connection pool”| Variable | Description | Default |
|---|---|---|
DB_POOL_MAX |
Maximum pooled connections | 20 |
DB_POOL_IDLE_TIMEOUT |
Milliseconds an idle connection is kept | 30000 |
DB_POOL_CONNECT_TIMEOUT |
Milliseconds to wait for a connection | 10000 |
DATABASE_DIRECT_URL |
Direct (non-pooled) connection. Realtime needs one: LISTEN/NOTIFY does not survive a transaction pooler such as PgBouncer, and without it change notifications are disabled with a warning rather than silently lost. |
— |
DATABASE_READ_URL |
Read replica. Reads go there when it is set and differs from DATABASE_URL; if the connection fails, everything falls back to the primary with a warning. |
— |
Runtime behaviour
Section titled “Runtime behaviour”Read by the runtime — rebase dev, rebase start and the published server
image. A project that has ejected owns these decisions in its own code instead.
| Variable | Description | Default |
|---|---|---|
REBASE_BASE_PATH |
Base path for every API route. The client must be told the same thing — see Changing basePath. |
/api |
REBASE_SERVE_STATIC |
Serve the bundle’s static/admin assets from this process. Turn it off when a CDN sits in front. | true |
REBASE_HISTORY |
Record entity change history. | true |
REBASE_COMPRESSION |
gzip/brotli responses. | true |
REBASE_MAX_BODY_SIZE |
Maximum request body, in bytes (10485760, not 10MB — a value that is not a number refuses to boot rather than silently removing the limit). |
— |
REBASE_ENABLE_SWAGGER |
The OpenAPI surface. Tri-state: unset means on in development, off in production; false turns both off anywhere. Note that true in production serves the spec at /api/docs but not the Swagger UI at /api/swagger — the UI is gated on NODE_ENV separately. |
— |
REBASE_METRICS |
Expose Prometheus metrics at /metrics. |
false |
REBASE_METRICS_TOKEN |
Bearer token guarding /metrics. Unset leaves the endpoint open to anything that can reach the port — fine on a private network, not on a public one, and the boot logs say so. |
— |
REBASE_MIGRATE_ON_BOOT |
What the runtime may do to the schema at boot. ensure (the default, everywhere — production included) runs the additive pass: create missing tables, columns and enum types, never drop or rewrite one. none touches nothing. The published image accepts only those two and refuses to boot on push. In a split deployment exactly one process may provision, so every other role must set none or refuse to boot. |
ensure |
REBASE_REQUIRE_SCHEMA_MATCH |
Refuse to boot when the database was last provisioned from a different set of collections than this process was built from. Unset (or anything other than true/1) warns instead. |
warn |
REALTIME_CDC |
Database-level change capture: auto (enable where the connection supports it, silently fall back otherwise), trigger (force it, warn if impossible), wal (degrades to trigger today), off. See Realtime. |
auto |
REALTIME_CHANNEL_BUS |
Cross-instance transport for broadcast channels and presence: memory or postgres. Ignored when realtime.bus was given a constructed transport. |
memory |
ALLOW_LOCALHOST_IN_PRODUCTION |
Permit localhost/loopback values under NODE_ENV=production. Off, so a production boot fails loudly rather than connecting to a database that is not there. |
false |
Split deployments
Section titled “Split deployments”One image and one bundle can be booted several times over, each serving a
different part of the project. REBASE_ROLE, REBASE_CRON_SCHEDULER,
REBASE_JOB_WORKERS, REBASE_FUNCTIONS_ONLY, REBASE_FUNCTIONS_EXCLUDE and
REBASE_FUNCTIONS_UPSTREAM are documented in full on
Split Processes.
Backups
Section titled “Backups”| Variable | Description | Default |
|---|---|---|
BACKUP_SCHEDULE |
Cron expression for scheduled backups. Unset means scheduled backups are off. | — |
BACKUP_DESTINATION |
Local path, or an s3://bucket/prefix / gs://bucket/prefix URL. |
./backups |
BACKUP_RETENTION_DAYS |
Delete backups older than N days. Unset or 0 keeps everything. |
— |
BACKUP_KEEP_MINIMUM |
Always retain at least N of the most recent backups, whatever retention says. | — |
PG_DUMP_PATH |
Override the pg_dump binary — it must match the server’s major version. |
— |
PG_RESTORE_PATH |
Override the pg_restore binary. |
— |
Backups contain secrets and PII. Use a private destination with encryption-at-rest.
Secrets in development
Section titled “Secrets in development”JWT_SECRET and REBASE_SERVICE_KEY are required in production and generated
for you outside it, so you can start without setting anything up.
Those generated values are cached in .rebase-dev-secrets.json, beside
.rebase-dev-port and .rebase-dev-url and gitignored with them. Before, they
were regenerated on every boot — so restarting the dev server logged you out of
your own app and invalidated any API key you had just created.
- Set either variable explicitly and yours is used; nothing is cached or read.
- Point the cache somewhere else with
REBASE_DEV_SECRETS_FILE. - Delete the file to roll both secrets. The next boot writes a fresh one.
- If the file cannot be written — a read-only container, say — the server starts anyway with an ephemeral secret, exactly as it used to.
Nothing is cached in production, or under a test runner. In production a boot that had to generate either secret still fails, naming the variable, and that is unchanged:
JWT_SECRET must be explicitly set in production.Do not rely on auto-generated secrets outside development.Backend Config Object
Section titled “Backend Config Object”The RebaseBackendConfig passed to initializeRebaseBackend() provides programmatic control:
import { initializeRebaseBackend } from "@rebasepro/server";import { createPostgresAdapter } from "@rebasepro/server-postgres";import { env } from "./env";
await initializeRebaseBackend({ app, server, collectionsDir: "./config/collections", basePath: "/api", // Base path for all API routes (default: "/api")
database: createPostgresAdapter({ connection: db, schema: { tables, enums, relations } }),
auth: { // Authentication config jwtSecret: env.JWT_SECRET, accessExpiresIn: env.JWT_ACCESS_EXPIRES_IN, refreshExpiresIn: env.JWT_REFRESH_EXPIRES_IN, requireAuth: true, // Require auth for data API (default: true) allowRegistration: env.ALLOW_REGISTRATION, google: env.GOOGLE_CLIENT_ID ? { clientId: env.GOOGLE_CLIENT_ID, clientSecret: env.GOOGLE_CLIENT_SECRET } : undefined, serviceKey: env.REBASE_SERVICE_KEY },
// No bucket configured in production means storage is off, not local: // uploads answer 501 rather than landing on a filesystem that is erased // on the next redeploy. storage: env.STORAGE_TYPE === "s3" ? { type: "s3", bucket: env.S3_BUCKET!, region: env.S3_REGION, accessKeyId: env.S3_ACCESS_KEY_ID, secretAccessKey: env.S3_SECRET_ACCESS_KEY, endpoint: env.S3_ENDPOINT } : env.STORAGE_TYPE === "gcs" ? { type: "gcs", bucket: env.GCS_BUCKET!, projectId: env.GCS_PROJECT_ID, keyFilename: env.GCS_KEY_FILENAME } : isProduction && !env.FORCE_LOCAL_STORAGE ? undefined : { type: "local", basePath: env.STORAGE_PATH || "./uploads" },
history: true, // Enable entity change history
enableSwagger: true, // Enable OpenAPI docs at /api/data/docs
logging: { level: "info" }});Changing basePath
Section titled “Changing basePath”basePath moves every API route, so the client has to be told the same thing —
otherwise it keeps asking for /api/... and gets a 404 for everything:
import { createRebaseClient } from "@rebasepro/client";
export const rebase = createRebaseClient({ baseUrl: "https://api.example.com", apiPath: "/v1" // must match the backend's basePath});The admin panel picks this up from the client it is given; nothing else needs
configuring. If you build a request URL by hand, join it from the client rather
than writing /api yourself:
import { useApiBase } from "@rebasepro/app";
function Widget() { const apiBase = useApiBase(); // e.g. "https://api.example.com/v1" // fetch(`${apiBase}/data/products`)}Troubleshooting
Section titled “Troubleshooting”SQL Editor Permission Denied (permission denied for table <name>)
Section titled “SQL Editor Permission Denied (permission denied for table <name>)”- Symptoms: Custom queries executed in the Rebase Studio SQL Editor fail with
cause: error: permission denied for table <name>, even though the spreadsheet CMS view loads data successfully. - Cause: By default, Rebase attempts to execute SQL Editor queries by temporarily switching database roles to match the active user’s application role (e.g.,
SET LOCAL ROLE "admin"). If you are using custom authentication where roles exist only in database tables rather than actual PostgreSQL roles, the role switch fails or database privileges are missing. The CMS spreadsheet view executes under the default connection owner user and bypasses this. - Solution: Add
DISABLE_DB_ROLE_SWITCHING=trueto your backend.envconfiguration. This forces Rebase to run SQL Editor queries using the connection owner’s privileges (typically a superuser/owner).
SQL Editor Schema Fetch Failed (Cross-database execution requires adminConnectionString)
Section titled “SQL Editor Schema Fetch Failed (Cross-database execution requires adminConnectionString)”- Symptoms: Studio fails to load the schema tree, or SQL Editor throws
Failed to fetch schema: Cross-database execution requires adminConnectionString to be configured in the backend. - Cause: Rebase requires administrative privileges to query database system catalogs and run administrative commands. If
adminConnectionStringis not provided to the bootstrapper, orgetAdmin()is overridden to returnundefined, these operations fail. - Solution: Ensure
adminConnectionStringis configured during backend bootstrapper initialization:createPostgresBootstrapper({connection: db,schema: { tables, enums, relations },adminConnectionString: process.env.ADMIN_CONNECTION_STRING || process.env.DATABASE_URL})
Next Steps
Section titled “Next Steps”- Deployment — Production deployment guide
- Backend Overview — Full backend configuration reference
