Prerequisites
Environment variables
.env.example is the complete, current reference — copy it to .env and
fill in real values. The groups:
Application
Application
APP_ENV (development | staging | production | test),
APP_DEBUG, APP_SECRET_KEY, APP_VERSION, API_PREFIX,
API_DOCS_ENABLED. In production, APP_DEBUG and API_DOCS_ENABLED
must be false — Settings refuses to boot otherwise.Database
Database
Three separate connection strings, one per role:
DATABASE_URL
(seo_app, RLS-enforced), DATABASE_PLATFORM_URL (seo_platform,
BYPASSRLS), DATABASE_MIGRATION_URL (seo_migration, full DDL). See
Multi-Tenancy for why there are three.Redis
Redis
REDIS_URL backs the Celery broker/result-backend split
(CELERY_BROKER_URL, CELERY_RESULT_BACKEND), the event bus (Redis
Streams), the registry cache, and the rate limiter — five
responsibilities on one instance today. See
Deployment → Observability for the
noeviction policy this requires.Vault
Vault
VAULT_URL, plus either VAULT_DEV_TOKEN (local/dev only) or
VAULT_APPROLE_ROLE_ID + VAULT_APPROLE_SECRET_ID (staging/production
— a dev token is rejected at boot when APP_ENV=production). See
Security.AI model providers
AI model providers
ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_AI_API_KEY,
OLLAMA_BASE_URL, VLLM_BASE_URL/VLLM_API_KEY. None are required to
boot — providers load lazily — but an agent whose configured model
needs a missing key will fail at call time, not startup.Integrations
Integrations
OAuth app credentials for Google (GSC/GA4/Gmail), WordPress.com, and
Slack. Only needed once you connect a tenant’s integrations — see
Integrations.
Observability
Observability
OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_SERVICE_NAME, PROMETHEUS_PORT,
LOG_LEVEL, LOG_FORMAT, and (Celery workers only)
PROMETHEUS_MULTIPROC_DIR. See
Deployment → Observability.Database roles
MAEL’s RLS design uses four distinct PostgreSQL roles instead of one shared connection:seo_app has a sentinel default of
00000000-0000-0000-0000-000000000000 for app.current_tenant_id. A
session that forgets SET LOCAL app.current_tenant_id sees zero rows —
fail-closed, not fail-open. Full detail in
Multi-Tenancy.
Common pitfalls
PgBouncer + prepared statements
PgBouncer + prepared statements
PgBouncer runs in
transaction pooling mode, which is incompatible
with asyncpg’s default prepared-statement cache. The engine is
configured with prepared_statement_cache_size=0 for exactly this
reason (core/database/engine.py) — don’t remove it if you’re
connecting through PgBouncer.uv, not pip
uv, not pip
pip install -e . will resolve dependencies differently than
uv.lock pins. Every environment — local, CI, Docker — installs via
uv sync --locked, which fails loudly on a stale lockfile instead of
silently re-resolving.Docker Desktop + WSL2
Docker Desktop + WSL2
If you’re on Windows/WSL2, make sure Docker Desktop’s WSL integration
is enabled for your distro (
docker should resolve without a full
path). Without it, docker/docker compose aren’t on PATH inside
the WSL shell even though Docker Desktop is running.Coverage gate on partial test runs
Coverage gate on partial test runs
pytest enforces an 80% coverage floor by default
(--cov-fail-under=80 in pyproject.toml). Running a single test file
or class will report a failure that’s actually just “you didn’t run
enough of the suite to hit 80%” — add --no-cov for partial runs.