152 lines
7.7 KiB
Bash
152 lines
7.7 KiB
Bash
# 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=
|