Entity-relationship diagram
The workspace ships two ERD artifacts, both rendered from the same live pg_catalog introspection in cli/src/erd.rs:
docs/erd.md(this file) — the MermaiderDiagramblock under Schema. GitHub renders Mermaid natively, so this is the "view in the repo" artifact.docs/erd.svg— a standalone SVG fromnavigator docs erd --format svg. The renderer is deterministic by construction (alphabeticalBTreeMapiteration, integer-only arithmetic, no timestamps, no random IDs): same schema in → byte-identical SVG out. Use it anywhere Mermaid won't render (slides, design docs, screenshots, links shared outside the repo). Unlike Mermaid's own SVG (text in<foreignObject>, invisible in many viewers), this renderer uses native<text>elements, so it opens in GNOME Image Viewer,feh, browsers — anything.
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
--format mermaid(default) — the GitHub-renderableerDiagramblock on stdout.--format svg— a standalone SVG document on stdout.
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"