Skip to content

Portal database model

The portal owns the operational data. This diagram shows the entities that matter and the relationships between them; framework tables (Solid Queue/Cache/Cable, Active Storage) are omitted on purpose. Diagrams are code: update this page in the same PR that changes the schema.

erDiagram
    COMPANY ||--o{ USER : employs
    COMPANY ||--o{ DOCUMENT : "scoped docs"
    COMPANY ||--o{ CONTRACT_DOCUMENT : holds
    COMPANY ||--o{ DONATION : "pledge 1%"
    COMPANY }o--|| CHARITY : "chosen cause"
    DONATION }o--|| CHARITY : "paid to"
    USER ||--o{ DOCUMENT : authors
    USER ||--o{ AUDIT_EVENT : performed
    USER ||--o{ LOGIN_EVENT : "signed in"
    USER ||--o{ BANNER_VERSION : created

    MARKETING_CONTACT ||--o{ MARKETING_CONSENT : "append only ledger"
    MARKETING_TOPIC ||--o{ MARKETING_CONSENT : about
    DEMO_STORE ||--o{ MARKETING_CONTACT : "first seen on"
    DEMO_STORE ||--o{ MARKETING_CONSENT : source
    MARKETING_CONTACT ||--o| VOUCHER : "one ever"

    AB_TEST ||--o{ AD_CAMPAIGN : splits
    USER ||--o{ AD_CAMPAIGN : approves

    GROWTH_SCENARIO {
        string name
        jsonb params "levers, hires, projects, ad budgets"
    }
    DEBT {
        int balance_pence
        int monthly_payment_pence
    }
    COMMITTED_PROJECT {
        int monthly_amount_cents
        string currency
        bool vat_included
    }
    ACTUAL_ENTRY {
        date entry_date
        int amount_pence "signed"
    }
    WORKSHOP_REQUEST {
        string company_name
        jsonb interests
        int estimated_monthly_pence
        int estimated_oneoff_pence
        int status
    }
    MARKETING_CONTACT {
        string email UK
        string first_name
        string last_name
        string company_name
        datetime confirmed_at "verified inbox"
    }
    VOUCHER {
        string code UK
        int discount_pct "1 per topic, cap 5"
        datetime expires_at "3 months"
        datetime redeemed_at
        datetime voided_at
        string void_reason
    }
    MARKETING_CONSENT {
        string action "granted or withdrawn"
        datetime occurred_at
        string source_page
    }
    ACCESS_REQUEST {
        string organisation
        int kind "client agency recruiter"
        int status
    }
    COMPANY {
        string name
        string vat_number
    }
    USER {
        string email UK
        string role
        string otp_secret "encrypted"
    }

Standalone tables (no foreign keys, by design): growth_scenarios, debts, committed_projects, actual_entries (the growth simulator treats them as independent facts), access_requests, workshop_requests (inbox items until converted), charities (vetted list), crunch_connections (accounting OAuth held by a user).

Conventions

  • Money is integer pence/cents, never floats; the column name says which.
  • Evidence tables (marketing_consents, audit_events, login_events) are append only: rows are never edited, state is derived from the latest row.
  • Anything a client could see is scoped by company_id; admin-only data has no scope.