Database Schema¶
Subscribe Flow uses PostgreSQL 15+ as its primary database. All tenant data is isolated via organization_id foreign keys (multi-tenant, shared-database model).
Entity Relationship Diagram¶
erDiagram
ORGANIZATIONS ||--o{ ORGANIZATION_MEMBERS : has
USERS ||--o{ ORGANIZATION_MEMBERS : belongs_to
ORGANIZATIONS ||--o{ SUBSCRIBERS : owns
ORGANIZATIONS ||--o{ TAGS : owns
ORGANIZATIONS ||--o{ API_KEYS : has
ORGANIZATIONS ||--o{ WEBHOOK_ENDPOINTS : configures
ORGANIZATIONS ||--o{ EMAIL_TEMPLATES : owns
ORGANIZATIONS ||--o{ CAMPAIGNS : owns
ORGANIZATIONS ||--o{ EMAIL_TRIGGERS : owns
ORGANIZATIONS ||--o{ AUDIT_LOGS : records
SUBSCRIBERS ||--o{ SUBSCRIBER_TAGS : has
TAGS ||--o{ SUBSCRIBER_TAGS : has
SUBSCRIBERS ||--o{ SUBSCRIPTION_HISTORY : has
TAGS ||--o{ SUBSCRIPTION_HISTORY : involves
SUBSCRIBERS ||--o{ SUBSCRIBER_NOTES : has
SUBSCRIBERS ||--o{ EMAIL_SENDS : receives
CAMPAIGNS ||--o{ EMAIL_SENDS : generates
WEBHOOK_ENDPOINTS ||--o{ WEBHOOK_DELIVERIES : delivers
ORGANIZATIONS {
uuid id PK
string name
string slug UK
boolean is_platform_admin
string stripe_customer_id UK
string stripe_subscription_id UK
string subscription_tier
string subscription_status
timestamp current_period_end
int max_subscribers
int max_emails_per_month
int emails_sent_this_month
string billing_email
string email_from_name
string email_from_address
string custom_domain
string custom_domain_status
string resend_domain_id
timestamp created_at
timestamp updated_at
}
USERS {
uuid id PK
string email UK
string name
timestamp last_login_at
timestamp created_at
timestamp updated_at
}
ORGANIZATION_MEMBERS {
uuid id PK
uuid organization_id FK
uuid user_id FK
string role
timestamp created_at
}
MAGIC_LINK_TOKENS {
uuid id PK
string email
string token_hash UK
timestamp expires_at
timestamp used_at
timestamp created_at
}
SUBSCRIBERS {
uuid id PK
uuid organization_id FK
string email
string first_name
string last_name
string status
string resend_contact_id
jsonb metadata
timestamp created_at
timestamp updated_at
}
TAGS {
uuid id PK
uuid organization_id FK
string name
string display_name
string description
string category
boolean is_active
int sort_order
timestamp created_at
timestamp updated_at
}
SUBSCRIBER_TAGS {
uuid id PK
uuid subscriber_id FK
uuid tag_id FK
timestamp subscribed_at
string source
}
SUBSCRIPTION_HISTORY {
uuid id PK
uuid subscriber_id FK
uuid tag_id FK
string action
string source
string ip_address
string user_agent
timestamp changed_at
}
API_KEYS {
uuid id PK
uuid organization_id FK
string name
string key_hash
string prefix
jsonb permissions
timestamp last_used_at
timestamp expires_at
timestamp created_at
}
WEBHOOK_ENDPOINTS {
uuid id PK
uuid organization_id FK
string url
string secret
jsonb events
boolean is_active
timestamp created_at
}
WEBHOOK_DELIVERIES {
uuid id PK
uuid organization_id FK
uuid webhook_endpoint_id FK
string event_type
string status
int status_code
int response_time_ms
timestamp created_at
}
EMAIL_TEMPLATES {
uuid id PK
uuid organization_id FK
string name
string subject
text body
timestamp created_at
timestamp updated_at
}
CAMPAIGNS {
uuid id PK
uuid organization_id FK
string name
string status
uuid template_id FK
timestamp scheduled_at
timestamp sent_at
timestamp created_at
timestamp updated_at
}
EMAIL_SENDS {
uuid id PK
uuid organization_id FK
uuid subscriber_id FK
uuid campaign_id FK
string status
timestamp sent_at
timestamp created_at
}
EMAIL_TRIGGERS {
uuid id PK
uuid organization_id FK
string name
string event_type
uuid template_id FK
boolean is_active
timestamp created_at
timestamp updated_at
}
AUDIT_LOGS {
uuid id PK
uuid organization_id FK
string action
string entity_type
uuid entity_id
jsonb changes
timestamp created_at
}
SUBSCRIBER_NOTES {
uuid id PK
uuid subscriber_id FK
uuid organization_id FK
text content
timestamp created_at
timestamp updated_at
} Table Details¶
Multi-Tenant Core Tables¶
organizations¶
Central tenant table. Each organization has its own isolated data set, billing configuration, and email sending settings.
| Column | Type | Constraints | Description |
|---|---|---|---|
id | UUID | PK | Primary key |
name | VARCHAR(255) | NOT NULL | Organization display name |
slug | VARCHAR(100) | UNIQUE, NOT NULL | URL-safe identifier |
is_platform_admin | BOOLEAN | NOT NULL, DEFAULT false | Platform admin flag (nur Talent Factory GmbH) |
stripe_customer_id | VARCHAR(255) | UNIQUE | Stripe Customer ID |
stripe_subscription_id | VARCHAR(255) | UNIQUE | Stripe Subscription ID |
subscription_tier | VARCHAR(50) | NOT NULL, DEFAULT free | free, starter, professional |
subscription_status | VARCHAR(50) | NOT NULL, DEFAULT active | active, trialing, past_due, canceled |
current_period_end | TIMESTAMP | End of current billing period | |
max_subscribers | INTEGER | NOT NULL, DEFAULT 500 | Subscriber limit for current tier |
max_emails_per_month | INTEGER | NOT NULL, DEFAULT 1000 | Monthly email limit |
emails_sent_this_month | INTEGER | NOT NULL, DEFAULT 0 | Counter, reset monthly by Celery-Beat |
billing_email | VARCHAR(255) | Billing contact email | |
email_from_name | VARCHAR(255) | Custom sender name | |
email_from_address | VARCHAR(255) | Custom sender address (requires verified domain) | |
custom_domain | VARCHAR(255) | Custom sending domain | |
custom_domain_status | VARCHAR(20) | pending, verified, failed | |
resend_domain_id | VARCHAR(255) | Resend Domain ID for verification | |
created_at | TIMESTAMP | NOT NULL | Creation timestamp |
updated_at | TIMESTAMP | NOT NULL | Last modification |
Subscription Tiers
| Tier | Subscribers | Emails/Month | Price |
|---|---|---|---|
| Free | 500 | 1,000 | CHF 0 |
| Starter | 5,000 | 10,000 | CHF 19/mo |
| Professional | 50,000 | 100,000 | CHF 49/mo |
users¶
Dashboard users who authenticate via magic links.
| Column | Type | Constraints | Description |
|---|---|---|---|
id | UUID | PK | Primary key |
email | VARCHAR(255) | UNIQUE, NOT NULL | Email address (login identifier) |
name | VARCHAR(255) | Display name | |
last_login_at | TIMESTAMP | Last successful login | |
created_at | TIMESTAMP | NOT NULL | Creation timestamp |
updated_at | TIMESTAMP | NOT NULL | Last modification |
organization_members¶
Junction table linking users to organizations with roles.
| Column | Type | Constraints | Description |
|---|---|---|---|
id | UUID | PK | Primary key |
organization_id | UUID | FK, NOT NULL | Organization |
user_id | UUID | FK, NOT NULL | User |
role | VARCHAR(20) | NOT NULL, DEFAULT member | owner or member |
created_at | TIMESTAMP | NOT NULL | Membership creation |
magic_link_tokens¶
One-time-use tokens for passwordless authentication.
| Column | Type | Constraints | Description |
|---|---|---|---|
id | UUID | PK | Primary key |
email | VARCHAR(255) | NOT NULL | Target email address |
token_hash | VARCHAR(255) | UNIQUE, NOT NULL | SHA-256 hash of the token |
expires_at | TIMESTAMP | NOT NULL | Token expiry (15 minutes) |
used_at | TIMESTAMP | Set when token is consumed | |
created_at | TIMESTAMP | NOT NULL | Creation timestamp |
Token Lifecycle
Tokens are single-use. On verification, used_at is set. Expired or used tokens are rejected.
Subscriber Management Tables¶
subscribers¶
Stores all subscribers of an organization.
| Column | Type | Constraints | Description |
|---|---|---|---|
id | UUID | PK | Primary key |
organization_id | UUID | FK, NOT NULL | Associated organization |
email | VARCHAR(255) | NOT NULL, UNIQUE per org | Email address |
first_name | VARCHAR(100) | First name | |
last_name | VARCHAR(100) | Last name | |
status | VARCHAR(20) | NOT NULL | active, unsubscribed, bounced |
resend_contact_id | VARCHAR(100) | Resend Contact ID | |
metadata | JSONB | Additional data | |
created_at | TIMESTAMP | NOT NULL | Creation timestamp |
updated_at | TIMESTAMP | NOT NULL | Last modification |
Status Values
active: Subscriber receives emailsunsubscribed: Fully unsubscribedbounced: Email undeliverablecomplained: Spam complaint
tags¶
Defines available tags for subscription preferences.
| Column | Type | Constraints | Description |
|---|---|---|---|
id | UUID | PK | Primary key |
organization_id | UUID | FK, NOT NULL | Associated organization |
name | VARCHAR(100) | NOT NULL, UNIQUE per org | Technical name |
display_name | VARCHAR(200) | NOT NULL | Display name |
description | TEXT | Description for Preference Center | |
category | VARCHAR(100) | Grouping | |
is_active | BOOLEAN | NOT NULL, DEFAULT true | Tag active |
sort_order | INTEGER | DEFAULT 0 | Sort order |
created_at | TIMESTAMP | NOT NULL | Creation timestamp |
updated_at | TIMESTAMP | NOT NULL | Last modification |
subscriber_tags¶
Junction table for subscriber-tag relationships.
| Column | Type | Constraints | Description |
|---|---|---|---|
id | UUID | PK | Primary key |
subscriber_id | UUID | FK, NOT NULL | Subscriber |
tag_id | UUID | FK, NOT NULL | Tag |
subscribed_at | TIMESTAMP | NOT NULL | Subscription timestamp |
source | VARCHAR(50) | api, preference_center, import |
subscription_history¶
Complete audit trail of all preference changes.
| Column | Type | Constraints | Description |
|---|---|---|---|
id | UUID | PK | Primary key |
subscriber_id | UUID | FK, NOT NULL | Subscriber |
tag_id | UUID | FK, NOT NULL | Tag |
action | VARCHAR(20) | NOT NULL | subscribe, unsubscribe |
source | VARCHAR(50) | NOT NULL | Source of the change |
ip_address | INET | IP address | |
user_agent | TEXT | Browser user agent | |
changed_at | TIMESTAMP | NOT NULL | Timestamp |
subscriber_notes¶
Notes attached to subscribers by team members.
| Column | Type | Constraints | Description |
|---|---|---|---|
id | UUID | PK | Primary key |
subscriber_id | UUID | FK, NOT NULL | Subscriber |
organization_id | UUID | FK, NOT NULL | Organization |
content | TEXT | NOT NULL | Note content |
created_at | TIMESTAMP | NOT NULL | Creation timestamp |
updated_at | TIMESTAMP | NOT NULL | Last modification |
Email & Campaign Tables¶
All of these tables include organization_id FK for tenant isolation.
- email_templates -- Reusable email templates
- campaigns -- Email campaigns with scheduling
- email_sends -- Individual email send records
- email_triggers -- Event-triggered email automations
Webhook Tables¶
- webhook_endpoints -- Configured outgoing webhook URLs with events and signing secrets
- webhook_deliveries -- Delivery log with status, response time, retry tracking
Other Tables¶
- api_keys -- Organization-scoped API keys with hashed storage and scopes
- audit_logs -- Organization-scoped audit trail of all admin actions
Indexes¶
Multi-Tenancy Pattern¶
All data tables include an organization_id column that references the organizations table. This ensures:
- Data Isolation: Queries always filter by
organization_id - Cascading Deletes: Organization deletion removes all associated data
- Unique Constraints: Email uniqueness, tag names, etc. are scoped per organization (
UNIQUE (organization_id, email))
The organization context is resolved at the API layer:
- API Key auth: Organization is determined from the API key record
- Session auth: Organization is provided via
X-Organization-Idheader and verified against user membership
Migrations¶
Migrations are managed with Alembic:
| Bash | |
|---|---|
Migration Best Practices
- Always use descriptive
--rev-idvalues - Include docstrings in migration files
- Prefer backward-compatible changes
- In production: run migrations in a separate deployment