Skip to content

Environment variables

All env vars use the SM_ prefix. Settings are loaded at boot — anything that must be known before the DB is open lives here. Runtime-tunable settings (SMTP creds, feature flags, storage backends) live in the DB-backed settings store and are edited from /admin/settings/.

This is the full reference. See Configuration for a narrative overview.

Framework

VariableDefaultNotes
SM_DATABASE_URLsqlite+aiosqlite:///./app.dbRequired in production. Async URL: postgresql+asyncpg://user:pw@host:5432/db or sqlite+aiosqlite:///./app.db. Relative sqlite paths resolve against the project root (the .env location), not the process cwd — CLI tools run from host/ hit the same file as the app.
SM_ENVIRONMENTdevelopmentdevelopment and testing are the only non-prod values (placeholder-secret check is skipped for both). Any value other than development triggers strict module discovery.
SM_SECRET_KEYchange-me-in-productionMust be overridden in production — session cookie signing key.
SM_DEBUGfalseEnables debug mode (tracebacks in HTTP responses).
SM_LOG_LEVELINFODEBUG/INFO/WARNING/ERROR.
SM_LOG_FORMATjsontext for readable dev logs, json for structured logs in prod.
SM_MODULES_ENABLEDunsetComma-separated allow-list to disable modules without uninstalling them.
SM_VITE_DEV_URLhttp://localhost:5050Dev only — where the Vite HMR client connects. Freshly scaffolded apps also derive the Vite dev server's port and origin from this one value (via client_app/vite.dev-url.ts), so changing it here is the whole move.
SM_VITE_PORT5050Dev only — port this repo's own host vite.config.ts binds to. If you change it, set SM_VITE_DEV_URL to match so the backend points the HMR client at the right origin. New scaffolds don't need it — they read SM_VITE_DEV_URL directly.
SM_PROJECT_ROOTunsetOverrides project-root discovery: where the .env is looked up and what relative sqlite paths resolve against. Normally unnecessary — settings walk up from the cwd (stopping at repo boundaries and $HOME) to find the .env on their own.
SM_AUTH_PUBLIC_PATHS[]JSON array of host-level anonymous-access path prefixes. Escape hatch for exposing a route without a session when no module owns it; modules should prefer the method-aware register_public_routes hook.
SM_TRUSTED_PROXYunsetComma-separated proxy IPs / CIDRs whose X-Forwarded-* headers are trusted, or * to trust any peer (correct when the container is only reachable through one proxy). Setting it installs uvicorn's ProxyHeadersMiddleware outermost, so request logs record the real client IP and request.url.scheme reflects X-Forwarded-Proto. Required behind a TLS-terminating proxy — without it Inertia's pushState sees a cross-scheme URL, throws a SecurityError, and login breaks. Forwarded headers are never trusted by default.

DB connection pool

VariableDefaultNotes
SM_DB_POOL_SIZE10SQLAlchemy pool_size (per process).
SM_DB_MAX_OVERFLOW20SQLAlchemy max_overflow (per process).
SM_DB_POOL_PRE_PINGtrueTest connections before use.
SM_DB_POOL_RECYCLE1800Recycle connections after N seconds (helps with LB idle drops).

Pools are per process. With multiple uvicorn --workers, total connections = workers × (SM_DB_POOL_SIZE + SM_DB_MAX_OVERFLOW); keep it under the database's max_connections (Postgres default 100) or workers raise asyncpg.TooManyConnectionsError under load. See deployment for sizing examples.

Host settings (HostSettings)

These are declared on HostSettings and registered under package="host", so they appear in the admin UI at /admin/settings/. They are also readable from env: Settings combines HostSettings with BootstrapSettings and inherits its env_prefix="SM_", so each field below resolves from the matching SM_* variable at boot.

Which source actually wins depends on the field, because HostSettings is consumed through two different objects:

SettingEnv varDefaultRead fromNotes
multi_tenantSM_MULTI_TENANTfalseenv at bootDecides whether TenantMiddleware is installed. A DB write cannot install a middleware after boot.
tenant_headerSM_TENANT_HEADER""env at bootHeader identifying the tenant (empty = header lookup disabled).
i18n_default_localeSM_I18N_DEFAULT_LOCALEenenv at bootMust be in i18n_supported_locales.
i18n_supported_localesSM_I18N_SUPPORTED_LOCALES["en"]env at bootJSON array, e.g. ["en","es"].
i18n_cookie_nameSM_I18N_COOKIE_NAMElocaleenv at bootCookie storing the selected locale.
maintenance_mode(none — see below)falseDB at request timeServe everyone but admins a 503. DB-backed so flipping it needs no redeploy — see Deployment.
maintenance_message(none — see below)""DB at request timeOptional operator note on the maintenance page.

The split is not arbitrary. The boot instance (Settings(), env-derived) lands on app.state.sm.settings and is what configures middleware at construction — LocaleMiddleware captures its locale set there, and the tenancy flag decides whether TenantMiddleware is added at all. The DB-hydrated instance is a plain HostSettings on app.state.host.settings, which declares no env_prefix and so is defaults-plus-overrides; MaintenanceMiddleware reads that one on every request.

So editing the tenancy or i18n rows in the admin UI updates what the UI shows without moving what the app serves — those need the env var and a restart. Maintenance mode is the one that genuinely takes effect live.

The two maintenance fields have no working env var, and this is worth stating plainly because the shape of the code suggests otherwise. SM_MAINTENANCE_MODE=true does set maintenance_mode on the boot Settings() object — but nothing reads maintenance from there, and the DB-hydrated HostSettings that MaintenanceMiddleware does read carries no SM_ prefix, so it never sees the variable. Setting it looks plausible and silently does nothing. Flip the flag in the admin UI, or write the settings row directly.

Users module

Most users settings (signup, mailer, SMTP, OAuth/OIDC credentials, rate limits) are now DB-backed — edit them in the admin UI at /admin/settings/ under Users, not via env vars. The env vars below are the genuine bootstrap-time exceptions read at process start.

VariableDefaultNotes
SM_USERS_BOOTSTRAP_EMAILunsetAuto-creates an admin if set and the users table is empty.
SM_USERS_BOOTSTRAP_PASSWORDunsetPaired with the email above.
SM_USERS_BOOTSTRAP_USER_EMAILunsetOptional second non-admin seed user (handy in dev).
SM_USERS_BOOTSTRAP_USER_PASSWORDunsetPaired with the user email above.
SM_USERS_RESET_PASSWORD_TOKEN_SECRETdev placeholderHMAC key for password-reset tokens. Must be overridden in production (boot fails otherwise).
SM_USERS_VERIFICATION_TOKEN_SECRETdev placeholderHMAC key for email-verification tokens. Must be overridden in production (boot fails otherwise).

Background tasks (Celery)

Celery config (broker_url, result_backend, queue, retention, etc.) is now DB-backed — edit it in the admin UI at /admin/settings/ under BackgroundTasks. Defaults are redis://localhost:6379/0 (broker) and /1 (result backend). Because workers read the broker/backend once at process start, changing those values requires a worker restart.

Three fields are still read from the environment, because they have to work before any DB row exists. A stored DB value still wins once hydration runs.

VariableDefaultNotes
SM_BG_TASKS_BROKER_URLredis://localhost:6379/0Read at construction. Needed in a container: the production validator rejects a localhost broker, so without this an app with the module installed can't boot far enough to hydrate settings — and a worker process, which never sees app.state, has no other source at all.
SM_BG_TASKS_RESULT_BACKENDredis://localhost:6379/1Same reasoning as the broker.
SM_BG_TASKS_TASK_ALWAYS_EAGERfalseRead at module-import time, so test suites that never run the FastAPI lifespan can still flip it. Runs tasks synchronously in the calling process.

File storage module

File storage config is now DB-backed — edit it in the admin UI at /admin/settings/ under Files. The key fields and defaults:

SettingDefaultNotes
backendfilesystemfilesystem or s3 (S3-compatible: AWS S3, MinIO, R2).
fs_root_path./uploadsFilesystem backend root directory.
s3_bucket""S3 bucket name (required when backend is s3).
s3_region""S3 region (required when backend is s3).
s3_endpoint_url""Custom endpoint for MinIO / R2; blank uses AWS default.

Patterns

Per-module prefix

Every module-owned env var uses SM_<MODULE_UPPER>_*. Keeps settings self-describing and avoids collisions.

Comma-separated lists

Pydantic parses comma-separated strings into list[str]SM_MODULES_ENABLED=users,dashboard becomes ["users", "dashboard"]. (SM_AUTH_PUBLIC_PATHS is the exception — it takes a JSON array, e.g. ["/status", "/api/webhook"].)

Booleans

true, 1, yes, onTrue. Everything else → False. Pydantic is strict — SM_DEBUG=True works, SM_DEBUG=TRUE works, but watch for whitespace.

Placeholder-secret check

In production (SM_ENVIRONMENT not in {development, testing}), boot fails if SM_SECRET_KEY == "change-me-in-production". Override before deploying.

.env files

The app loads .env via pydantic's BaseSettings. Order of precedence:

  1. Actual environment variables.
  2. .env file in the current working directory.
  3. Defaults from the Settings class.

Keep secrets out of .env.example — check in only non-secret defaults.

Module-enabled allow-list

bash
SM_MODULES_ENABLED=users,orders,dashboard

Loads only the listed modules. Useful for:

  • TestsSM_MODULES_ENABLED=users,orders for a minimal app.
  • Worker processes — if you run a Celery worker as a separate container, it only needs background_tasks and whatever modules define tasks it processes.
  • Emergency disable — hide a broken module without rebuilding the image. Remove it from the list, restart.

Released under the MIT License.