ADR-001: Unified Integrations Table Architecture¶
Status: Accepted
Date: 2025-11-10
Deciders: root (with AI assistant)
Technical Story: docs/specs/spec_threema_integration_v2.md
Context¶
We are adding Threema as a new notification channel to Swisper Signals. The current implementation stores integration fields directly in the user table:
class User(UserBase, table=True):
# Telegram integration (notification channel)
telegram_verification_code: str | None
telegram_chat_id: str | None
telegram_connected_at: datetime | None
# WealthOS integration (data source)
wealthos_api_key: str | None
wealthos_connected_at: datetime | None
Problem: This pattern doesn't scale: - Each new integration (Threema, WhatsApp, Signal, Slack, Zapier) requires adding 3-5 fields to User table - Violates separation of concerns (user identity ≠ integrations) - Cannot support multiple integrations of same type per user - Difficult to query "all users with integration X" without scanning entire user table - Migration complexity grows with each integration - Mixes different integration types (notification channels + data sources) with no structure
Decision¶
For Threema and all future integrations (notification channels AND data sources), we will use a unified integrations table:
class IntegrationType(str, Enum):
"""All integration types in one place"""
# Notification channels
THREEMA = "threema"
# Future: WHATSAPP, SIGNAL, DISCORD, SLACK, etc.
# Data integrations (when migrated)
# TELEGRAM, WEALTHOS, GMAIL, OFFICE365, etc.
class UserIntegration(SQLModel, table=True):
"""
Unified table for ALL user integrations
One-to-many: User -> UserIntegrations
"""
__tablename__ = "integrations"
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
user_id: uuid.UUID = Field(foreign_key="user.id", index=True)
integration_type: IntegrationType = Field(index=True)
# Integration-specific configuration (flexible JSON)
# Examples:
# - Threema: {"threema_id": "ABCD1234", "registration_token": "..."}
# - WealthOS: {"api_key": "...", "last_sync_at": "..."}
# - Telegram: {"chat_id": "123", "verification_code": "1234"}
config: dict = Field(default_factory=dict, sa_column=Column(JSON))
# Connection metadata (common to all integrations)
is_active: bool = Field(default=True)
connected_at: datetime
disconnected_at: datetime | None
last_used_at: datetime | None
Existing integrations remain unchanged (Telegram, WealthOS in User table) to avoid breaking existing functionality. They will be migrated incrementally (see TD-001).
Rationale¶
Why Unified Integrations Table?¶
- Ultimate Scalability: Add unlimited integrations (notification channels + data sources) without User table changes
- Separation of Concerns: User identity separate from integrations
- Unified Management: All integrations managed the same way (consistent API)
- Flexibility: Support multiple integrations of same type per user (future: personal + work accounts)
- Performance: Indexed queries for "all users with integration X" without full table scan
- Soft Delete: Preserve connection history for analytics and audit
- Flexible Schema: JSON config field allows per-integration customization without schema changes
- Type Safety: Pydantic schemas validate config structure in service layer
Why JSON Config Field?¶
Pros: - ✅ No schema changes when adding new integration types - ✅ Each integration can have unique fields (Threema ID vs. API key vs. chat ID) - ✅ Easy to add optional fields per integration - ✅ JSONB in PostgreSQL supports indexes and queries when needed
Cons: - ⚠️ No database-level schema enforcement (handled in service layer with Pydantic) - ⚠️ Typos in JSON keys won't be caught by database (caught by validation)
Mitigation:
- Use Pydantic schemas for each integration type (e.g., ThreemaConfig, WealthOSConfig)
- Validate on save and load in service layer
- Type hints in code maintain type safety
Why Keep Telegram/WealthOS in User Table (For Now)?¶
- Lower Risk: Don't break existing Telegram/WealthOS users
- Faster Delivery: Focus on Threema implementation
- Proof of Concept: Demonstrates unified pattern works before migrating existing integrations
- Clear Migration Path: Telegram/WealthOS refactor becomes separate, well-scoped task (TD-001)
Alternatives Considered¶
Option 1: Keep adding fields to User table - ❌ Doesn't scale - ❌ Poor separation of concerns - ❌ Rejected
Option 2: Separate notification_channels table (only for notifications) - ✅ Clearer purpose (just notifications) - ❌ Still need separate table for data integrations (WealthOS, etc.) - ❌ Creates two new tables instead of one unified table - ❌ Rejected (too narrow scope)
Option 3: Refactor all integrations first, then add Threema - ✅ Consistent architecture from start - ❌ Higher risk (might break Telegram, WealthOS) - ❌ Much slower delivery (3-4 weeks before Threema) - ❌ Rejected (prefer pragmatic incremental approach)
Option 4: Unified integrations table for new integrations, migrate existing later (CHOSEN) - ✅ Lower risk (don't touch working code) - ✅ Faster Threema delivery - ✅ Best pattern demonstrated (unified table for all integration types) - ✅ Clear migration path for existing integrations (TD-001) - ⚠️ Temporary inconsistency (acceptable trade-off)
Consequences¶
Positive¶
- ✅ Ultimate Scalability: Add ANY integration (notifications, data sources, webhooks) without User table changes
- ✅ Unified Management: All integrations managed the same way (consistent API, predictable patterns)
- ✅ Flexible Schema: JSON config supports integration-specific needs without migrations
- ✅ Performance: Efficient indexed queries for integration-specific operations
- ✅ Audit Trail: Soft deletes preserve connection/disconnection history
- ✅ Multi-Integration: Can support 2+ integrations of same type per user (future: personal + work accounts)
- ✅ Type Safety: Pydantic validation in service layer ensures config structure correctness
Negative¶
- ⚠️ Inconsistency: Three patterns in codebase (Telegram in User, WealthOS in User, Threema in integrations)
- ⚠️ Technical Debt: Telegram + WealthOS refactoring postponed (see TD-001)
- ⚠️ Query Complexity: Need JOIN for "user with Threema" vs. direct field check for Telegram/WealthOS
- ⚠️ Validation Burden: Must validate JSON config in code (not enforced by database schema)
Risks¶
- ⚠️ Developer Confusion: New developers might not understand why three patterns exist
-
Mitigation: Clear code comments referencing this ADR and TD-001 in all relevant files
-
⚠️ Forgotten Refactor: Telegram/WealthOS might stay in User table indefinitely
-
Mitigation: TD-001 tracked in backlog with Medium priority, revisit when adding 3rd integration
-
⚠️ Config Validation Bugs: Typos in JSON config keys won't be caught by database
- Mitigation: Strict Pydantic validation in service layer, comprehensive tests
Implementation¶
Database Schema¶
New table:
CREATE TABLE integrations (
id UUID PRIMARY KEY,
user_id UUID REFERENCES "user"(id) ON DELETE CASCADE,
integration_type VARCHAR(50) NOT NULL,
config JSONB DEFAULT '{}'::jsonb NOT NULL,
is_active BOOLEAN DEFAULT TRUE NOT NULL,
connected_at TIMESTAMP WITH TIME ZONE NOT NULL,
disconnected_at TIMESTAMP WITH TIME ZONE,
last_used_at TIMESTAMP WITH TIME ZONE
);
-- Performance indexes
CREATE INDEX ix_integrations_user_id ON integrations(user_id);
CREATE INDEX ix_integrations_integration_type ON integrations(integration_type);
CREATE INDEX ix_integrations_user_type ON integrations(user_id, integration_type);
-- Future: GIN index for JSON queries if needed
-- CREATE INDEX ix_integrations_config ON integrations USING GIN(config);
Code Structure¶
- Model:
backend/app/models.py-UserIntegration,IntegrationType - Migration:
backend/app/alembic/versions/[timestamp]_create_integrations_table.py - Services: Use
UserIntegrationfor Threema and all future integrations - Validation: Pydantic schemas for each integration type's config (in service layer)
- Documentation: Code comments reference this ADR in models.py and services
Example: Threema Config Validation¶
# In service layer (e.g., threema_registration_service.py)
from pydantic import BaseModel
class ThreemaConfig(BaseModel):
"""Type-safe config structure for Threema integration"""
threema_id: str | None = None
registration_token: str | None = None
registration_token_expires_at: datetime | None = None
# Usage
def get_threema_config(integration: UserIntegration) -> ThreemaConfig:
"""Get validated Threema config"""
return ThreemaConfig(**integration.config)
Related¶
- Technical Debt: TD-001 - Telegram + WealthOS Migration to integrations table
- Spec:
docs/specs/spec_threema_integration_v2.md - Plan:
docs/plans/plan_threema_integration_v1.md - Code:
backend/app/models.pylines ~84-169 (UserIntegration, IntegrationType)- Threema services will reference UserIntegration model
- Tests:
backend/tests/api/test_notification_channels_foundation.py(will be renamed to test_integrations_foundation.py)
Scope Note¶
Initial Implementation (Threema):
- Only IntegrationType.THREEMA supported
- Proves unified integrations pattern works
Future Scope (incremental migration): - Migrate Telegram to integrations table (TD-001) - Migrate WealthOS to integrations table (TD-001) - Add WhatsApp, Signal, Discord as needed - Add Slack, Zapier, make.com as needed - Consider migrating Gmail/Office365 (separate evaluation)
Key Insight: This ADR establishes the long-term pattern for ALL user integrations, but implementation is incremental to minimize risk.
Status Updates:
- 2025-11-10: Decision made and documented - unified integrations table for all integration types
- Future: When all integrations migrated, update this ADR with "Migration Complete" note