Soft-delete DELETE /api/user/ for authenticated users (hide conversations, blacklist tokens, block staff self-delete). Sync Stripe portal cancel/change via subscription.updated/deleted webhooks and expose cancel_at_period_end for Account UI (chat_web_app#75 companion).
8.5 KiB
Chat Backend
Django + Channels API for AIML Operations chat (chatbackend.aimloperations.com).
Packaging via uv; production deploy via server-infra.
Companion frontend: chat_web_app
(node-static, not Docker).
Ticket: chat_backend#6
Layout
chat_backend/ ← repo root (Dockerfile, compose, pyproject, .gitea)
├── llm_be/ ← Django project root (manage.py)
│ ├── manage.py
│ ├── llm_be/ ← settings, urls, asgi/wsgi
│ └── chat_backend/ ← app (models, consumers, services, storage)
├── scripts/
│ ├── docker-entrypoint.sh
│ └── validate-env.sh
├── docker-compose.yml ← local/CI (bundled Postgres)
└── docker-compose.prod.yml ← server-infra (external DATABASE_URL)
Local development
Prerequisites
- Python 3.12+
- uv
- Docker + Docker Compose (optional, recommended)
- Ollama reachable at
OLLAMA_BASE_URLfor LLM features
uv (host)
cp .env.example .env
uv sync
cd llm_be
uv run python manage.py migrate
uv run python manage.py runserver 0.0.0.0:8003
Without DATABASE_URL / DB_HOST, settings fall back to SQLite (llm_be/db.sqlite3).
Tests:
cd llm_be
uv run python manage.py test
The suite is offline by default — the custom test runner
(llm_be/test_runner.py) sets SKIP_RAG_INIT=1 and a cheap password hasher, and
LLM chains are faked, so no Ollama, Chroma, SMTP or network access is needed.
Tests live in llm_be/chat_backend/tests/ (models, storage, serializers, views,
services, signals, consumers).
Non-deterministic checks against a real model server are opt-in:
cd llm_be
RUN_LIVE_OLLAMA_TESTS=1 uv run python manage.py test chat_backend.tests.test_live_ollama
Docker (dev, bundled Postgres)
docker compose up --build
App: http://localhost:8003 — Postgres via bundled db
(postgres://chat_backend:chat_backend@db:5432/chat_backend).
Compose does not read host DATABASE_URL (avoids CI/prod leaks); override
with COMPOSE_DATABASE_URL if needed.
Environment variables
| Variable | Dev default | Prod required | Notes |
|---|---|---|---|
DJANGO_ENV |
dev |
prod / beta |
|
DJANGO_SECRET_KEY |
insecure default | yes | Must be real in prod/beta |
DJANGO_DEBUG |
true when dev |
false |
|
DJANGO_ALLOWED_HOSTS |
localhost + chat hosts | yes | Comma-separated |
DJANGO_CSRF_TRUSTED_ORIGINS |
derived from hosts | optional | Full origins |
DATABASE_URL |
SQLite fallback | yes | Shared Postgres in prod |
WEB_PORT |
n/a (compose maps 8003) | 8003 |
Host port for prod compose |
OLLAMA_BASE_URL |
http://127.0.0.1:11434 |
yes | GPU host in prod: http://10.0.0.128:11434 |
OLLAMA_MODEL / OLLAMA_EMBED_MODEL |
from DEBUG |
optional | Override model names |
EMAIL_HOST_* |
empty | yes (prod/beta) | SMTP2GO |
CAPTCHA_SECRET_KEY |
empty | recommended | |
ENABLE_ACCOUNT_REGISTRATION |
false |
optional | Self-serve sign-up; keep false until ready |
STRIPE_SECRET_KEY / STRIPE_PUBLISHABLE_KEY / STRIPE_WEBHOOK_SECRET |
empty | yes for billing | Stripe API + webhook |
STRIPE_PRICE_ID |
empty | optional | Pre-created Price; else $10/mo from settings |
FRONTEND_BASE_URL |
http://localhost:3000 |
set in prod/beta | Checkout success/cancel, portal return, OAuth return |
STRIPE_PORTAL_RETURN_URL |
{FRONTEND}/account/ |
optional | Stripe Customer Portal return URL |
CORS_ALLOWED_ORIGINS |
local + chat FE (+ beta FE default) | set in prod/beta | Frontend origin(s) |
USE_TLS_PROXY |
false (dev) | true behind NPM | Sets SECURE_PROXY_SSL_HEADER |
GUNICORN_WORKERS / GUNICORN_BIND |
2 / 0.0.0.0:8000 |
optional | Entrypoint |
SKIP_RAG_INIT |
unset | CI/migrate often 1 |
Skip Chroma/Ollama boot work |
Assistant identity (Hesychia) lives in code:
llm_be/chat_backend/services/assistant_identity.py — prepended to user-facing
generation prompts (chat, RAG, data analysis). Not env-configurable.
Templates: .env.example (local), .env.prod.example (control-node secret).
Control-node secret paths (server-infra on ai-server-4080):
~/Documents/secrets/chat_backend/chat_backend_prod.env
~/Documents/secrets/chat_backend/chat_backend_beta.env
Validate with:
./scripts/validate-env.sh ~/Documents/secrets/chat_backend/chat_backend_prod.env
./scripts/validate-env.sh ~/Documents/secrets/chat_backend/chat_backend_beta.env
If DATABASE_URL password contains $, escape each as $$ for Compose.
Ollama
All clients (ollama.Client, OllamaLLM, OllamaEmbeddings, ChatOllama) use
OLLAMA_BASE_URL — never hardcoded localhost in deployed code.
| Env | Typical URL |
|---|---|
| Local (Ollama on same machine) | http://127.0.0.1:11434 |
| prod / beta (containers on adama/roslin/ai-server) | http://10.0.0.128:11434 |
Firewall / Ollama listen on ai-server-4080 must allow 10.0.0.0/24 → :11434.
File storage
Prompt attachments and workspace documents use DatabaseStorage
(chat_backend.StoredFile BinaryField in Postgres). Blobs are not written
to the container filesystem under media/.
RAG loaders that need a path materialize a short-lived temp file, then delete it.
Chroma’s vector index may still use a volume (chroma_db); that is embeddings
metadata, not the original upload.
Production / beta (docker-compose.prod.yml)
- Single
webservice; no bundled DB —DATABASE_URL→ shared Postgres (10.0.0.230). - Host port from
WEB_PORT(prod 8003; beta 8013). - Entrypoint: wait DB → migrate → collectstatic →
gunicorn+UvicornWorker(ASGI for HTTP and WebSockets). - Active/active on adama + roslin + ai-server-4080; NPM balances upstreams.
- Manual / local deploy:
# beta (day-to-day)
~/Documents/repos/server-infra/scripts/deploy.sh \
--app chat_backend --env beta --ref <sha>
# prod (intentional)
~/Documents/repos/server-infra/scripts/deploy.sh \
--app chat_backend --env prod --ref <sha>
Beta hosts / CORS: beta.chatbackend.aimloperations.com API +
https://beta.chat.aimloperations.com SPA (see .env.prod.example beta block).
DB: chat_backend_beta. Pair with server-infra#7
and frontend chat_web_app#35.
CI / CD (Gitea Actions)
| Workflow | Trigger | Action |
|---|---|---|
unittests.yml |
push + PR → master |
uv sync + manage.py test |
ci.yml |
PR → master |
same unit tests |
deploy-beta.yml |
push to master |
unit tests → docker compose tests → deploy.sh --env beta |
deploy-prod.yml |
manual workflow_dispatch only |
unit tests → docker compose tests → deploy.sh --env prod |
Push/merge to master auto-deploys beta only. Prod requires the Gitea
Run workflow button on Deploy Prod. Deploy never runs on PRs.
Frontend API notes
Self-delete account (#34)
| Method / path | DELETE /api/user/ |
| Auth | JWT (authenticated user only; always deletes request.user) |
| Optional body | { "refresh_token": "<current refresh>" } |
| Success | 200 { "detail": "Account deleted.", "deleted": true } |
| Effects | Sets deleted=True, is_active=False; soft-deletes conversations; blacklists outstanding refresh tokens |
| Staff | Staff/superuser self-delete rejected (400, code=staff_forbidden) |
| Privacy v1 | Soft-delete only (no anonymization / hard purge) |
Post-delete UX: clear local tokens → redirect to sign-in. Subsequent
/token/obtain/ fails. Do not send another user's id/email — ignored.
Subscription change / cancel (portal + webhooks)
Plan change and cancel stay on Stripe Customer Portal
(POST /api/finance/portal/). Local state syncs via
customer.subscription.updated / deleted webhooks.
GET /api/finance/subscription/ includes cancel_at_period_end and
current_period_end for Account UI messaging.
Security note
Secrets previously hardcoded in settings.py (email password, captcha, Django
secret) must live only in the control-node env file. Rotate anything that was
ever committed; never commit .env or ~/Documents/secrets/.