Deployment
Containers
Docker Compose in agency-server runs the full Agency stack.
Core services
WSGI (
agency_django) — HTTP REST, admin, product SPA shell at/ASGI (
agency_asgi) — WebSockets at/channel/Celery — background tasks (workers + beat; Flower optional)
Nginx (
agency_nginx) — reverse proxy and/static/
Data
PostgreSQL — primary database
ChromaDB — vector store for corpus embeddings
Redis — Celery broker and Channels layer
AI
Ollama — local LLM inference (optional GPU)
ComfyUI (
agency_comfy) — local GPU txt2img (SD 1.5); UI athttp://127.0.0.1:8188Voice (
agency_voice) — STT (faster-whisper) + TTS (Piper), proxied at/api/voice/
Workflow
Nginx reverse-proxies HTTP and WebSocket traffic to Django (WSGI/ASGI).
Protocols
HTTP/1.1
HTTP/2
Server-Sent Events (SSE) — log streams
WebSockets — see Connections
Boot
docker compose up runs migrate, collectstatic, and seed scripts (superuser, default workspace, Ollama provider).
Container names use the agency_ prefix (e.g. agency_django).
Dev commands and container aliases: Server.
Authentication
Agency uses django-allauth for authentication. The product login page is
/login/ (served by the SPA shell). Local development defaults to
username/password; production uses Google OAuth as the primary login method
with password behind a dev toggle.
URL routing
path("accounts/", include("allauth.urls")) is mounted in server.urls
before the SPA catch-all so allauth endpoints (/accounts/google/login/,
/accounts/google/login/callback/, etc.) are resolved by Django, not the
Vue router.
Login UI contract
When GOOGLE_CLIENT_ID is set, the login bootstrap JSON includes
oauth.google.enabled = true and login_url = "/accounts/google/login/".
The SPA renders a Continuar con Google button that POSTs to
/accounts/google/login/ with CSRF. Username/password fields are hidden
behind a dev toggle in production.
Google OAuth configuration
To configure Google OAuth for a production host (e.g. agency.nabla.net):
Create OAuth 2.0 credentials in the Google Cloud Console.
Configure the OAuth consent screen and add authorized domains.
Set Authorized JavaScript origins:
https://agency.nabla.net
Set Authorized redirect URIs:
https://agency.nabla.net/accounts/google/login/callback/
Set environment variables (via
.envor secrets manager):GOOGLE_CLIENT_ID=<your-client-id>.apps.googleusercontent.com GOOGLE_CLIENT_SECRET=<your-client-secret>
Site domain
allauth builds OAuth callback URLs from django.contrib.sites.Site.domain.
If the domain stays at Django’s default example.com, token exchange fails
with a redirect-URI mismatch.
boot.py calls ensure_site_domain() at startup. It sets Site.domain
from the first non-wildcard host in DJANGO_ALLOWED_HOSTS, or from the
explicit DJANGO_SITE_DOMAIN env var if set. Production example:
DJANGO_ALLOWED_HOSTS=agency.nabla.net
# or explicit override:
# DJANGO_SITE_DOMAIN=agency.nabla.net
HTTPS protocol
When DJANGO_SECURE_SSL=true, settings enables:
ACCOUNT_DEFAULT_HTTP_PROTOCOL = 'https'— allauth buildshttps://callback URLs (required when nginx terminates TLS)SECURE_PROXY_SSL_HEADER,SECURE_SSL_REDIRECTSESSION_COOKIE_SECURE,CSRF_COOKIE_SECURE
DJANGO_DEBUG=False
DJANGO_SECURE_SSL=true
DJANGO_ALLOWED_HOSTS=agency.nabla.net
DJANGO_CSRF_TRUSTED_ORIGINS=https://agency.nabla.net
SECRET_KEY stability
OAuth state is stored in the Django session. If WSGI, ASGI, and Celery containers use different (or auto-generated) secret keys, the session cannot be decoded and the callback returns 401.
Production requires a stable DJANGO_SECRET_KEY shared by all containers.
If DEBUG=False and the key is missing, startup raises RuntimeError.
# Generate once, share everywhere:
python -c "import secrets; print(secrets.token_urlsafe(64))"
DJANGO_SECRET_KEY=<generated-key>
Adapter logging
LoggingSocialAccountAdapter (server.adapters) logs OAuth errors that
allauth would otherwise swallow. Check system.log (allauth logger) when
callbacks fail.
See context/django/config.env.production.example for a complete template.
Staff / superuser
Production does not seed the insecure admin/1234 account. Staff access is
granted through first-admin promotion or an email allowlist.
First-admin promotion — on a fresh install with zero superusers, the first
user to sign in via Google is promoted to is_staff=True and
is_superuser=True. The adapter calls maybe_promote_user on every
Google login (save_user for new accounts, pre_social_login for
existing ones).
Email allowlist — DJANGO_SUPERUSER_EMAILS (comma-separated,
case-insensitive) promotes listed users on their next Google login, even when
a superuser already exists:
DJANGO_SUPERUSER_EMAILS=alice@example.com,bob@example.com
At boot, promote_allowlisted_users() promotes any existing accounts on the
list (for deploys that already have users before the allowlist is configured).
Package helpers — agency.auth.staff provides maybe_promote_user(user),
promote_allowlisted_users(), and the promote_superuser management
command for manual promotion:
docker compose exec wsgi python manage.py promote_superuser alice@example.com
Dev environment — DJANGO_SUPERUSER_PASSWORD=1234 in config.env seeds
admin/1234 for local testing. Omit or leave empty in production.