Skip to content

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 db service in backend/docker-compose.yml); staging and dev run postgres:16 in a container.
  • MinIO/S3 stores recordings and voice messages — DEFAULT_FILE_STORAGE is minio_storage, and the API hands out presigned URLs.
  • Whisper, at AUDIO_TRANSCRIPTION_SERVICE, produces CallTranscription rows.
  • 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.