Entity-relationship diagram

The workspace ships two ERD artifacts, both rendered from the same live pg_catalog introspection in cli/src/erd.rs:

Regenerating

Both artifacts come from one command; --format selects the renderer. The command reads DATABASE_URL (or --database-url) and migrates that database idempotently before introspecting — point at a throwaway or already- migrated database if you don't want migrations applied in place:

set -a && source .devx/env && set +a   # DATABASE_URL for the KIND Postgres

# Mermaid erDiagram block → paste into the fenced block under Schema in docs/erd.md
cargo run -p cli -- docs erd --format mermaid

# Deterministic SVG → overwrite the committed file
cargo run -p cli -- docs erd --format svg > docs/erd.svg

After any schema-changing migration, refresh both so the picture and the schema stay in sync.

The docs/erd.svg idempotency test

cli/tests/erd_svg.rs spins up a Postgres testcontainer (store::test_support::schema()), runs migrations, introspects via docs erd --format svg, and asserts the output byte-matches the committed docs/erd.svg. The test is part of cargo test -p cli and cargo test --workspace, so a migration that changes the schema without an SVG refresh fails CI; the failure message prints the byte sizes, the first divergent line, and the exact refresh command. The intended flow: write the migration → cargo test -p cli → see the drift → regenerate → commit the migration and the refreshed SVG together on a topic branch (never on main).

Verifying against production

The deploy cron rolls migrations Mon–Thu 06:00 UTC, so local and prod should match within a day of any change. To diff (this migrates the target DB, so it is safe only against the already-at-head prod schema):

gcloud auth application-default login   # one-time per session
cloud-sql-proxy --auto-iam-authn --port 15433 YOUR_PROJECT_ID:us-west4:navigator-pg &

DATABASE_URL="postgres://${USER}@your-domain.example@127.0.0.1:15433/navigator?sslmode=disable" \
  cargo run -p cli -- docs erd --format mermaid > /tmp/prod_mermaid.txt
diff <(cargo run -p cli -- docs erd --format mermaid) /tmp/prod_mermaid.txt   # no output = identical
kill %1

See test-database.md for the local Postgres / DATABASE_URL connection story.

Opening the SVG

firefox docs/erd.svg                              # or google-chrome, or any image viewer
xdg-mime default firefox.desktop image/svg+xml    # optional: make Firefox the default SVG handler
xdg-open docs/erd.svg

Layout tuning

The layout is intentionally simple — a 4-column alphabetical row-major grid, straight-line edges, no crossing avoidance; the priority is byte-stable, readable enough. The constants live at the top of cli/src/erd.rs: CHAR_WIDTH / ROW_HEIGHT / TITLE_HEIGHT (text dimensions), CELL_PAD / CELL_GAP_X / CELL_GAP_Y (spacing), GRID_COLS (columns wide), and MARGIN / FONT_SIZE (outer margin, base font). Any layout change trips the idempotency test until docs/erd.svg is refreshed — a feature, not a bug.

Other lenses on the schema

When you don't need a picture: psql \dt+ (table list with sizes), psql \d <table> (columns, types, defaults, FKs, indexes), or cargo run -p cli -- docs erd --format mermaid (plain text you can grep).

Schema

erDiagram
    addresses {
        UUID id PK
        UUID person_id FK
        UUID entity_id FK
        CHARACTER VARYING line1
        CHARACTER VARYING line2
        CHARACTER VARYING city
        CHARACTER VARYING region
        CHARACTER VARYING postal_code
        CHARACTER VARYING country
        TEXT inserted_at
        TEXT updated_at
    }
    answers {
        UUID id PK
        UUID question_id FK
        UUID person_id FK
        JSONB value
        TEXT inserted_at
        TEXT updated_at
        CHARACTER VARYING source
        UUID authored_by_person_id FK
        UUID notation_id FK
        TEXT state_name
    }
    assets {
        UUID id PK
        TEXT storage_key
        TEXT content_type
        BIGINT byte_size
        TEXT sha256_hex
        UUID project_id FK
        TEXT filename
        TEXT kind
        TEXT source
        TEXT received_at
        TEXT description
        TEXT inserted_at
        TEXT updated_at
        TEXT secondary_storage_key
    }
    attestations {
        UUID id PK
        UUID notation_id FK
        CHARACTER VARYING chain
        CHARACTER VARYING sha256
        CHARACTER VARYING status
        CHARACTER VARYING pda
        CHARACTER VARYING tx_signature
        CHARACTER VARYING firm_wallet
        CHARACTER VARYING client_wallet
        CHARACTER VARYING recorded_at
        CHARACTER VARYING inserted_at
        CHARACTER VARYING updated_at
    }
    case_reports {
        UUID id PK
        UUID project_id FK
        CHARACTER VARYING slug
        CHARACTER VARYING title
        UUID current_asset_id FK
        CHARACTER VARYING status
        UUID created_by_person_id FK
        CHARACTER VARYING published_at
        CHARACTER VARYING inserted_at
        CHARACTER VARYING updated_at
    }
    communications {
        UUID id PK
        UUID project_id FK
        CHARACTER VARYING channel
        CHARACTER VARYING direction
        UUID author_person_id FK
        CHARACTER VARYING counterparty
        CHARACTER VARYING subject
        TEXT body
        CHARACTER VARYING source_ref
        UUID asset_id FK
        CHARACTER VARYING occurred_at
        CHARACTER VARYING inserted_at
        CHARACTER VARYING updated_at
        TEXT body_format
        JSONB body_tiptap
    }
    contract_reviews {
        UUID id PK
        UUID notation_id FK
        UUID playbook_id FK
        UUID asset_id FK
        CHARACTER VARYING status
        TEXT risk_summary
        JSONB findings
        CHARACTER VARYING inserted_at
        CHARACTER VARYING updated_at
    }
    coupons {
        UUID id PK
        CHARACTER VARYING code
        INTEGER discount_percent
        BIGINT discount_amount_cents
        CHARACTER VARYING product_code
        CHARACTER VARYING expires_at
        INTEGER max_redemptions
        INTEGER redeemed_count
        BOOLEAN active
        CHARACTER VARYING inserted_at
        CHARACTER VARYING updated_at
    }
    credentials {
        UUID id PK
        UUID person_id FK
        UUID jurisdiction_id FK
        CHARACTER VARYING license_number
        TEXT inserted_at
        TEXT updated_at
    }
    disclosures {
        UUID id PK
        UUID entity_id FK
        UUID project_id FK
        CHARACTER VARYING kind
        TEXT summary
        TEXT inserted_at
        TEXT updated_at
    }
    document_comments {
        UUID id PK
        UUID review_document_id FK
        UUID person_id FK
        INTEGER anchor_start
        INTEGER anchor_end
        TEXT quoted_text
        TEXT body
        BOOLEAN resolved
        CHARACTER VARYING inserted_at
        CHARACTER VARYING updated_at
        UUID communication_id FK
    }
    email_conversation_messages {
        UUID id PK
        UUID conversation_id FK
        CHARACTER VARYING direction
        CHARACTER VARYING from_addr
        CHARACTER VARYING to_addr
        CHARACTER VARYING subject
        TEXT body_text
        CHARACTER VARYING raw_storage_key
        CHARACTER VARYING provider_message_id
        CHARACTER VARYING in_reply_to
        TEXT command_payload
        CHARACTER VARYING inserted_at
        CHARACTER VARYING updated_at
    }
    email_conversations {
        UUID id PK
        CHARACTER VARYING token
        CHARACTER VARYING external_email
        CHARACTER VARYING external_name
        UUID person_id FK
        CHARACTER VARYING subject
        CHARACTER VARYING status
        UUID notation_id FK
        CHARACTER VARYING inserted_at
        CHARACTER VARYING updated_at
    }
    email_tokens {
        UUID id PK
        UUID person_id FK
        CHARACTER VARYING email
        CHARACTER VARYING purpose
        CHARACTER VARYING token_hash
        CHARACTER VARYING expires_at
        CHARACTER VARYING used_at
        CHARACTER VARYING inserted_at
        CHARACTER VARYING updated_at
    }
    entities {
        UUID id PK
        CHARACTER VARYING name
        UUID entity_type_id FK
        UUID jurisdiction_id FK
        TEXT inserted_at
        TEXT updated_at
        CHARACTER VARYING phone
        CHARACTER VARYING url
    }
    entity_billing_profiles {
        UUID id PK
        UUID entity_id FK
        CHARACTER VARYING billing_email
        UUID billing_address_id FK
        TEXT inserted_at
        TEXT updated_at
    }
    entity_types {
        UUID id PK
        CHARACTER VARYING name
        TEXT inserted_at
        TEXT updated_at
    }
    expunge_records {
        UUID id PK
        UUID project_id FK
        CHARACTER VARYING path
        CHARACTER VARYING category
        UUID authorized_by_person_id FK
        CHARACTER VARYING head_before
        CHARACTER VARYING head_after
        TEXT note
        CHARACTER VARYING inserted_at
        CHARACTER VARYING updated_at
    }
    expunge_requests {
        UUID id PK
        UUID project_id FK
        UUID asset_id FK
        UUID requested_by_person_id FK
        CHARACTER VARYING status
        TEXT note
        UUID resolved_by_person_id FK
        UUID expunge_record_id FK
        CHARACTER VARYING inserted_at
        CHARACTER VARYING updated_at
    }
    filings {
        UUID id PK
        UUID notation_id FK
        CHARACTER VARYING kind
        CHARACTER VARYING office
        CHARACTER VARYING reference
        TEXT summary
        CHARACTER VARYING submitted_at
        CHARACTER VARYING inserted_at
        CHARACTER VARYING updated_at
    }
    git_access_tokens {
        UUID id PK
        UUID person_id FK
        UUID project_id FK
        CHARACTER VARYING token_hash
        CHARACTER VARYING scope
        CHARACTER VARYING expires_at
        CHARACTER VARYING inserted_at
        CHARACTER VARYING updated_at
    }
    git_repositories {
        UUID id PK
        CHARACTER VARYING remote_hash
        CHARACTER VARYING last_commit_sha
        TEXT inserted_at
        TEXT updated_at
    }
    invoice_line_items {
        UUID id PK
        UUID invoice_id FK
        CHARACTER VARYING description
        INTEGER quantity
        BIGINT unit_price_cents
        TEXT inserted_at
        TEXT updated_at
    }
    invoices {
        UUID id PK
        UUID entity_billing_profile_id FK
        CHARACTER VARYING number
        CHARACTER VARYING status
        BIGINT total_cents
        CHARACTER VARYING currency
        TEXT inserted_at
        TEXT updated_at
    }
    jurisdictions {
        UUID id PK
        CHARACTER VARYING name
        CHARACTER VARYING code
        TEXT inserted_at
        TEXT updated_at
        CHARACTER VARYING jurisdiction_type
    }
    letters {
        UUID id PK
        UUID mailroom_id FK
        CHARACTER VARYING direction
        CHARACTER VARYING sender
        CHARACTER VARYING recipient
        TEXT summary
        TEXT inserted_at
        TEXT updated_at
    }
    mailrooms {
        UUID id PK
        CHARACTER VARYING name
        UUID address_id FK
        TEXT inserted_at
        TEXT updated_at
    }
    notarizations {
        UUID id PK
        UUID notation_id FK
        UUID notary_person_id FK
        UUID asset_id FK
        CHARACTER VARYING provider
        CHARACTER VARYING provider_id
        CHARACTER VARYING notarized_at
        CHARACTER VARYING inserted_at
        CHARACTER VARYING updated_at
    }
    notation_clauses {
        UUID id PK
        UUID notation_id FK
        INTEGER position
        TEXT body_markdown
        UUID authored_by_person_id FK
        CHARACTER VARYING inserted_at
        CHARACTER VARYING updated_at
    }
    notation_events {
        UUID id PK
        UUID notation_id FK
        CHARACTER VARYING machine_kind
        CHARACTER VARYING from_state
        CHARACTER VARYING to_state
        CHARACTER VARYING condition
        TEXT payload
        CHARACTER VARYING recorded_at
        TEXT inserted_at
        TEXT updated_at
        UUID acting_person_id FK
        UUID template_version_id FK
    }
    notations {
        UUID id PK
        UUID template_id FK
        UUID person_id FK
        UUID entity_id FK
        CHARACTER VARYING state
        UUID project_id FK
        TEXT inserted_at
        TEXT updated_at
        CHARACTER VARYING delivery
        INTEGER discount_pct
        BIGINT discount_amount_cents
        CHARACTER VARYING discount_reason
        CHARACTER VARYING discount_approved_by
        CHARACTER VARYING discount_approved_at
        JSONB questionnaire_snapshot
        TEXT git_commit_sha
    }
    person_entity_roles {
        UUID id PK
        UUID person_id FK
        UUID entity_id FK
        CHARACTER VARYING role
        TEXT inserted_at
        TEXT updated_at
    }
    person_project_roles {
        UUID id PK
        UUID person_id FK
        UUID project_id FK
        CHARACTER VARYING participation
        TEXT inserted_at
        TEXT updated_at
    }
    persons {
        UUID id PK
        CHARACTER VARYING name
        CHARACTER VARYING email
        CHARACTER VARYING oidc_subject
        TEXT inserted_at
        TEXT updated_at
        TEXT role
        CHARACTER VARYING preferred_language
        CHARACTER VARYING title
        CHARACTER VARYING phone
        CHARACTER VARYING xero_contact_id
        CHARACTER VARYING profile_image_url
        CHARACTER VARYING given_name
        CHARACTER VARYING family_name
        CHARACTER VARYING middle_name
        TEXT github_handle
    }
    playbooks {
        UUID id PK
        UUID entity_id FK
        CHARACTER VARYING name
        JSONB positions
        BOOLEAN active
        CHARACTER VARYING inserted_at
        CHARACTER VARYING updated_at
    }
    products {
        UUID id PK
        CHARACTER VARYING code
        CHARACTER VARYING display_name
        BIGINT list_price_cents
        CHARACTER VARYING currency
        CHARACTER VARYING cadence
        CHARACTER VARYING billing_kind
        BOOLEAN active
        CHARACTER VARYING xero_item_code
        CHARACTER VARYING matter_close_template_code
        CHARACTER VARYING inserted_at
        CHARACTER VARYING updated_at
        CHARACTER VARYING account_code
        CHARACTER VARYING retainer_template_code
    }
    projects {
        UUID id PK
        CHARACTER VARYING name
        CHARACTER VARYING status
        UUID entity_id FK
        TEXT inserted_at
        TEXT updated_at
        CHARACTER VARYING git_initialized_at
        CHARACTER VARYING closed_at
        TEXT description
        UUID staff_dri_person_id FK
        UUID client_dri_person_id FK
        CHARACTER VARYING code
        CHARACTER VARYING product_code FK
    }
    question_translations {
        UUID id PK
        UUID question_id FK
        CHARACTER VARYING locale
        TEXT prompt
        TEXT help_text
        CHARACTER VARYING inserted_at
        CHARACTER VARYING updated_at
    }
    questions {
        UUID id PK
        CHARACTER VARYING code
        TEXT prompt
        CHARACTER VARYING answer_type
        TEXT inserted_at
        TEXT updated_at
        CHARACTER VARYING audience
    }
    relationship_edges {
        UUID id PK
        CHARACTER VARYING from_type
        UUID from_id
        CHARACTER VARYING to_type
        UUID to_id
        CHARACTER VARYING kind
        INTEGER confidence_pct
        CHARACTER VARYING source_kind
        UUID source_id
        TEXT detail
        CHARACTER VARYING inserted_at
        CHARACTER VARYING updated_at
    }
    relationship_logs {
        UUID id PK
        UUID actor_person_id FK
        CHARACTER VARYING subject_type
        UUID subject_id
        CHARACTER VARYING action
        TEXT detail
        TEXT inserted_at
        TEXT updated_at
    }
    review_documents {
        UUID id PK
        UUID notation_id FK
        CHARACTER VARYING kind
        CHARACTER VARYING title
        TEXT body_html
        CHARACTER VARYING status
        CHARACTER VARYING inserted_at
        CHARACTER VARYING updated_at
    }
    sent_emails {
        UUID id PK
        CHARACTER VARYING recipient
        CHARACTER VARYING subject
        CHARACTER VARYING sender
        CHARACTER VARYING template_slug
        TEXT body
        CHARACTER VARYING outcome
        CHARACTER VARYING sent_at
        TEXT inserted_at
        TEXT updated_at
        CHARACTER VARYING sg_message_id
    }
    share_issuances {
        UUID id PK
        UUID entity_id FK
        CHARACTER VARYING holder_name
        CHARACTER VARYING share_class
        BIGINT shares
        CHARACTER VARYING issued_at
        TEXT inserted_at
        TEXT updated_at
    }
    signatures {
        UUID id PK
        UUID notation_id FK
        UUID signer_person_id FK
        CHARACTER VARYING field
        CHARACTER VARYING provider
        CHARACTER VARYING provider_id
        CHARACTER VARYING signed_at
        CHARACTER VARYING inserted_at
        CHARACTER VARYING updated_at
    }
    statute_revisions {
        UUID id PK
        UUID statute_id FK
        TEXT body
        CHARACTER VARYING body_sha256
        CHARACTER VARYING section_title
        CHARACTER VARYING history_note
        CHARACTER VARYING observed_at
        CHARACTER VARYING inserted_at
        CHARACTER VARYING updated_at
    }
    statutes {
        UUID id PK
        CHARACTER VARYING jurisdiction
        CHARACTER VARYING code
        CHARACTER VARYING chapter
        CHARACTER VARYING chapter_title
        CHARACTER VARYING section
        CHARACTER VARYING source_url
        CHARACTER VARYING status
        CHARACTER VARYING first_seen_at
        CHARACTER VARYING last_checked_at
        CHARACTER VARYING last_changed_at
        CHARACTER VARYING inserted_at
        CHARACTER VARYING updated_at
    }
    statutory_deadlines {
        UUID id PK
        UUID project_id FK
        CHARACTER VARYING kind
        DATE trigger_on
        DATE due_on
        CHARACTER VARYING statute
        CHARACTER VARYING source
        CHARACTER VARYING status
        CHARACTER VARYING inserted_at
        CHARACTER VARYING updated_at
    }
    subscriptions {
        UUID id PK
        UUID person_id
        UUID entity_id
        UUID project_id
        CHARACTER VARYING product_code
        CHARACTER VARYING contact_name
        CHARACTER VARYING contact_email
        CHARACTER VARYING status
        CHARACTER VARYING started_at
        CHARACTER VARYING last_invoiced_period
        INTEGER discount_percent
        BIGINT discount_amount_cents
        CHARACTER VARYING inserted_at
        CHARACTER VARYING updated_at
    }
    templates {
        UUID id PK
        CHARACTER VARYING code
        CHARACTER VARYING title
        CHARACTER VARYING respondent_type
        TEXT inserted_at
        TEXT updated_at
        UUID project_id FK
        UUID asset_id FK
        CHARACTER VARYING form_code
        BOOLEAN is_current
        TEXT source_commit_sha
        CHARACTER VARYING kind
    }
    testimonials {
        UUID id PK
        UUID project_id FK
        UUID person_id FK
        CHARACTER VARYING product_code FK
        TEXT quote
        CHARACTER VARYING attribution_label
        CHARACTER VARYING consented_at
        CHARACTER VARYING published_at
        INTEGER display_order
        CHARACTER VARYING inserted_at
        CHARACTER VARYING updated_at
    }
    visitor_route_counts {
        UUID id PK
        DATE bucket_date
        CHARACTER VARYING country_code
        CHARACTER VARYING route_pattern
        CHARACTER VARYING source
        CHARACTER VARYING locale
        CHARACTER VARYING status_class
        BIGINT visits
        CHARACTER VARYING inserted_at
        CHARACTER VARYING updated_at
    }
    xero_invoices {
        UUID id PK
        UUID project_id FK
        CHARACTER VARYING xero_invoice_id
        CHARACTER VARYING reference
        CHARACTER VARYING status
        BIGINT amount_cents
        BIGINT amount_paid_cents
        CHARACTER VARYING currency
        CHARACTER VARYING inserted_at
        CHARACTER VARYING updated_at
    }
    persons ||--o{ addresses : "person_id"
    entities ||--o{ addresses : "entity_id"
    questions ||--o{ answers : "question_id"
    persons ||--o{ answers : "person_id"
    persons ||--o{ answers : "authored_by_person_id"
    notations ||--o{ answers : "notation_id"
    projects ||--o{ assets : "project_id"
    notations ||--o{ attestations : "notation_id"
    projects ||--o{ case_reports : "project_id"
    assets ||--o{ case_reports : "current_asset_id"
    persons ||--o{ case_reports : "created_by_person_id"
    projects ||--o{ communications : "project_id"
    persons ||--o{ communications : "author_person_id"
    assets ||--o{ communications : "asset_id"
    notations ||--o{ contract_reviews : "notation_id"
    playbooks ||--o{ contract_reviews : "playbook_id"
    assets ||--o{ contract_reviews : "asset_id"
    persons ||--o{ credentials : "person_id"
    jurisdictions ||--o{ credentials : "jurisdiction_id"
    entities ||--o{ disclosures : "entity_id"
    projects ||--o{ disclosures : "project_id"
    review_documents ||--o{ document_comments : "review_document_id"
    persons ||--o{ document_comments : "person_id"
    communications ||--o{ document_comments : "communication_id"
    email_conversations ||--o{ email_conversation_messages : "conversation_id"
    persons ||--o{ email_conversations : "person_id"
    notations ||--o{ email_conversations : "notation_id"
    persons ||--o{ email_tokens : "person_id"
    entity_types ||--o{ entities : "entity_type_id"
    jurisdictions ||--o{ entities : "jurisdiction_id"
    entities ||--o{ entity_billing_profiles : "entity_id"
    addresses ||--o{ entity_billing_profiles : "billing_address_id"
    projects ||--o{ expunge_records : "project_id"
    persons ||--o{ expunge_records : "authorized_by_person_id"
    projects ||--o{ expunge_requests : "project_id"
    assets ||--o{ expunge_requests : "asset_id"
    persons ||--o{ expunge_requests : "requested_by_person_id"
    persons ||--o{ expunge_requests : "resolved_by_person_id"
    expunge_records ||--o{ expunge_requests : "expunge_record_id"
    notations ||--o{ filings : "notation_id"
    persons ||--o{ git_access_tokens : "person_id"
    projects ||--o{ git_access_tokens : "project_id"
    invoices ||--o{ invoice_line_items : "invoice_id"
    entity_billing_profiles ||--o{ invoices : "entity_billing_profile_id"
    mailrooms ||--o{ letters : "mailroom_id"
    addresses ||--o{ mailrooms : "address_id"
    notations ||--o{ notarizations : "notation_id"
    persons ||--o{ notarizations : "notary_person_id"
    assets ||--o{ notarizations : "asset_id"
    notations ||--o{ notation_clauses : "notation_id"
    persons ||--o{ notation_clauses : "authored_by_person_id"
    notations ||--o{ notation_events : "notation_id"
    persons ||--o{ notation_events : "acting_person_id"
    templates ||--o{ notation_events : "template_version_id"
    templates ||--o{ notations : "template_id"
    persons ||--o{ notations : "person_id"
    entities ||--o{ notations : "entity_id"
    projects ||--o{ notations : "project_id"
    persons ||--o{ person_entity_roles : "person_id"
    entities ||--o{ person_entity_roles : "entity_id"
    persons ||--o{ person_project_roles : "person_id"
    projects ||--o{ person_project_roles : "project_id"
    entities ||--o{ playbooks : "entity_id"
    entities ||--o{ projects : "entity_id"
    persons ||--o{ projects : "staff_dri_person_id"
    persons ||--o{ projects : "client_dri_person_id"
    products ||--o{ projects : "product_code"
    questions ||--o{ question_translations : "question_id"
    persons ||--o{ relationship_logs : "actor_person_id"
    notations ||--o{ review_documents : "notation_id"
    entities ||--o{ share_issuances : "entity_id"
    notations ||--o{ signatures : "notation_id"
    persons ||--o{ signatures : "signer_person_id"
    statutes ||--o{ statute_revisions : "statute_id"
    projects ||--o{ statutory_deadlines : "project_id"
    projects ||--o{ templates : "project_id"
    assets ||--o{ templates : "asset_id"
    projects ||--o{ testimonials : "project_id"
    persons ||--o{ testimonials : "person_id"
    products ||--o{ testimonials : "product_code"
    projects ||--o{ xero_invoices : "project_id"