config.py and the environment. Where
both set the same key, config.py wins.
The scaffolded config.py
config.py
import os
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY', 'dev-secret-key')
DATABASE_URL = os.environ.get('DATABASE_URL', 'postgresql://localhost/myapp')
SQLALCHEMY_TRACK_MODIFICATIONS = False
# Session cookies (for OAuth)
SESSION_COOKIE_SAMESITE = "Lax"
SESSION_COOKIE_HTTPONLY = True
class DevelopmentConfig(Config):
DEBUG = True
SESSION_COOKIE_SECURE = False # allow HTTP
SESSION_PROTECTION = "basic" # relaxed for the Vite proxy
class ProductionConfig(Config):
DEBUG = False
SESSION_COOKIE_SECURE = True # HTTPS only
SESSION_PROTECTION = "basic" # marks session non-fresh on IP/UA change
A typical .env
.env
# Required
SECRET_KEY=your-production-secret-key
DATABASE_URL=postgresql://user:pass@localhost/myapp
# Authentication
GOOGLE_CLIENT_ID=your-client-id
GOOGLE_CLIENT_SECRET=your-client-secret
SESSION_LIFETIME_DAYS=7
# Multi-tenancy
FEATHER_MULTI_TENANT=true
FEATHER_ALLOW_PUBLIC_EMAILS=true
# Storage (S3_BUCKET selects s3 when STORAGE_BACKEND is unset)
STORAGE_BACKEND=local
S3_BUCKET=my-bucket
# Redis (selects the rq and redis backends when JOB_BACKEND and CACHE_BACKEND are unset)
REDIS_URL=redis://localhost:6379/0
# Background Jobs
JOB_BACKEND=thread
JOB_MAX_WORKERS=4
# Logging
LOG_LEVEL=INFO
LOG_FORMAT=json
Core
| Key | Default | Purpose |
|---|---|---|
SECRET_KEY | dev key | Signs sessions. Refused outside debug if left at the default. |
APP_NAME | the project name | The app’s name for people: sign-in emails and the admin panel. New apps set it from the project name (“my-shop” becomes “My Shop”). |
DATABASE_URL | sqlite | SQLAlchemy connection string. |
FLASK_CONFIG / FLASK_ENV | development | Which config class to load. Accepts production, prod, development, dev, testing, test. Leaving both unset logs a warning and selects development. |
SESSION_LIFETIME_DAYS | 7 | Session expiry. |
REMEMBER_COOKIE_DAYS | 365 | Remember-me cookie lifetime. |
WTF_CSRF_TIME_LIMIT | None | Token lifetime. None means the session bounds it. |
LOG_LEVEL, LOG_FORMAT | INFO, plain | Logging. JSON is automatic in production. |
Hosting and proxies
| Key | Default | Purpose |
|---|---|---|
TRUSTED_HOSTS | unset | Hostnames this app answers to, comma-separated or a list. A request with any other Host gets 400. Set this in production. |
FEATHER_PROXY_FIX | True | Install ProxyFix so forwarded headers are honoured. Turn it off when nothing in front of the app normalises them. |
FEATHER_PROXY_FIX_NUM | 1 | Number of trusted proxy hops. |
OAUTH_CALLBACK_URL | unset | Pins the OAuth redirect URI. Without it the URI comes from the request Host header. |
FEATHER_SECURITY_HEADERS | True | Send CSP, HSTS and friends in production. |
FEATHER_PERMISSIONS_POLICY | camera and microphone denied | Permissions-Policy header value. |
Authentication and tenancy
| Key | Default | Purpose |
|---|---|---|
SIGN_IN_METHOD | google | google, or email for sign-in codes. See Authentication. |
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET | unset | Google OAuth credentials. |
SIGN_IN_RELAY_URL, SIGN_IN_RELAY_TOKEN | unset | A relay that sends sign-in emails. Hosts such as Appentic set them. |
SIGN_IN_CODE_MINUTES | 15 | How long a sign-in code works. |
ADMIN_EMAIL | the --admin-email | Single-tenant: this address is an active admin from its first sign-in. |
FEATHER_MULTI_TENANT | False | Enable multi-tenant mode. |
FEATHER_ALLOW_PUBLIC_EMAILS | False | Allow Gmail, Outlook and similar domains. |
FEATHER_PRE_REGISTER_CALLBACK, FEATHER_POST_LOGIN_CALLBACK | unset | Dotted paths to hooks run around sign-up and login. |
SESSION_PROTECTION | basic | Flask-Login session protection. |
Storage, cache and jobs
| Key | Default | Purpose |
|---|---|---|
STORAGE_BACKEND | automatic | local, s3 or gcs. |
STORAGE_BACKEND_FALLBACK | local | Backend when STORAGE_BACKEND and S3_BUCKET are both unset. |
S3_BUCKET | unset | Required for the s3 backend. Setting it selects s3 when STORAGE_BACKEND is unset. |
S3_ENDPOINT | unset | Endpoint URL of an S3-compatible service. Unset means AWS. |
S3_REGION | boto3 default | Region name. |
S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY | unset | Credentials. Leave both unset to use the boto3 credential chain. |
S3_ADDRESSING_STYLE | path with S3_ENDPOINT, else auto | path, virtual or auto. |
S3_URL_EXPIRES | 3600 | Lifetime of the presigned URLs upload() and get_url() return, in seconds. |
S3_PUBLIC_URL | unset | Base URL of a public bucket or CDN. URLs become S3_PUBLIC_URL/<key> and nothing is presigned. |
GCS_BUCKET | unset | Required for the gcs backend. |
STORAGE_BLOCKED_EXTENSIONS | html, htm, svg, xhtml, xml, js, mjs, php, phtml | Extensions upload refuses on the local and s3 backends, because they are served with script-capable content types. Setting this replaces the list. |
STORAGE_ALLOWED_EXTENSIONS | unset | Allow-list. Wins over the block list, so this is how you permit SVG. |
CACHE_BACKEND | automatic | memory or redis. |
CACHE_BACKEND_FALLBACK | memory | Backend when CACHE_BACKEND and REDIS_URL are both unset. |
CACHE_URL | REDIS_URL | Redis URL for the redis cache. |
JOB_BACKEND | automatic | sync, thread or rq. |
JOB_BACKEND_FALLBACK | sync | Backend when JOB_BACKEND and REDIS_URL are both unset. The scaffold sets thread for apps with background jobs. |
JOB_MAX_WORKERS | 4 | Thread pool size for the thread backend. |
JOB_SERIALIZER | json | pickle only if a job argument cannot be JSON-encoded. Pickle payloads are code execution for anyone who can write to Redis. |
RATELIMIT_STORAGE_URI | unset | Where Flask-Limiter keeps counters. Falls back to REDIS_URL, then to memory with a warning. |
REDIS_URL | unset | Redis for the rq and redis backends. Setting it selects them when JOB_BACKEND and CACHE_BACKEND are unset. |
Automatic backends
When the app does not set a backend, Feather picks one from what the environment provides. A hosting platform that injectsREDIS_URL or S3_BUCKET then works without
further configuration.
| Setting | 1. Explicit value | 2. Selected by | 3. Otherwise |
|---|---|---|---|
JOB_BACKEND | always wins | REDIS_URL gives rq | JOB_BACKEND_FALLBACK, else sync |
CACHE_BACKEND | always wins | REDIS_URL gives redis | CACHE_BACKEND_FALLBACK, else memory |
STORAGE_BACKEND | always wins | S3_BUCKET gives s3 | STORAGE_BACKEND_FALLBACK, else local |
config.py or in the environment. An empty string counts
as unset. A config.py line such as JOB_BACKEND = os.environ.get("JOB_BACKEND", "thread")
is explicit, so that app keeps thread whatever the environment holds. The scaffold writes
JOB_BACKEND = os.environ.get("JOB_BACKEND") so the choice is left to Feather.
The app resolves the three settings once at startup, writes the result back into
app.config, and logs it at INFO:
Backends: jobs=rq (REDIS_URL is set), cache=redis (REDIS_URL is set), storage=local (default)
With
REDIS_URL set and JOB_BACKEND unset, jobs go to Redis and only run if a
feather worker process is consuming them. Set JOB_BACKEND=thread or sync when you
run no worker.Development
| Key | Default | Purpose |
|---|---|---|
VITE_DEV_SERVER | http://localhost:5173 | Where debug-mode island scripts are loaded from. |
FEATHER_NO_VITE | unset | Serve built island assets instead. feather dev --no-vite sets it. |
FEATHER_NO_UPDATE_CHECK | unset | Skip the CLI’s PyPI version check. |
feather env check reports which of these keys your config.py reads and which are
missing from your environment. It exits non-zero when a key with no fallback is unset, so
it works as a CI or deploy gate.