Database Schema Reference
Generated from migration files in
migrations/postgres/andmigrations/sqlite/. Migrations V1–V32. Last updated: 2026-04-02.
SQLite vs PostgreSQL Differences
| Aspect | PostgreSQL | SQLite |
|---|---|---|
| Primary keys | UUID DEFAULT gen_random_uuid() | TEXT (app-generated UUIDs) |
| Timestamps | TIMESTAMPTZ DEFAULT now() | TEXT DEFAULT (datetime('now')) |
| Booleans | BOOLEAN (TRUE/FALSE) | INTEGER (1/0) |
| JSON columns | JSONB | TEXT (JSON stored as string) |
| Large integers | BIGINT | INTEGER |
| Multi-tenancy | Row Level Security (RLS) policies | Explicit AND org_id = ? in queries |
| UUID function | gen_random_uuid() | App-generated; migrations use hex(randomblob(...)) |
| Tenant context | current_setting('app.tenant_id')::uuid | Passed as query parameter |
Tables
schema_info
Tracks the Alloy version embedded in the database. Created in V1.
| Column | PG Type | SQLite Type | Constraints |
|---|---|---|---|
| key | TEXT | TEXT | PK |
| value | TEXT | TEXT | NOT NULL |
| created_at | TIMESTAMPTZ | TEXT | NOT NULL, DEFAULT now |
organizations
Top-level tenant entity. Created in V2.
| Column | PG Type | SQLite Type | Constraints |
|---|---|---|---|
| id | UUID | TEXT | PK, DEFAULT gen_random_uuid() (PG) |
| name | TEXT | TEXT | NOT NULL |
| slug | TEXT | TEXT | NOT NULL, UNIQUE |
| created_at | TIMESTAMPTZ | TEXT | NOT NULL, DEFAULT now |
| updated_at | TIMESTAMPTZ | TEXT | NOT NULL, DEFAULT now |
RLS (PG): id = current_tenant_id() (V9)
users
Application users. Created in V3, amended in V22.
| Column | PG Type | SQLite Type | Constraints | Migration |
|---|---|---|---|---|
| id | UUID | TEXT | PK, DEFAULT gen_random_uuid() (PG) | V3 |
| TEXT | TEXT | NOT NULL, UNIQUE | V3 | |
| display_name | TEXT | TEXT | NOT NULL | V3 |
| password_hash | TEXT | TEXT | Nullable | V3 |
| active | BOOLEAN | INTEGER | NOT NULL, DEFAULT TRUE/1 | V22 |
| created_at | TIMESTAMPTZ | TEXT | NOT NULL, DEFAULT now | V3 |
| updated_at | TIMESTAMPTZ | TEXT | NOT NULL, DEFAULT now | V3 |
Note: Users are not tenant-scoped — a user can belong to multiple organizations via org_memberships.
org_memberships
Links users to organizations with a role. Created in V4.
| Column | PG Type | SQLite Type | Constraints |
|---|---|---|---|
| user_id | UUID | TEXT | PK (composite), FK → users(id) |
| org_id | UUID | TEXT | PK (composite), FK → organizations(id) |
| role | TEXT | TEXT | NOT NULL |
| joined_at | TIMESTAMPTZ | TEXT | NOT NULL, DEFAULT now |
RLS (PG): org_id = current_tenant_id() (V9)
teams
Groups of users within an organization. Created in V5.
| Column | PG Type | SQLite Type | Constraints |
|---|---|---|---|
| id | UUID | TEXT | PK, DEFAULT gen_random_uuid() (PG) |
| org_id | UUID | TEXT | NOT NULL, FK → organizations(id) |
| name | TEXT | TEXT | NOT NULL |
| description | TEXT | TEXT | Nullable |
| created_at | TIMESTAMPTZ | TEXT | NOT NULL, DEFAULT now |
RLS (PG): org_id = current_tenant_id() (V9)
team_memberships
Links users to teams. Created in V5.
| Column | PG Type | SQLite Type | Constraints |
|---|---|---|---|
| user_id | UUID | TEXT | PK (composite), FK → users(id) |
| team_id | UUID | TEXT | PK (composite), FK → teams(id) |
| joined_at | TIMESTAMPTZ | TEXT | NOT NULL, DEFAULT now |
projects
A project within an organization, optionally assigned to a team. Created in V6, amended in V14 (workflow_id), V17 (capitalization fields), V27 (budget fields).
| Column | PG Type | SQLite Type | Constraints | Migration |
|---|---|---|---|---|
| id | UUID | TEXT | PK, DEFAULT gen_random_uuid() (PG) | V6 |
| org_id | UUID | TEXT | NOT NULL, FK → organizations(id) | V6 |
| team_id | UUID | TEXT | Nullable, FK → teams(id) | V6 |
| key | TEXT | TEXT | NOT NULL, UNIQUE(org_id, key) | V6 |
| name | TEXT | TEXT | NOT NULL | V6 |
| description | TEXT | TEXT | Nullable | V6 |
| ticket_counter | INTEGER | INTEGER | NOT NULL, DEFAULT 0 | V6 |
| workflow_id | UUID | TEXT | Nullable, FK → workflows(id) | V14 |
| capitalization_type | TEXT | TEXT | Nullable | V17 |
| development_phase | TEXT | TEXT | Nullable | V17 |
| cost_center_id | TEXT | TEXT | Nullable | V17 |
| amortization_months | INTEGER | INTEGER | Nullable | V17 |
| budget_cents | BIGINT | INTEGER | Nullable | V27 |
| budget_period | TEXT | TEXT | Nullable | V27 |
| created_at | TIMESTAMPTZ | TEXT | NOT NULL, DEFAULT now | V6 |
| updated_at | TIMESTAMPTZ | TEXT | NOT NULL, DEFAULT now | V6 |
RLS (PG): org_id = current_tenant_id() (V9)
tickets
Work items within a project. Created in V7, amended in V15 (sprint_id), V31 (indexes).
| Column | PG Type | SQLite Type | Constraints | Migration |
|---|---|---|---|---|
| id | UUID | TEXT | PK, DEFAULT gen_random_uuid() (PG) | V7 |
| project_id | UUID | TEXT | NOT NULL, FK → projects(id) | V7 |
| ticket_number | INTEGER | INTEGER | NOT NULL, UNIQUE(project_id, ticket_number) | V7 |
| title | TEXT | TEXT | NOT NULL | V7 |
| description | TEXT | TEXT | Nullable | V7 |
| status | TEXT | TEXT | NOT NULL, DEFAULT ‘Backlog’ | V7 |
| priority | TEXT | TEXT | NOT NULL, DEFAULT ‘None’ | V7 |
| assignee_id | UUID | TEXT | Nullable, FK → users(id) | V7 |
| reporter_id | UUID | TEXT | NOT NULL, FK → users(id) | V7 |
| sprint_id | UUID | TEXT | Nullable, FK → sprints(id) ON DELETE SET NULL | V15 |
| created_at | TIMESTAMPTZ | TEXT | NOT NULL, DEFAULT now | V7 |
| updated_at | TIMESTAMPTZ | TEXT | NOT NULL, DEFAULT now | V7 |
Indexes: idx_tickets_project_status(project_id, status), idx_tickets_assignee_id(assignee_id), idx_tickets_sprint_id(sprint_id) (V31)
RLS (PG): project_id IN (SELECT id FROM projects WHERE org_id = current_tenant_id()) (V9)
api_keys
API key credentials for programmatic access. Created in V8, amended in V25 (project_ids).
| Column | PG Type | SQLite Type | Constraints | Migration |
|---|---|---|---|---|
| id | UUID | TEXT | PK, DEFAULT gen_random_uuid() (PG) | V8 |
| org_id | UUID | TEXT | NOT NULL, FK → organizations(id) | V8 |
| user_id | UUID | TEXT | NOT NULL, FK → users(id) | V8 |
| name | TEXT | TEXT | NOT NULL | V8 |
| key_prefix | TEXT | TEXT | NOT NULL | V8 |
| key_hash | TEXT | TEXT | NOT NULL, UNIQUE | V8 |
| scopes | TEXT | TEXT | NOT NULL, DEFAULT ‘*’ | V8 |
| project_ids | TEXT | TEXT | NOT NULL, DEFAULT ‘’ | V25 |
| created_at | TIMESTAMPTZ | TEXT | NOT NULL, DEFAULT now | V8 |
| last_used_at | TIMESTAMPTZ | TEXT | Nullable | V8 |
| expires_at | TIMESTAMPTZ | TEXT | Nullable | V8 |
Indexes: idx_api_keys_key_hash(key_hash) UNIQUE
comments
Threaded comments on tickets. Created in V10.
| Column | PG Type | SQLite Type | Constraints |
|---|---|---|---|
| id | UUID | TEXT | PK, DEFAULT gen_random_uuid() (PG) |
| ticket_id | UUID | TEXT | NOT NULL, FK → tickets(id) ON DELETE CASCADE |
| author_id | UUID | TEXT | NOT NULL, FK → users(id) |
| body | TEXT | TEXT | NOT NULL |
| parent_comment_id | UUID | TEXT | Nullable, FK → comments(id) ON DELETE CASCADE |
| created_at | TIMESTAMPTZ | TEXT | NOT NULL, DEFAULT now |
| updated_at | TIMESTAMPTZ | TEXT | NOT NULL, DEFAULT now |
Indexes: idx_comments_ticket_id(ticket_id), idx_comments_author_id(author_id), idx_comments_parent_id(parent_comment_id)
audit_logs
Immutable log of all mutations. Created in V11.
| Column | PG Type | SQLite Type | Constraints |
|---|---|---|---|
| id | UUID | TEXT | PK, DEFAULT gen_random_uuid() (PG) |
| entity_type | TEXT | TEXT | NOT NULL |
| entity_id | TEXT | TEXT | NOT NULL |
| action | TEXT | TEXT | NOT NULL |
| actor_id | UUID | TEXT | NOT NULL |
| changes | TEXT | TEXT | NOT NULL, DEFAULT ‘[]’ |
| created_at | TIMESTAMPTZ | TEXT | NOT NULL, DEFAULT now |
Indexes: idx_audit_logs_entity(entity_type, entity_id), idx_audit_logs_actor(actor_id), idx_audit_logs_created(created_at)
attachments
File attachments on tickets. Created in V12.
| Column | PG Type | SQLite Type | Constraints |
|---|---|---|---|
| id | UUID | TEXT | PK, DEFAULT gen_random_uuid() (PG) |
| ticket_id | UUID | TEXT | NOT NULL, FK → tickets(id) ON DELETE CASCADE |
| filename | TEXT | TEXT | NOT NULL |
| content_type | TEXT | TEXT | NOT NULL |
| size_bytes | BIGINT | INTEGER | NOT NULL |
| s3_key | TEXT | TEXT | NOT NULL |
| uploaded_by | UUID | TEXT | NOT NULL, FK → users(id) |
| created_at | TIMESTAMPTZ | TEXT | NOT NULL, DEFAULT now |
Indexes: idx_attachments_ticket_id(ticket_id), idx_attachments_uploaded_by(uploaded_by)
RLS (PG): Via ticket → project → org join (V12)
labels
Organization-scoped labels for categorizing tickets. Created in V13.
| Column | PG Type | SQLite Type | Constraints |
|---|---|---|---|
| id | UUID | TEXT | PK, DEFAULT gen_random_uuid() (PG) |
| org_id | UUID | TEXT | NOT NULL, FK → organizations(id) ON DELETE CASCADE |
| name | TEXT | TEXT | NOT NULL |
| color | TEXT | TEXT | NOT NULL |
| created_at | TIMESTAMPTZ | TEXT | NOT NULL, DEFAULT now |
| updated_at | TIMESTAMPTZ | TEXT | NOT NULL, DEFAULT now |
Indexes: idx_labels_org_id(org_id), idx_labels_org_name(org_id, name) UNIQUE
RLS (PG): org_id = current_setting('app.tenant_id')::uuid (V13)
ticket_labels
Many-to-many join between tickets and labels. Created in V13.
| Column | PG Type | SQLite Type | Constraints |
|---|---|---|---|
| ticket_id | UUID | TEXT | PK (composite), FK → tickets(id) ON DELETE CASCADE |
| label_id | UUID | TEXT | PK (composite), FK → labels(id) ON DELETE CASCADE |
Indexes: idx_ticket_labels_label_id(label_id)
RLS (PG): Via label → org join (V13)
workflows
Configurable status workflows per organization. Created in V14, amended in V28 (seed data), V29 (enforcement).
| Column | PG Type | SQLite Type | Constraints | Migration |
|---|---|---|---|---|
| id | UUID | TEXT | PK, DEFAULT gen_random_uuid() (PG) | V14 |
| org_id | UUID | TEXT | NOT NULL, FK → organizations(id) | V14 |
| name | TEXT | TEXT | NOT NULL, UNIQUE(org_id, name) | V14 |
| statuses | JSONB | TEXT | NOT NULL, DEFAULT ‘[]’ | V14 |
| transitions | JSONB | TEXT | NOT NULL, DEFAULT ‘[]’ | V14 |
| enforcement | TEXT | TEXT | NOT NULL, DEFAULT ‘none’ | V29 |
| created_at | TIMESTAMPTZ | TEXT | NOT NULL | V14 |
| updated_at | TIMESTAMPTZ | TEXT | NOT NULL | V14 |
RLS (PG): org_id = current_setting('app.tenant_id')::uuid (V14)
Seed data (V28): A “Default” workflow with 6 statuses (Backlog, Todo, InProgress, InReview, Done, Cancelled) and all-to-all transitions is created for every existing organization.
sprints
Time-boxed iterations within a project. Created in V15.
| Column | PG Type | SQLite Type | Constraints |
|---|---|---|---|
| id | UUID | TEXT | PK |
| project_id | UUID | TEXT | NOT NULL, FK → projects(id) ON DELETE CASCADE |
| name | TEXT | TEXT | NOT NULL |
| goal | TEXT | TEXT | Nullable |
| start_date | TEXT | TEXT | NOT NULL |
| end_date | TEXT | TEXT | NOT NULL |
| status | TEXT | TEXT | NOT NULL, DEFAULT ‘Planned’ |
| created_at | TIMESTAMPTZ | TEXT | NOT NULL, DEFAULT now |
| updated_at | TIMESTAMPTZ | TEXT | NOT NULL, DEFAULT now |
Indexes: idx_sprints_project_id(project_id)
RLS (PG): Via project → org join (V15)
time_entries
Logged time against tickets for finance/capitalization. Created in V16, amended in V32 (indexes).
| Column | PG Type | SQLite Type | Constraints |
|---|---|---|---|
| id | UUID | TEXT | PK |
| user_id | UUID | TEXT | NOT NULL, FK → users(id) |
| ticket_id | UUID | TEXT | NOT NULL, FK → tickets(id) ON DELETE CASCADE |
| project_id | UUID | TEXT | NOT NULL, FK → projects(id) ON DELETE CASCADE |
| date | TEXT | TEXT | NOT NULL |
| duration_minutes | INTEGER | INTEGER | NOT NULL |
| description | TEXT | TEXT | Nullable |
| activity_type | TEXT | TEXT | NOT NULL |
| status | TEXT | TEXT | NOT NULL, DEFAULT ‘Draft’ |
| approved_by | UUID | TEXT | Nullable, FK → users(id) |
| approved_at | TIMESTAMPTZ | TEXT | Nullable |
| created_at | TIMESTAMPTZ | TEXT | NOT NULL, DEFAULT now |
| updated_at | TIMESTAMPTZ | TEXT | NOT NULL, DEFAULT now |
Indexes: idx_time_entries_user_id(user_id), idx_time_entries_ticket_id(ticket_id), idx_time_entries_project_id(project_id), idx_time_entries_date(date), idx_time_entries_user_date(user_id, date) (V32), idx_time_entries_project_date(project_id, date) (V32)
RLS (PG): Via project → org join (V16)
user_labor_rates
Per-user billing rates for capitalization calculations. Created in V18.
| Column | PG Type | SQLite Type | Constraints |
|---|---|---|---|
| id | UUID | TEXT | PK |
| user_id | UUID | TEXT | NOT NULL, FK → users(id) |
| org_id | UUID | TEXT | NOT NULL, FK → organizations(id) |
| loaded_rate_cents | INTEGER | INTEGER | NOT NULL |
| effective_date | TEXT | TEXT | NOT NULL |
| created_at | TIMESTAMPTZ | TEXT | NOT NULL, DEFAULT now |
| updated_at | TIMESTAMPTZ | TEXT | NOT NULL, DEFAULT now |
Indexes: idx_user_labor_rates_user_id(user_id), idx_user_labor_rates_org_id(org_id), idx_user_labor_rates_effective_date(effective_date), idx_user_labor_rates_user_org_date(user_id, org_id, effective_date) UNIQUE
RLS (PG): Via org join (V18)
webhooks
Outbound webhook configurations. Created in V19.
| Column | PG Type | SQLite Type | Constraints |
|---|---|---|---|
| id | UUID | TEXT | PK |
| org_id | UUID | TEXT | NOT NULL, FK → organizations(id) |
| url | TEXT | TEXT | NOT NULL |
| secret | TEXT | TEXT | NOT NULL |
| event_types | TEXT | TEXT | NOT NULL |
| active | BOOLEAN | INTEGER | NOT NULL, DEFAULT TRUE/1 |
| created_at | TIMESTAMPTZ | TEXT | NOT NULL, DEFAULT now |
| updated_at | TIMESTAMPTZ | TEXT | NOT NULL, DEFAULT now |
Indexes: idx_webhooks_org_id(org_id), idx_webhooks_active(active)
RLS (PG): Via org join (V19)
webhook_deliveries
Delivery attempts for webhooks. Created in V19.
| Column | PG Type | SQLite Type | Constraints |
|---|---|---|---|
| id | UUID | TEXT | PK |
| webhook_id | UUID | TEXT | NOT NULL, FK → webhooks(id) ON DELETE CASCADE |
| event_type | TEXT | TEXT | NOT NULL |
| payload | TEXT | TEXT | NOT NULL |
| status | TEXT | TEXT | NOT NULL, DEFAULT ‘pending’ |
| response_status | INTEGER | INTEGER | Nullable |
| response_body | TEXT | TEXT | Nullable |
| attempt | INTEGER | INTEGER | NOT NULL, DEFAULT 0 |
| next_retry_at | TIMESTAMPTZ | TEXT | Nullable |
| created_at | TIMESTAMPTZ | TEXT | NOT NULL, DEFAULT now |
| updated_at | TIMESTAMPTZ | TEXT | NOT NULL, DEFAULT now |
Indexes: idx_webhook_deliveries_webhook_id(webhook_id), idx_webhook_deliveries_status(status)
slack_thread_mappings
Maps Slack threads to Alloy tickets. Created in V20.
| Column | PG Type | SQLite Type | Constraints |
|---|---|---|---|
| id | TEXT | TEXT | PK |
| tenant_id | TEXT | — | NOT NULL, DEFAULT current_setting(‘app.tenant_id’) (PG only) |
| ticket_id | TEXT | TEXT | NOT NULL, UNIQUE(ticket_id, channel_id) |
| channel_id | TEXT | TEXT | NOT NULL |
| thread_ts | TEXT | TEXT | NOT NULL |
| created_at | TIMESTAMPTZ | TEXT | NOT NULL, DEFAULT now |
RLS (PG): tenant_id = current_setting('app.tenant_id') (V20)
Note: The tenant_id column exists only in PostgreSQL. SQLite is single-tenant.
identity_providers
SSO/OIDC configuration per organization. Created in V21.
| Column | PG Type | SQLite Type | Constraints |
|---|---|---|---|
| id | UUID | TEXT | PK, DEFAULT gen_random_uuid() (PG) |
| org_id | UUID | TEXT | NOT NULL, UNIQUE, FK → organizations(id) |
| provider_type | TEXT | TEXT | NOT NULL, DEFAULT ‘oidc’ |
| provider_name | TEXT | TEXT | NOT NULL, DEFAULT ‘okta’ |
| issuer_url | TEXT | TEXT | NOT NULL |
| client_id | TEXT | TEXT | NOT NULL |
| client_secret | TEXT | TEXT | NOT NULL |
| authorization_endpoint | TEXT | TEXT | Nullable |
| token_endpoint | TEXT | TEXT | Nullable |
| jwks_uri | TEXT | TEXT | Nullable |
| created_at | TIMESTAMPTZ | TEXT | NOT NULL, DEFAULT now |
| updated_at | TIMESTAMPTZ | TEXT | NOT NULL, DEFAULT now |
RLS (PG): org_id = current_setting('app.tenant_id')::UUID (V21)
invites
Organization membership invitations. Created in V23.
| Column | PG Type | SQLite Type | Constraints |
|---|---|---|---|
| id | UUID | TEXT | PK, DEFAULT gen_random_uuid() (PG) |
| org_id | UUID | TEXT | NOT NULL, FK → organizations(id) |
| TEXT | TEXT | Nullable | |
| invite_code | TEXT | TEXT | NOT NULL, UNIQUE |
| role | TEXT | TEXT | NOT NULL, DEFAULT ‘Member’ |
| created_by | UUID | TEXT | NOT NULL, FK → users(id) |
| expires_at | TIMESTAMPTZ | TEXT | NOT NULL |
| accepted_at | TIMESTAMPTZ | TEXT | Nullable |
| revoked_at | TIMESTAMPTZ | TEXT | Nullable |
| created_at | TIMESTAMPTZ | TEXT | NOT NULL, DEFAULT now |
RLS (PG): org_id::text = current_setting('app.tenant_id', true) (V23)
refresh_tokens
JWT refresh token tracking. Created in V24.
| Column | PG Type | SQLite Type | Constraints |
|---|---|---|---|
| id | UUID | TEXT | PK, DEFAULT gen_random_uuid() (PG) |
| user_id | UUID | TEXT | NOT NULL, FK → users(id) |
| token_hash | TEXT | TEXT | NOT NULL, UNIQUE |
| expires_at | TIMESTAMPTZ | TEXT | NOT NULL |
| revoked_at | TIMESTAMPTZ | TEXT | Nullable |
| created_at | TIMESTAMPTZ | TEXT | NOT NULL, DEFAULT now |
Indexes: idx_refresh_tokens_user_id(user_id), idx_refresh_tokens_token_hash(token_hash)
entity_tags
Key-value tags on any entity (projects, tickets, etc.). Created in V26.
| Column | PG Type | SQLite Type | Constraints |
|---|---|---|---|
| id | UUID | TEXT | PK, DEFAULT gen_random_uuid() (PG) |
| org_id | UUID | TEXT | NOT NULL, FK → organizations(id) ON DELETE CASCADE |
| entity_type | TEXT | TEXT | NOT NULL |
| entity_id | TEXT | TEXT | NOT NULL |
| tag_key | TEXT | TEXT | NOT NULL |
| tag_value | TEXT | TEXT | NOT NULL |
| created_at | TIMESTAMPTZ | TEXT | NOT NULL, DEFAULT now |
| updated_at | TIMESTAMPTZ | TEXT | NOT NULL, DEFAULT now |
Indexes: idx_entity_tags_org_id(org_id), idx_entity_tags_entity(org_id, entity_type, entity_id), idx_entity_tags_unique(org_id, entity_type, entity_id, tag_key) UNIQUE, idx_entity_tags_search(org_id, tag_key, tag_value)
RLS (PG): org_id = current_setting('app.tenant_id')::uuid (V26)
project_memberships
Links users to projects they can access. Created in V30.
| Column | PG Type | SQLite Type | Constraints |
|---|---|---|---|
| project_id | UUID | TEXT | PK (composite), FK → projects(id) ON DELETE CASCADE |
| user_id | UUID | TEXT | PK (composite), FK → users(id) ON DELETE CASCADE |
| created_at | TIMESTAMPTZ | TEXT | NOT NULL, DEFAULT now |
RLS (PG): Via project → org join (V30)
RLS Summary (PostgreSQL Only)
V9 creates the current_tenant_id() helper function and enables RLS on the core tables. Subsequent migrations (V12–V30) enable RLS on each new table as it’s created.
| Table | RLS Policy | Isolation Path |
|---|---|---|
| organizations | id = current_tenant_id() | Direct |
| projects | org_id = current_tenant_id() | Direct |
| tickets | Via project → org | Indirect |
| teams | org_id = current_tenant_id() | Direct |
| org_memberships | org_id = current_tenant_id() | Direct |
| attachments | Via ticket → project → org | Indirect |
| labels | org_id = current_setting(...)::uuid | Direct |
| ticket_labels | Via label → org | Indirect |
| workflows | org_id = current_setting(...)::uuid | Direct |
| sprints | Via project → org | Indirect |
| time_entries | Via project → org | Indirect |
| user_labor_rates | Via org | Indirect |
| webhooks | Via org | Indirect |
| webhook_deliveries | (Via webhook — no direct RLS) | N/A |
| slack_thread_mappings | tenant_id = current_setting(...) | Direct (own column) |
| identity_providers | org_id = current_setting(...)::UUID | Direct |
| invites | org_id::text = current_setting(...) | Direct |
| entity_tags | org_id = current_setting(...)::uuid | Direct |
| project_memberships | Via project → org | Indirect |
V9 also creates the app_user role with SELECT/INSERT/UPDATE/DELETE on all tables and grants default privileges for future tables.
Entity Relationship Overview
organizations ─┬── org_memberships ── users
├── teams ── team_memberships ── users
├── projects ─┬── tickets ─┬── comments
│ │ ├── attachments
│ │ ├── ticket_labels ── labels
│ │ └── time_entries
│ ├── sprints (tickets.sprint_id)
│ └── project_memberships ── users
├── workflows (projects.workflow_id)
├── labels
├── webhooks ── webhook_deliveries
├── identity_providers
├── invites
├── user_labor_rates
├── entity_tags
└── slack_thread_mappings (via tickets)
users ─┬── api_keys
└── refresh_tokens
audit_logs (standalone, references entities by type+id)
schema_info (standalone metadata)
Migration History
| Version | Description |
|---|---|
| V1 | Create schema_info |
| V2 | Create organizations |
| V3 | Create users |
| V4 | Create org_memberships |
| V5 | Create teams + team_memberships |
| V6 | Create projects |
| V7 | Create tickets |
| V8 | Create api_keys |
| V9 | Enable RLS (PG); no-op (SQLite) |
| V10 | Create comments |
| V11 | Create audit_logs |
| V12 | Create attachments |
| V13 | Create labels + ticket_labels |
| V14 | Create workflows; add projects.workflow_id |
| V15 | Create sprints; add tickets.sprint_id |
| V16 | Create time_entries |
| V17 | Add project capitalization fields |
| V18 | Create user_labor_rates |
| V19 | Create webhooks + webhook_deliveries |
| V20 | Create slack_thread_mappings |
| V21 | Create identity_providers |
| V22 | Add users.active |
| V23 | Create invites |
| V24 | Create refresh_tokens |
| V25 | Add api_keys.project_ids |
| V26 | Create entity_tags |
| V27 | Add project budget fields |
| V28 | Seed default workflows |
| V29 | Add workflows.enforcement |
| V30 | Create project_memberships |
| V31 | Add ticket indexes |
| V32 | Add time_entry indexes |