Skip to main content

Architecture

System diagram​

The pieces​

Backend (emaw-django-app)​

One Django project with 14 apps behind a single /rest/ prefix. It runs as a Docker Compose stack of six containers: django (gunicorn, 4 workers), worker (Celery), beat (Celery beat), redis, qdrant, and tunnel (cloudflared). Nothing publishes a port to the internet; all traffic arrives through the Cloudflare tunnel.

  • PostgreSQL is outside the stack: a VM in dev, an Azure Database for PostgreSQL flexible server in prod.
  • Redis is the cache, the Celery broker and result store, the throttle counter and the Stripe webhook de-duplication store. Caching and throttles fail open if it is down; Celery does not.
  • Qdrant holds the Bible verse embeddings for meaning-based search. It starts empty and is loaded with a management command.
  • Azure Blob (emawblob, container media) is Django's default file storage, shared by dev and prod: cover images, generated verse audio, the embeddings parquet.
  • Cloudflare R2 (emaw-public) holds website resource media (audio, video, documents) because it is served from the Cloudflare edge with no egress fees.

Frontends​

Three React single-page apps and one React Native app. All of them talk only to the backend's /rest/ API with JWT bearer tokens. None of them hold secrets.

  • The website is the only place accounts are created. It also mounts Stripe Elements for the store, events and donations.
  • The web app receives signed-in users from the website through a one-time hand-off code; it never sees the website's tokens.
  • The Admin CRM is served by a Cloudflare Worker that proxies /rest/* to the backend so that its refresh cookie is first-party and the Cloudflare Access JWT reaches Django, which rejects staff requests that did not come through Access.
  • The mobile app uses the same API with tokens in the device's secure store and downloads per-version SQLite Bibles for offline reading.

Key request paths​

Store purchase​

Events and donations follow the same shape (pending record → Checkout Session → webhook marks paid → Celery links the CRM contact and sends email). The single Stripe webhook endpoint dispatches on metadata.app.

Website → web app hand-off​

The code travels in the URL fragment so it never reaches a server log. Only origins in the backend's APP_ALLOWED_ORIGINS may exchange a code.

Staff request from the Admin CRM​

Two independent logins are therefore required for staff: the Cloudflare Access Google login and the app's own account, which must be in the Django group CRM Staff or have capabilities granted.

Dev tunnel​

In development the same cloudflared tunnel token can run on the dev VM or on a developer's laptop. Whoever runs the stack with that token is dev.emaw-app.com. See Backend local development for the hand-over procedure.

Cross-cutting conventions​

  • Deny by default. DRF's default permission is IsAuthenticated; public endpoints opt in with AllowAny.
  • Privacy by hashing. App usernames are SHA256(salt + lowercase email). Purchase history is matched to accounts by email_hash. Logs carry user ids, never emails or tokens.
  • Capabilities, not roles, for staff. The CRM resolves a capability set and a scope (all, region, areas, self) per staff user; views and querysets are filtered by it.
  • Everything idempotent on webhooks. Stripe events are de-duplicated in Redis for 24 hours.
  • One request id. X-Request-ID is set by middleware, logged, and surfaced on error toasts in the Admin CRM.