Files
saas_backend/.env.example
T

152 lines
7.7 KiB
Bash
Raw Normal View History

2026-08-31 20:04:12 -04:00
# Every setting the application reads, with the required ones marked.
#
# Twenty-two of these have no default, so a deployment missing one fails at
# startup — one at a time, in whatever order pydantic happens to check. This file
# exists so that is discoverable by reading rather than by crashing.
#
# Copy to `.env.local` (development) or set them in the environment (production).
# Nothing here is a real credential.
#
# A note on the committed `.env.*` files: they hold live values and are in the
# working tree. Rotating those secrets and purging them from git history is still
# outstanding, and every value in a committed file should be treated as known.
# ── Application ───────────────────────────────────────────────── REQUIRED ────
PROJECT_NAME=SaaS Architecture
VERSION=1.0.0
APP_ENV=local
HOST=0.0.0.0
PORT=8000
# Where the browser reaches the console. Used in emails and CORS.
FRONTEND_URL=http://localhost:5173
# ── Secrets ───────────────────────────────────────────────────── REQUIRED ────
# Three separate secrets on purpose: a token minted for one purpose must not
# verify as another. Generate with `python -c "import secrets;
# print(secrets.token_urlsafe(64))"` and never reuse one across environments.
SECRET_KEY=change-me-a-long-random-string
ACCESS_TOKEN_SECRET=change-me-a-different-long-random-string
REFRESH_TOKEN_SECRET=change-me-a-third-long-random-string
ACCESS_TOKEN_EXPIRES=900
REFRESH_TOKEN_EXPIRES=864000
# ── Database ──────────────────────────────────────────────────── REQUIRED ────
# In production this should be the unprivileged role created by
# `scripts/create_app_role.py`, NOT the owner. Row-level security does not apply
# to a role that can bypass it, and nothing in the schema shows the difference —
# `check_rls_enforced()` reports it.
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/saas
# ── Redis ─────────────────────────────────────────────────────── REQUIRED ────
# Rate limiting, SSO grant storage, the token blacklist and the replay-nonce
# store. The application starts without it; those features degrade rather than
# fail, except v2 replay controls, which refuse rather than wave requests
# through.
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_ENABLED=True
# ── Email ─────────────────────────────────────────────────────── REQUIRED ────
# Password reset codes and subscription notices. A workspace whose subscription
# lapses hears about it here or not at all.
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=noreply@example.com
SMTP_PASSWORD=change-me
SMTP_SECURE=False
EMAIL_FROM=noreply@example.com
# ── First administrator ───────────────────────────────────────── REQUIRED ────
# Used by `scripts/seed_superadmin.py`. Change the password immediately after
# the first sign-in; it is in a file on disk.
SUPER_ADMIN_EMAIL=admin@example.com
SUPER_ADMIN_PASSWORD=change-me-Str0ng!
# ── Payments ──────────────────────────────────────────────────── REQUIRED ────
PAYPAL_CLIENT_ID=change-me
PAYPAL_CLIENT_SECRET=change-me
PAYPAL_MODE=sandbox
# ── Signup ────────────────────────────────────────────────────── optional ────
# Off by default, deliberately. Public signup used to create accounts belonging
# to no workspace, which the code then treated as platform superadmins
# (finding S-1). Turn it on only with a signup flow that assigns a workspace.
ALLOW_PUBLIC_SIGNUP=False
# ── Module identity ───────────────────────────────────────────── optional ────
# Signs the tokens returned by the grant-exchange endpoint. Unset means that one
# endpoint answers 503; nothing else is affected, because the sign-on handoff and
# the event channel use per-environment HMAC.
#
# Generate with: python scripts/generate_module_key.py --env
# The public half is published at /.well-known/jwks.json for modules to verify
# against. Rotate by giving the new key a new SAAS_KEY_ID.
SAAS_PRIVATE_KEY=
SAAS_KEY_ID=saas-key-v1
# ── Module trust ──────────────────────────────────────────────── optional ────
# Requires inbound module requests to carry a timestamp and nonce (signature
# version 2). Leave off until the modules have shipped it — turning it on first
# refuses every legitimate call. See docs/MODULE_CONTRACT.md §2.
MODULE_TRUST_REQUIRE_REPLAY_CONTROLS=False
MODULE_TRUST_MAX_SKEW_SECONDS=120
# ── Alerting ──────────────────────────────────────────────────── optional ────
# Both empty means alerting is built and silent: the loop returns immediately
# rather than computing counts nobody will see. Setting either turns it on.
#
# Three conditions are sent: events not getting through, events that gave up
# entirely, and refresh tokens presented after the real client had spent them.
ALERT_WEBHOOK_URL=
ALERT_EMAIL=
ALERT_RENOTIFY_MINUTES=60
ALERT_STUCK_EVENTS_THRESHOLD=5
ALERT_STUCK_EVENTS_CRITICAL=50
ALERT_TOKEN_REUSE_WINDOW_HOURS=24
# ── CORS ──────────────────────────────────────────────────────── optional ────
# Comma-separated. Leave both empty to allow only FRONTEND_URL.
CORS_ALLOWED_ORIGINS=
CORS_ALLOW_ORIGIN_REGEX=
# --- Documents -------------------------------------------------------------
# Where uploaded files are written. A directory the application can write to and
# that is NOT served by a web server: downloads go through the API so the
# workspace and the permission are checked on every read.
DOCUMENT_STORAGE_PATH=./storage/documents
# Largest single upload, in bytes. 25 MB.
DOCUMENT_MAX_BYTES=26214400
# Total live bytes one workspace may hold. 2 GB.
DOCUMENT_QUOTA_BYTES=2147483648
# --- Audit retention -------------------------------------------------------
# The application role has UPDATE and DELETE revoked on audit_logs, so the trail
# cannot be rewritten by anything reaching the database as the application.
# Retention still has to remove expired rows, and connects as its own role with
# SELECT and DELETE on that one table.
# python scripts/create_audit_retention_role.py
# Leave unset and the retention job refuses to run — which is the point: a job
# that no-ops forever while reporting success is worse than one that fails.
AUDIT_RETENTION_DATABASE_URL=
# For the same reason, the one-off attribution backfill is an UPDATE and cannot
# run as the application either. It runs as the role that owns the schema — the
# one that runs the migrations — and only when somebody runs it by hand:
# python scripts/backfill_audit_tenants.py --dry-run
# Set here or passed as --database-url. Not needed by the running application.
AUDIT_BACKFILL_DATABASE_URL=