For the fastest path to a running stack, see Quickstart. This page is the reference for everything that page glosses over.

Prerequisites

The Postgres image is never vanilla postgres — always pgvector/pgvector:pg16 (or the project’s own docker/postgres. Dockerfile, which just adds a Vault-aware entrypoint on top of it). Role creation, RLS policies, and the fail-closed sentinel tenant all come from docker/init/postgres-init.sql, which vanilla Postgres never runs.

Environment variables

.env.example is the complete, current reference — copy it to .env and fill in real values. The groups:
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.
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_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_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.
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.
OAuth app credentials for Google (GSC/GA4/Gmail), WordPress.com, and Slack. Only needed once you connect a tenant’s integrations — see Integrations.
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 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.
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.
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.
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.

Next

Architecture overview

Deploy to production