Architecture overview¶
The shape of the system¶
PSTN / SIP
│
Twilio
(Voice, SIP domains, Queues,
Conferences, Messaging)
│ webhooks + REST
▼
┌──────────────────────────────────────────┐
│ Django + DRF (backend/) │
│ /api/v1 · /webhooks/twilio · /google │
└───────┬──────────────┬───────────────────┘
│ │ mirror
│ REST (JWT) ▼
│ Cloud Firestore ──── live updates ───┐
│ │
▼ ▼
┌────────────────────┐ ┌────────────────────────┐
│ SvelteKit SPA │ │ Capacitor app │
│ (frontend/) │ │ Android / iOS │
│ Twilio Voice SDK │ │ native CallPlugin │
└────────────────────┘ └────────────────────────┘
PostgreSQL · MinIO/S3 (recordings) · Whisper (transcription)
Stripe (billing) · Graylog (logs)
The landing site (landing/) stands apart: it is a separate SSR app that only talks to the
backend through two public form endpoints.
Request paths¶
There are four distinct ways traffic reaches the backend, and they have different authentication rules.
| Path | Caller | Auth |
|---|---|---|
/api/v1/** |
The SPA and the mobile app | JWT bearer token (SimpleJWT) |
/webhooks/twilio/** |
Twilio | X-Twilio-Signature, via the IsTwilio permission class |
/google/hooks/** |
An external scheduler | None — see Known issues |
/admin/ |
Staff | Django session |
Real-time: Firestore, not WebSockets¶
This is the single most counter-intuitive thing about the codebase, so it is worth stating
plainly: there are no working WebSockets. Django Channels is installed and configured
(backend/settings/settings.py:109-138), the ASGI app routes a websocket protocol
(backend/settings/asgi.py:17), and both routers are empty —
voip/ws_urls.py:3 and calls/ws_urls.py:5 return URLRouter([]).
Live updates travel through Cloud Firestore instead. The backend writes, the clients
subscribe with onSnapshot:
| Data | Backend writer | Client reader |
|---|---|---|
| Notifications | voip/notifications/features/fs_notification.py |
FSNotifications |
| SMS threads and unread counts | voip/messages/fs_msg.py |
FSChatList, FSMessageCTL, FSUnread |
| Agent presence | voip/stateTrace/fs_status.py |
FSStatus |
| Heartbeat | voip/stateTrace/fs_heartbeat.py |
HeartBeat.ts |
Treat VITE_WSAPI_URL, WEBSOCKET_DOMAIN and CHANNELS_* as inert configuration.
Multi-tenancy¶
Company is the tenant root; almost every model holds a foreign key to it. A User is not
attached to a company directly — the CompanyUser join carries the membership and the
role (Owner, Admin, Staff) (backend/voip/companyUser/models.py:7).
Views resolve the current tenant with request.user.defaultCompanyUser().company, a method
monkey-patched onto the user model (backend/voip/companyUser/models.py:59) that returns
the single CompanyUser row flagged is_default=True. Switching company means flipping
that flag. There is no tenant header, no subdomain and no schema separation.
One company carries isAdmin=True and acts as a super-tenant whose members bypass
per-company scoping. The code itself flags the fragility of that design
(backend/voip/company/permissions.py:9-10).
Agents are polymorphic¶
An "agent" who can receive a call is either:
- a
User, reached as a Twilio Client (client:<email>) from the browser or the mobile app, or - a
SipCredential, reached at a Twilio SIP domain — a hardphone or a third-party softphone.
CallTrace and CallConfCalleeData both carry nullable foreign keys to each, and most call
routing code has to handle both branches. Keep this in mind when reading
Telephony.
Storage and media¶
- PostgreSQL holds all relational state. It is external in production (no
dbservice inbackend/docker-compose.yml); staging and dev runpostgres:16in a container. - MinIO/S3 stores recordings and voice messages —
DEFAULT_FILE_STORAGEisminio_storage, and the API hands out presigned URLs. - Whisper, at
AUDIO_TRANSCRIPTION_SERVICE, producesCallTranscriptionrows. - Graylog receives container logs through the Docker syslog driver.
Background work¶
There is no working task queue. celery appears in requirements.txt and two compose
services run celery -A settings worker|beat, but no task code exists anywhere in the
backend. The batch jobs run as HTTP endpoints under /google/hooks/ called by an
external scheduler:
| Endpoint | Job |
|---|---|
POST /google/hooks/reminders |
Turn due NoteReminder rows into notifications |
POST /google/hooks/records |
Recording payment and auto-removal (voip/crm/tasks/__init__.py:10) |
POST /google/hooks/charges |
Pull Twilio usage records into the billing mirror |
See Known issues before relying on any of this.