Skip to content

Data Flow

This document shows the complete data flows through Subscribe Flow, including multi-tenant request handling, authentication, billing, and email sending.

Multi-Tenant Request Flow

Every API request is resolved to an organization context before any business logic executes.

sequenceDiagram
    participant C as Client
    participant API as FastAPI Gateway
    participant Auth as Auth Middleware
    participant Service as Business Logic
    participant DB as PostgreSQL

    C->>API: Request with API Key or Session
    API->>Auth: Authenticate
    alt API Key Auth
        Auth->>DB: Lookup api_keys WHERE key_hash = hash(key)
        DB-->>Auth: api_key.organization_id
    else Magic Link Session
        Auth->>Auth: Decode session cookie
        Auth-->>Auth: user_id + organization_id
    end
    Auth-->>API: OrgContext (org_id, tier, limits)
    API->>Service: Business Operation
    Service->>DB: SELECT ... WHERE organization_id = org_id
    DB-->>Service: Org-scoped results
    Service-->>API: Response DTO
    API-->>C: HTTP Response

Dashboard users authenticate via magic links (passwordless).

sequenceDiagram
    participant U as User (Browser)
    participant API as FastAPI
    participant MLS as MagicLinkService
    participant Email as Resend API
    participant DB as PostgreSQL

    Note over U,DB: 1. Request Magic Link
    U->>API: POST /auth/magic-link {email}
    API->>MLS: request_magic_link(email)
    MLS->>MLS: Generate token (secrets.token_urlsafe)
    MLS->>MLS: Hash token (SHA-256)
    MLS->>DB: INSERT INTO magic_link_tokens (email, token_hash, expires_at)
    MLS->>Email: Send email with login link
    Email-->>MLS: 200 OK
    MLS-->>API: Success (always, even if user not found)
    API-->>U: "Check your email"

    Note over U,DB: 2. Verify Magic Link
    U->>API: GET /auth/verify?token={token}
    API->>MLS: verify_magic_link(token)
    MLS->>MLS: Hash token (SHA-256)
    MLS->>DB: SELECT FROM magic_link_tokens WHERE token_hash = ...
    DB-->>MLS: Token record (email, expires_at, used_at)
    MLS->>DB: UPDATE magic_link_tokens SET used_at = now() (one-time use)
    MLS->>DB: SELECT user, org_membership
    DB-->>MLS: User + Organization
    MLS->>MLS: Create JWT session token
    MLS-->>API: Session token + user info
    API-->>U: Set-Cookie: session={jwt} + user data

Stripe Billing Flow

Organizations subscribe to plans via Stripe Checkout.

sequenceDiagram
    participant U as User (Dashboard)
    participant API as FastAPI
    participant BS as BillingService
    participant Stripe as Stripe API
    participant DB as PostgreSQL
    participant Beat as Celery-Beat

    Note over U,Beat: 1. Start Checkout
    U->>API: POST /api/v1/billing/checkout {price_id}
    API->>BS: create_checkout_session(org_id, price_id)
    BS->>Stripe: stripe.checkout.Session.create()
    Stripe-->>BS: checkout_url
    BS-->>API: {checkout_url}
    API-->>U: Redirect to Stripe Checkout

    Note over U,Beat: 2. Payment Complete (Webhook)
    Stripe->>API: POST /webhooks/stripe (checkout.session.completed)
    API->>BS: handle_checkout_completed(session)
    BS->>Stripe: Retrieve subscription details
    Stripe-->>BS: Subscription + price_id
    BS->>BS: Map price_id to tier (free/starter/professional)
    BS->>DB: UPDATE organizations SET tier, stripe_customer_id, stripe_subscription_id
    DB-->>BS: Success
    BS->>DB: UPDATE organizations SET subscriber_limit, email_limit, ...
    BS-->>API: 200 OK

    Note over U,Beat: 3. Monthly Email Counter Reset
    Beat->>Beat: Cron: 1st of month, 00:00 UTC
    Beat->>DB: UPDATE organizations SET emails_sent_this_month = 0

Email Sending Flow with Per-Org From Address

Each organization can have its own send domain and from address.

sequenceDiagram
    participant C as Client
    participant API as FastAPI
    participant ES as Email Service
    participant EU as Email Utils
    participant DB as PostgreSQL
    participant Resend as Resend API

    C->>API: POST /api/v1/emails/send {template, to}
    API->>ES: send_email(org_id, template_slug, to)
    ES->>DB: SELECT organization WHERE id = org_id
    DB-->>ES: Organization (send_domain, from_name, from_email)
    ES->>EU: resolve_from_address(org)
    alt Custom Domain (Professional)
        EU-->>ES: "Company Name <newsletter@custom-domain.com>"
    else Default Domain
        EU-->>ES: "Company Name <org-slug@default-send-domain.com>"
    end
    ES->>DB: SELECT template WHERE slug = ... AND organization_id = org_id
    DB-->>ES: Template + MJML
    ES->>ES: Render template with variables
    ES->>Resend: POST /emails {from, to, subject, html}
    Resend-->>ES: {id: "email_id"}
    ES->>DB: UPDATE organizations SET emails_sent_this_month += 1
    ES-->>API: {id, status: "queued"}
    API-->>C: 202 Accepted

Preference Center Flow

sequenceDiagram
    participant User as User
    participant PC as Preference Center
    participant API as FastAPI Gateway
    participant Service as Business Logic
    participant DB as PostgreSQL
    participant Redis as Redis Cache
    participant Resend as Resend API

    Note over User,Resend: 1. User opens Preference Center
    User->>PC: Click on email link with token
    PC->>API: GET /preference-center?token=xyz
    API->>Service: Validate Token & Fetch Preferences
    Service->>DB: SELECT subscriber, tags (org-scoped)
    DB-->>Service: Subscriber + Tags
    Service-->>API: Preferences Data
    API-->>PC: JSON Response
    PC->>User: Display current subscriptions

    Note over User,Resend: 2. User adds a new tag
    User->>PC: Click "Add Interest: Newsletter"
    PC->>API: POST /subscribers/{id}/tags
    API->>Service: Add Tag to Subscriber
    Service->>DB: INSERT INTO subscriber_tags
    DB-->>Service: Success
    Service->>Resend: Update Contact Topics
    Resend-->>Service: 200 OK
    Service->>Redis: Invalidate Cache
    Service-->>API: Success
    API-->>PC: 201 Created
    PC->>User: Toast "Successfully subscribed!"

    Note over User,Resend: 3. Resend sends webhook
    Resend->>API: POST /webhooks/resend (contact.updated)
    API->>Service: Process Webhook
    Service->>DB: UPDATE subscriber (org-scoped)
    Service->>Redis: Invalidate Cache
    Service-->>API: 200 OK
    API-->>Resend: Webhook Acknowledged

Caching Strategy

  • Cache Key: org:{org_id}:subscriber:{id}:preferences
  • TTL: 5 minutes
  • Invalidation: On every change (tag add/remove, subscriber update)
  • Fallback: On cache miss, fall back to PostgreSQL query
  • Magic Link Tokens: Stored in PostgreSQL (magic_link_tokens table) with 15-minute expiry
  • Rate Limit Counters: Per-organization counters in Redis