Integrations — Architecture¶
Audience: Architects, tech leads, senior engineers.
Context and Purpose¶
The Integrations module exists to provide a unified, secure mechanism for connecting Swisper to external services. Each integration type has a different connection protocol (OAuth 2.0, verification code, API key), but all share common concerns: encrypted credential storage, connection lifecycle management, and a unified status API for the frontend.
The driving architectural requirements are:
- Encrypted at rest — All tokens, API keys, and configuration must be PGP-encrypted in PostgreSQL
- Separate storage models — OAuth tokens (with refresh logic, expiry, sync state) have different storage needs than simple key-value integrations, justifying two tables
- Unified status — The frontend needs a single API call to show the state of all integrations regardless of their underlying storage model
Architecture Overview¶
graph TD
subgraph Frontend["Frontend"]
UI["Integrations Settings"]
CB["OAuth Callback Pages"]
end
subgraph API["API Layer (swisper/api/routes/)"]
IE["integrations_email.py"]
IS["integrations_status.py"]
IT["integrations_telegram.py"]
ITH["integrations_threema.py"]
IW["integrations_wealthos.py"]
end
subgraph Services["Services"]
TRS["TelegramRegistrationService"]
THRS["ThreemaRegistrationService"]
end
subgraph Storage["Database (PGP-encrypted)"]
ET["emailtoken table"]
UI_T["integrations table"]
end
subgraph External["External Providers"]
GOOGLE["Google OAuth / Gmail API"]
MS["Microsoft OAuth / Graph API"]
TBOT["Telegram Bot API"]
TGW["Threema Gateway"]
WOS["WealthOS API"]
end
UI --> IS
CB --> IE
UI --> IT & ITH & IW
IE --> ET
IE --> GOOGLE & MS
IT --> TRS --> UI_T
IT --> TBOT
ITH --> THRS --> UI_T
IW --> UI_T
IS --> ET & UI_T
Component Responsibilities¶
| Component | File | Responsibility |
|---|---|---|
| Email routes | api/routes/integrations_email.py |
OAuth callback handling (code → token exchange), disconnect, default provider selection, email signature CRUD |
| Status route | api/routes/integrations_status.py |
Aggregates connection state from both emailtoken and integrations tables into a unified response |
| Telegram routes | api/routes/integrations_telegram.py |
Generate verification code, verify code (poll getUpdates for matching message), disconnect |
| Threema routes | api/routes/integrations_threema.py |
Connect (store Threema ID), status check, disconnect |
| WealthOS routes | api/routes/integrations_wealthos.py |
Connect (store API key), disconnect |
| TelegramRegistrationService | services/signals/telegram_registration_service.py |
Verification code generation, Telegram Bot API polling, chat_id extraction and storage |
| ThreemaRegistrationService | services/signals/threema_registration_service.py |
Threema ID validation, integration record creation |
Data Model¶
EmailToken (table: emailtoken)¶
Stores OAuth credentials for email/calendar providers. One row per provider per user.
| Field | Type | Purpose |
|---|---|---|
id |
UUID | Primary key |
user_id |
UUID | FK → user, CASCADE |
email |
PGPString | User's email address (encrypted) |
access_token |
PGPString | OAuth access token (encrypted) |
refresh_token |
PGPString | OAuth refresh token (encrypted, nullable) |
expires_in |
int | Token lifetime in seconds |
token_type |
str | Default "Bearer" |
provider |
Enum | GMAIL or OFFICE365 |
is_default |
bool | Whether this is the default send provider |
default_email_signature |
PGPString | HTML email signature (encrypted, nullable) |
last_email_sync |
datetime | Last successful email ingestion timestamp |
last_calendar_sync |
datetime | Last successful calendar sync timestamp |
sync_state |
PGPString | Provider-specific sync cursor (encrypted, nullable) |
UserIntegration (table: integrations)¶
Stores non-OAuth integrations with a typed JSON config field.
| Field | Type | Purpose |
|---|---|---|
id |
UUID | Primary key |
user_id |
UUID | FK → user, CASCADE |
integration_type |
Enum | TELEGRAM, THREEMA, WEALTHOS |
integration_category |
str | notification_channel or data_integration |
config |
PGPString | Encrypted JSON (schema depends on integration_type) |
is_active |
bool | Whether the integration is currently connected |
connected_at |
datetime | When the integration was established |
disconnected_at |
datetime | When the integration was disconnected (nullable) |
last_used_at |
datetime | Last time the integration was used (nullable) |
notifications_enabled |
bool | Per-channel notification toggle (nullable for data integrations) |
Config Schemas by Type¶
| Type | Config Fields |
|---|---|
| Telegram | verification_code (during setup), chat_id (after verification) |
| Threema | threema_id (8-char uppercase ID) |
| WealthOS | api_key |
Key Design Decisions¶
Decision: Two separate tables for OAuth vs. key-based integrations
- Chosen:
emailtokenfor OAuth providers,integrationsfor everything else - Rejected: Single unified table for all integration types
- Rationale: OAuth tokens have provider-specific fields (access/refresh tokens, expiry, sync state, email signature) that don't apply to key-based integrations. A single table would require many nullable columns. Separate tables keep each model focused, at the cost of the status API needing to query both.
Decision: PGP encryption for all credential fields
- Chosen: PostgreSQL PGP symmetric encryption (
PGPStringcolumn type) for tokens, API keys, and config JSON - Rejected: Application-layer encryption, vault-based secrets
- Rationale: PGP encryption at the database column level means credentials are encrypted even in database backups and direct SQL access. It requires no external infrastructure (like HashiCorp Vault) while meeting the security requirements.
Decision: Polling-based Telegram verification instead of webhook
- Chosen: Backend polls
getUpdatesfrom the Telegram Bot API to find the verification code message - Rejected: Telegram webhook that pushes messages to the backend
- Rationale: Polling is simpler to deploy (no public webhook URL needed) and works behind firewalls. The verification is a one-time operation, so the polling overhead is negligible.
Interfaces and Contracts¶
| Interface | Direction | Consumer | Contract |
|---|---|---|---|
GET /integrations/status |
Outbound | Frontend integrations page | Returns IntegrationsStatusResponse with optional gmail, office365, telegram, threema, wealthos keys |
POST /integrations/email/gmail/callback |
Inbound | Frontend OAuth redirect | { code, user_id } → exchanges code for tokens, stores encrypted |
POST /integrations/telegram/generate-code |
Outbound | Frontend | Returns 4-digit verification code |
POST /integrations/telegram/verify |
Inbound | Frontend | Polls Telegram Bot API, extracts chat_id, stores in integrations |
EmailToken → Provider adapters |
Outbound | Productivity agent, background jobs | GmailProvider / Office365Provider read tokens, auto-refresh if expired |
UserIntegration → Signal channels |
Outbound | SignalsService, notification jobs | TelegramChannel / ThreemaChannel read config.chat_id / config.threema_id for message delivery |
Known Trade-offs and Debt¶
| Item | Impact | Remediation |
|---|---|---|
| No webhook-based email sync | Email and calendar data are fetched via polling jobs, introducing a delay between arrival and availability in Swisper | Implement Gmail push notifications and Microsoft Graph subscriptions for real-time sync |
| One token per provider | Reconnecting replaces the existing token. Users cannot connect multiple Gmail accounts | Support multi-account by removing the upsert constraint and adding account selection |
| Telegram polling for verification | The getUpdates call retrieves recent messages and scans for the code. In high-traffic bots, the code message may be missed |
Consider Telegram webhooks or a longer polling window |
| No token health monitoring | If a refresh token is silently revoked, the integration fails on next use with no proactive alert to the user | Add a periodic token health check job that tests connectivity and alerts on failure |