# Port Nimara CRM — Database Schema (PostgreSQL + Drizzle ORM)

**Compiled:** 2026-03-11
**ORM:** Drizzle ORM with TypeScript strict mode
**Database:** PostgreSQL 16 (Docker container)
**Conventions:** snake_case for all columns and tables, UUID primary keys, `created_at`/`updated_at` on every table, `port_id` on every port-scoped table, soft deletes via `archived_at` where applicable.

---

## Schema Overview

### Multi-Tenancy Core

- `ports` — Marina ports (tenants)
- `users` — CRM user accounts (managed by Better Auth)
- `roles` — Permission role definitions
- `port_role_overrides` — Per-port tweaks to global roles
- `user_port_roles` — User ↔ port ↔ role assignments
- `sessions` — Better Auth session management

### Client Domain

- `clients` — Anchor records for people/entities. Yacht and company details
  are no longer stored here — see the Yacht and Company domains.
- `client_contacts` — Multi-channel contact entries per client
- `client_addresses` — Physical addresses per client (primary + others)
- `client_relationships` — Relationships between clients (referrals, broker, family)
- `client_notes` — Timestamped notes on clients
- `client_tags` — Tags assigned to clients
- `client_merge_log` — Audit trail of client merges

### Yacht Domain

- `yachts` — First-class yacht records. Polymorphic ownership via
  `current_owner_type` (`'client' | 'company'`) + `current_owner_id`.
- `yacht_ownership_history` — Append-only log of every transfer; partial
  unique index `idx_yoh_active` enforces a single active owner per yacht.
- `yacht_notes`, `yacht_tags` — Notes / tags on yachts.

### Company Domain

- `companies` — Legal entities that may own yachts or be billed.
- `company_addresses` — Addresses per company.
- `company_memberships` — Active client ↔ company links with role
  (director / shareholder / beneficial_owner / authorised_signatory),
  start/end dates.

### Reservation Domain

- `berth_reservations` — Concrete client + yacht + berth holds with
  start/end dates and status. Partial unique index `idx_br_active`
  enforces one active reservation per berth.

### Interest Domain

- `interests` — Per-berth pipeline records. Each row references a
  `client_id`, `yacht_id` (the yacht in scope for the inquiry), and
  optional `berth_id`. Milestone dates are inline columns.
- `interest_notes` — Timestamped notes on interests
- `interest_tags` — Tags assigned to interests

### Berth Domain

- `berths` — Physical marina berths
- `berth_map_data` — SVG coordinates and visual data for berth map rendering
- `berth_recommendations` — AI/manual berth recommendations for interests
- `berth_waiting_list` — Waiting list entries per berth
- `berth_maintenance_log` — Maintenance, repair, and inspection records
- `berth_tags` — Tags assigned to berths

### Document Domain

- `documents` — All documents (EOIs, contracts, NDAs, custom)
- `document_signers` — Signers assigned to each document
- `document_events` — Signature events and status changes
- `document_templates` — Reusable document templates with merge fields
- `form_templates` — Pre-filled data collection form definitions
- `form_submissions` — Client form submission data

### Financial Domain

- `expenses` — Operational expenses with receipt tracking
- `invoices` — Generated invoices
- `invoice_line_items` — Line items per invoice
- `invoice_expenses` — Junction table linking invoices to expenses

### Communication Domain

- `email_accounts` — User SMTP/IMAP connection configs
- `email_threads` — Email conversation threads linked to clients
- `email_messages` — Individual email messages within threads

### Operations Domain

- `reminders` — CRM reminders and follow-ups (replaces tasks)
- `google_calendar_tokens` — Per-user Google Calendar OAuth tokens
- `google_calendar_cache` — Cached Google Calendar events for display
- `notifications` — In-app notification records
- `scheduled_reports` — Report schedule configurations
- `report_recipients` — Recipients per scheduled report

### System Domain

- `audit_logs` — Deep audit log with before/after values
- `tags` — Tag definitions (color, name)
- `files` — File metadata (MinIO references)
- `webhooks` — Outbound webhook configurations
- `webhook_deliveries` — Webhook delivery log
- `system_settings` — Key-value system settings (per-user preferences stored inline on `user_profiles.preferences`)
- `saved_views` — Saved filter/view configurations
- `scratchpad_notes` — Per-user quick notes not tied to records
- `user_notification_preferences` — Per-user per-type notification toggles (in-app, email)
- `currency_rates` — Exchange rates cache (Frankfurter API + manual overrides)
- `custom_field_definitions` — Admin-defined custom fields per entity type
- `custom_field_values` — Custom field data per entity record

---

## Detailed Table Definitions

### ports

```sql
CREATE TABLE ports (
  id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name          TEXT NOT NULL,
  slug          TEXT NOT NULL UNIQUE,          -- URL-safe identifier
  logo_url      TEXT,
  primary_color TEXT,                          -- hex color for branding
  default_currency TEXT NOT NULL DEFAULT 'USD',
  timezone      TEXT NOT NULL DEFAULT 'America/Anguilla',
  settings      JSONB NOT NULL DEFAULT '{}',   -- port-specific config overrides
  is_active     BOOLEAN NOT NULL DEFAULT true,
  created_at    TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at    TIMESTAMPTZ NOT NULL DEFAULT now()
);
```

### users

Better Auth manages the core user table. Additional CRM-specific fields:

```sql
-- Better Auth creates its own user/session tables.
-- We extend with:

CREATE TABLE user_profiles (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id         TEXT NOT NULL UNIQUE,         -- references Better Auth user ID
  display_name    TEXT NOT NULL,
  avatar_url      TEXT,
  phone           TEXT,
  is_super_admin  BOOLEAN NOT NULL DEFAULT false,
  is_active       BOOLEAN NOT NULL DEFAULT true,
  last_login_at   TIMESTAMPTZ,
  preferences     JSONB NOT NULL DEFAULT '{}',  -- dark mode, locale, etc.
  created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);
```

### roles

```sql
CREATE TABLE roles (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name            TEXT NOT NULL,
  description     TEXT,
  permissions     JSONB NOT NULL DEFAULT '{}',  -- { "clients.view": true, "clients.create": true, ... }
  is_global       BOOLEAN NOT NULL DEFAULT true, -- available at all ports
  is_system       BOOLEAN NOT NULL DEFAULT false, -- protected from deletion (super_admin, director)
  created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);
```

### port_role_overrides

```sql
CREATE TABLE port_role_overrides (
  id                    UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  port_id               UUID NOT NULL REFERENCES ports(id) ON DELETE CASCADE,
  role_id               UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
  permission_overrides  JSONB NOT NULL DEFAULT '{}', -- overrides specific permissions for this port
  created_at            TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at            TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE(port_id, role_id)
);
```

### user_port_roles

```sql
CREATE TABLE user_port_roles (
  id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id     TEXT NOT NULL,                    -- references Better Auth user ID
  port_id     UUID NOT NULL REFERENCES ports(id) ON DELETE CASCADE,
  role_id     UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
  assigned_by TEXT,                             -- user ID of who assigned this
  created_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE(user_id, port_id, role_id)
);
CREATE INDEX idx_upr_user ON user_port_roles(user_id);
CREATE INDEX idx_upr_port ON user_port_roles(port_id);
```

### clients

```sql
CREATE TABLE clients (
  id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  port_id             UUID NOT NULL REFERENCES ports(id),
  full_name           TEXT NOT NULL,
  company_name        TEXT,
  nationality         TEXT,
  is_proxy            BOOLEAN NOT NULL DEFAULT false,
  proxy_type          TEXT,                      -- broker, representative, family_member, legal_counsel, other
  actual_owner_name   TEXT,                      -- if known, the real owner behind the proxy
  relationship_notes  TEXT,
  yacht_name          TEXT,
  yacht_length_ft     NUMERIC,
  yacht_width_ft      NUMERIC,
  yacht_draft_ft      NUMERIC,
  yacht_length_m      NUMERIC,
  yacht_width_m       NUMERIC,
  yacht_draft_m       NUMERIC,
  berth_size_desired  TEXT,
  preferred_contact_method TEXT,                 -- email, phone, whatsapp
  preferred_language  TEXT,
  timezone            TEXT,
  source              TEXT,                      -- website, manual, referral, broker
  source_details      TEXT,                      -- who referred, which broker, etc.
  archived_at         TIMESTAMPTZ,               -- soft delete / archive
  created_at          TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at          TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_clients_port ON clients(port_id);
CREATE INDEX idx_clients_name ON clients(port_id, full_name);
CREATE INDEX idx_clients_archived ON clients(port_id, archived_at);
```

### client_contacts

```sql
CREATE TABLE client_contacts (
  id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  client_id   UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
  channel     TEXT NOT NULL,                    -- email, phone, whatsapp, other
  value       TEXT NOT NULL,                    -- the actual address/number
  label       TEXT,                             -- primary, secondary, work, personal, broker, assistant
  is_primary  BOOLEAN NOT NULL DEFAULT false,
  notes       TEXT,
  created_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_cc_client ON client_contacts(client_id);
CREATE INDEX idx_cc_email ON client_contacts(channel, value) WHERE channel = 'email';
CREATE INDEX idx_cc_phone ON client_contacts(channel, value) WHERE channel = 'phone';
```

### client_relationships

```sql
CREATE TABLE client_relationships (
  id                UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  port_id           UUID NOT NULL REFERENCES ports(id),
  client_a_id       UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
  client_b_id       UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
  relationship_type TEXT NOT NULL,               -- referred_by, broker_for, family_member, same_vessel, custom
  description       TEXT,
  created_at        TIMESTAMPTZ NOT NULL DEFAULT now(),
  CHECK (client_a_id != client_b_id)
);
```

### client_merge_log

```sql
CREATE TABLE client_merge_log (
  id                UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  port_id           UUID NOT NULL REFERENCES ports(id),
  surviving_client_id UUID NOT NULL REFERENCES clients(id),
  merged_client_id  UUID NOT NULL,               -- the client that was merged away (may no longer exist)
  merged_by         TEXT NOT NULL,                -- user ID
  merge_details     JSONB NOT NULL,               -- which fields were kept from which record
  created_at        TIMESTAMPTZ NOT NULL DEFAULT now()
);
```

### interests

```sql
CREATE TABLE interests (
  id                UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  port_id           UUID NOT NULL REFERENCES ports(id),
  client_id         UUID NOT NULL REFERENCES clients(id),
  berth_id          UUID REFERENCES berths(id),   -- nullable for general inquiries
  pipeline_stage    TEXT NOT NULL DEFAULT 'open',  -- open, details_sent, in_communication, visited, signed_eoi_nda, deposit_10pct, contract, completed
  lead_category     TEXT,                          -- general_interest, specific_qualified, hot_lead
  source            TEXT,                          -- website, manual, referral, broker
  eoi_status        TEXT,                          -- null, waiting_for_signatures, signed, expired
  documenso_id      TEXT,                          -- Documenso document ID
  contract_status   TEXT,
  deposit_status    TEXT,
  reservation_status TEXT,
  date_first_contact TIMESTAMPTZ,
  date_last_contact  TIMESTAMPTZ,
  date_eoi_sent     TIMESTAMPTZ,
  date_eoi_signed   TIMESTAMPTZ,
  date_contract_sent TIMESTAMPTZ,
  date_contract_signed TIMESTAMPTZ,
  date_deposit_received TIMESTAMPTZ,
  reminder_enabled  BOOLEAN NOT NULL DEFAULT false,
  reminder_days     INTEGER,                       -- follow up in X days if no activity
  reminder_last_fired TIMESTAMPTZ,
  notes             TEXT,                          -- quick notes field
  archived_at       TIMESTAMPTZ,
  created_at        TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at        TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_interests_port ON interests(port_id);
CREATE INDEX idx_interests_client ON interests(client_id);
CREATE INDEX idx_interests_berth ON interests(berth_id);
CREATE INDEX idx_interests_stage ON interests(port_id, pipeline_stage);
CREATE INDEX idx_interests_archived ON interests(port_id, archived_at);
```

### interest_notes

```sql
CREATE TABLE interest_notes (
  id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  interest_id   UUID NOT NULL REFERENCES interests(id) ON DELETE CASCADE,
  author_id     TEXT NOT NULL,                    -- user ID
  content       TEXT NOT NULL,
  mentions      TEXT[],                           -- array of mentioned user IDs
  is_locked     BOOLEAN NOT NULL DEFAULT false,   -- locked after 15 min edit window
  created_at    TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at    TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_in_interest ON interest_notes(interest_id);
```

### client_notes

```sql
CREATE TABLE client_notes (
  id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  client_id     UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
  author_id     TEXT NOT NULL,
  content       TEXT NOT NULL,
  mentions      TEXT[],
  is_locked     BOOLEAN NOT NULL DEFAULT false,
  created_at    TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at    TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_cn_client ON client_notes(client_id);
```

### berths

```sql
CREATE TABLE berths (
  id                    UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  port_id               UUID NOT NULL REFERENCES ports(id),
  mooring_number        TEXT NOT NULL,
  area                  TEXT,
  status                TEXT NOT NULL DEFAULT 'available', -- available, under_offer, sold
  length_ft             NUMERIC,
  width_ft              NUMERIC,
  draft_ft              NUMERIC,
  length_m              NUMERIC,
  width_m               NUMERIC,
  draft_m               NUMERIC,
  width_is_minimum      BOOLEAN DEFAULT false,
  nominal_boat_size     TEXT,
  nominal_boat_size_m   TEXT,
  water_depth           NUMERIC,
  water_depth_m         NUMERIC,
  water_depth_is_minimum BOOLEAN DEFAULT false,
  side_pontoon          TEXT,
  power_capacity        TEXT,
  voltage               TEXT,
  mooring_type          TEXT,
  cleat_type            TEXT,
  cleat_capacity        TEXT,
  bollard_type          TEXT,
  bollard_capacity      TEXT,
  access                TEXT,
  price                 NUMERIC,
  price_currency        TEXT NOT NULL DEFAULT 'USD',
  bow_facing            TEXT,
  berth_approved        BOOLEAN DEFAULT false,
  tenure_type           TEXT NOT NULL DEFAULT 'permanent', -- permanent, fixed_term
  tenure_years          INTEGER,                           -- for fixed_term
  tenure_start_date     DATE,
  tenure_end_date       DATE,
  status_last_changed_by TEXT,                            -- user ID who last changed status (manual or accepted suggestion)
  status_last_changed_reason TEXT,                       -- 'manual', 'rule:interest_linked', 'rule:eoi_sent', etc.
  status_last_modified  TIMESTAMPTZ,
  created_at            TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at            TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_berths_port ON berths(port_id);
CREATE INDEX idx_berths_status ON berths(port_id, status);
CREATE INDEX idx_berths_area ON berths(port_id, area);
CREATE UNIQUE INDEX idx_berths_mooring ON berths(port_id, mooring_number);
```

### berth_map_data

```sql
CREATE TABLE berth_map_data (
  id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  berth_id    UUID NOT NULL UNIQUE REFERENCES berths(id) ON DELETE CASCADE,
  svg_path    TEXT,                              -- SVG path string
  x           NUMERIC,
  y           NUMERIC,
  transform   TEXT,
  font_size   NUMERIC,
  extra_data  JSONB DEFAULT '{}',               -- any additional map rendering data
  updated_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);
```

### berth_recommendations

```sql
CREATE TABLE berth_recommendations (
  id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  interest_id   UUID NOT NULL REFERENCES interests(id) ON DELETE CASCADE,
  berth_id      UUID NOT NULL REFERENCES berths(id) ON DELETE CASCADE,
  match_score   NUMERIC,                        -- 0-100
  match_reasons JSONB,                          -- { "dimensional_fit": 95, "power_match": 80, ... }
  source        TEXT NOT NULL DEFAULT 'ai',     -- ai, manual
  created_by    TEXT,                            -- user ID for manual recommendations
  created_at    TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE(interest_id, berth_id)
);
```

### berth_waiting_list

```sql
CREATE TABLE berth_waiting_list (
  id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  berth_id      UUID NOT NULL REFERENCES berths(id) ON DELETE CASCADE,
  client_id     UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
  position      INTEGER NOT NULL,
  priority      TEXT NOT NULL DEFAULT 'normal', -- normal, high
  notify_pref   TEXT DEFAULT 'email',           -- email, in_app, both
  notes         TEXT,
  created_at    TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE(berth_id, client_id)
);
CREATE INDEX idx_bwl_berth ON berth_waiting_list(berth_id, position);
```

### berth_maintenance_log

```sql
CREATE TABLE berth_maintenance_log (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  berth_id        UUID NOT NULL REFERENCES berths(id) ON DELETE CASCADE,
  port_id         UUID NOT NULL REFERENCES ports(id),
  category        TEXT NOT NULL,                 -- routine, repair, inspection, upgrade
  description     TEXT NOT NULL,
  cost            NUMERIC,
  cost_currency   TEXT DEFAULT 'USD',
  responsible_party TEXT,
  performed_date  DATE NOT NULL,
  photo_file_ids  UUID[],                        -- references to files table
  created_by      TEXT NOT NULL,
  created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);
```

### documents

```sql
CREATE TABLE documents (
  id                UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  port_id           UUID NOT NULL REFERENCES ports(id),
  interest_id       UUID REFERENCES interests(id),
  client_id         UUID REFERENCES clients(id),
  document_type     TEXT NOT NULL,                -- eoi, contract, nda, reservation_agreement, other
  title             TEXT NOT NULL,
  status            TEXT NOT NULL DEFAULT 'draft', -- draft, sent, partially_signed, completed, expired, cancelled
  documenso_id      TEXT,                          -- Documenso document ID (null for manual uploads)
  file_id           UUID REFERENCES files(id),     -- MinIO file reference
  signed_file_id    UUID REFERENCES files(id),     -- signed version file reference
  is_manual_upload  BOOLEAN NOT NULL DEFAULT false,
  notes             TEXT,
  created_by        TEXT NOT NULL,
  created_at        TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at        TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_docs_port ON documents(port_id);
CREATE INDEX idx_docs_interest ON documents(interest_id);
CREATE INDEX idx_docs_client ON documents(client_id);
CREATE INDEX idx_docs_type ON documents(port_id, document_type);
```

### document_signers

```sql
CREATE TABLE document_signers (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  document_id     UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
  signer_name     TEXT NOT NULL,
  signer_email    TEXT NOT NULL,
  signer_role     TEXT NOT NULL,                  -- client, developer, sales, approver, other
  signing_order   INTEGER NOT NULL,               -- sequential order
  status          TEXT NOT NULL DEFAULT 'pending', -- pending, signed, declined
  signed_at       TIMESTAMPTZ,
  signing_url     TEXT,
  embedded_url    TEXT,
  created_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_ds_doc ON document_signers(document_id);
```

### document_events

```sql
CREATE TABLE document_events (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  document_id     UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
  event_type      TEXT NOT NULL,                  -- created, sent, viewed, signed, completed, expired, reminder_sent
  signer_id       UUID REFERENCES document_signers(id),
  event_data      JSONB DEFAULT '{}',
  signature_hash  TEXT,                           -- deduplication
  created_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_de_doc ON document_events(document_id);
CREATE UNIQUE INDEX idx_de_dedup ON document_events(document_id, signature_hash) WHERE signature_hash IS NOT NULL;
```

### document_templates

```sql
CREATE TABLE document_templates (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  port_id         UUID NOT NULL REFERENCES ports(id),
  name            TEXT NOT NULL,
  description     TEXT,
  template_type   TEXT NOT NULL,                   -- welcome_letter, handover_checklist, acknowledgment, correspondence, custom
  body_html       TEXT NOT NULL,                    -- rich text with merge field tokens: {{client.full_name}}, {{berth.mooring_number}}, etc.
  merge_fields    JSONB NOT NULL DEFAULT '[]',      -- array of field definitions: [{ "token": "client.full_name", "label": "Client Name", "source": "client" }]
  is_active       BOOLEAN NOT NULL DEFAULT true,
  created_by      TEXT NOT NULL,
  created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_dt_port ON document_templates(port_id);
CREATE INDEX idx_dt_type ON document_templates(port_id, template_type);
```

### form_templates

```sql
CREATE TABLE form_templates (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  port_id         UUID NOT NULL REFERENCES ports(id),
  name            TEXT NOT NULL,
  description     TEXT,
  fields          JSONB NOT NULL,                 -- field definitions with types, labels, validation
  branding        JSONB DEFAULT '{}',             -- logo, colors, header text
  is_active       BOOLEAN NOT NULL DEFAULT true,
  created_by      TEXT NOT NULL,
  created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);
```

### form_submissions

```sql
CREATE TABLE form_submissions (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  form_template_id UUID NOT NULL REFERENCES form_templates(id),
  client_id       UUID REFERENCES clients(id),
  interest_id     UUID REFERENCES interests(id),
  token           TEXT NOT NULL UNIQUE,           -- secure token for form URL
  prefilled_data  JSONB DEFAULT '{}',
  submitted_data  JSONB,
  status          TEXT NOT NULL DEFAULT 'pending', -- pending, submitted, expired
  expires_at      TIMESTAMPTZ,
  submitted_at    TIMESTAMPTZ,
  created_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX idx_fs_token ON form_submissions(token);
```

### expenses

```sql
CREATE TABLE expenses (
  id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  port_id             UUID NOT NULL REFERENCES ports(id),
  establishment_name  TEXT,
  amount              NUMERIC NOT NULL,
  currency            TEXT NOT NULL DEFAULT 'USD',
  amount_usd          NUMERIC,                    -- converted equivalent
  exchange_rate       NUMERIC,                    -- rate used for conversion
  payment_method      TEXT,
  category            TEXT,
  payer               TEXT,
  expense_date        TIMESTAMPTZ NOT NULL,
  description         TEXT,
  receipt_file_ids    UUID[],                      -- references to files table
  payment_status      TEXT DEFAULT 'unpaid',       -- unpaid, paid, partial
  payment_date        DATE,
  payment_reference   TEXT,
  payment_notes       TEXT,
  created_by          TEXT NOT NULL,
  archived_at         TIMESTAMPTZ,
  created_at          TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at          TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_expenses_port ON expenses(port_id);
CREATE INDEX idx_expenses_date ON expenses(port_id, expense_date);
CREATE INDEX idx_expenses_category ON expenses(port_id, category);
```

### invoices

```sql
CREATE TABLE invoices (
  id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  port_id             UUID NOT NULL REFERENCES ports(id),
  invoice_number      TEXT NOT NULL,               -- INV-YYYYMM-### auto-generated
  client_name         TEXT NOT NULL,
  billing_email       TEXT,
  billing_address     TEXT,
  due_date            DATE NOT NULL,
  payment_terms       TEXT NOT NULL DEFAULT 'net30', -- immediate, net10, net15, net30, net45, net60
  currency            TEXT NOT NULL DEFAULT 'USD',
  subtotal            NUMERIC NOT NULL,
  discount_pct        NUMERIC DEFAULT 0,           -- e.g., 2 for net10
  discount_amount     NUMERIC DEFAULT 0,
  fee_pct             NUMERIC DEFAULT 0,           -- e.g., 5 for processing fee
  fee_amount          NUMERIC DEFAULT 0,
  total               NUMERIC NOT NULL,
  status              TEXT NOT NULL DEFAULT 'draft', -- draft, sent, paid, overdue, cancelled
  payment_status      TEXT DEFAULT 'unpaid',
  payment_date        DATE,
  payment_method      TEXT,
  payment_reference   TEXT,
  pdf_file_id         UUID REFERENCES files(id),
  notes               TEXT,
  created_by          TEXT NOT NULL,
  archived_at         TIMESTAMPTZ,
  created_at          TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at          TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX idx_invoices_number ON invoices(port_id, invoice_number);
CREATE INDEX idx_invoices_port ON invoices(port_id);
CREATE INDEX idx_invoices_status ON invoices(port_id, status);
```

### invoice_line_items

```sql
CREATE TABLE invoice_line_items (
  id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  invoice_id    UUID NOT NULL REFERENCES invoices(id) ON DELETE CASCADE,
  description   TEXT NOT NULL,
  quantity      NUMERIC NOT NULL DEFAULT 1,
  unit_price    NUMERIC NOT NULL,
  total         NUMERIC NOT NULL,
  sort_order    INTEGER NOT NULL DEFAULT 0,
  created_at    TIMESTAMPTZ NOT NULL DEFAULT now()
);
```

### invoice_expenses

```sql
CREATE TABLE invoice_expenses (
  invoice_id    UUID NOT NULL REFERENCES invoices(id) ON DELETE CASCADE,
  expense_id    UUID NOT NULL REFERENCES expenses(id) ON DELETE CASCADE,
  PRIMARY KEY (invoice_id, expense_id)
);
```

### email_accounts

```sql
CREATE TABLE email_accounts (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id         TEXT NOT NULL,
  port_id         UUID NOT NULL REFERENCES ports(id),
  provider        TEXT NOT NULL,                  -- google, outlook, custom
  email_address   TEXT NOT NULL,
  smtp_host       TEXT NOT NULL,
  smtp_port       INTEGER NOT NULL,
  imap_host       TEXT NOT NULL,
  imap_port       INTEGER NOT NULL,
  credentials_enc BYTEA NOT NULL,                 -- encrypted credentials
  is_active       BOOLEAN NOT NULL DEFAULT true,
  last_sync_at    TIMESTAMPTZ,
  created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);
```

### email_threads

```sql
CREATE TABLE email_threads (
  id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  port_id       UUID NOT NULL REFERENCES ports(id),
  client_id     UUID REFERENCES clients(id),
  subject       TEXT,
  last_message_at TIMESTAMPTZ,
  message_count INTEGER NOT NULL DEFAULT 0,
  created_at    TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at    TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_et_client ON email_threads(client_id);
CREATE INDEX idx_et_port ON email_threads(port_id);
```

### email_messages

```sql
CREATE TABLE email_messages (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  thread_id       UUID NOT NULL REFERENCES email_threads(id) ON DELETE CASCADE,
  message_id_header TEXT,                        -- email Message-ID header
  from_address    TEXT NOT NULL,
  to_addresses    TEXT[] NOT NULL,
  cc_addresses    TEXT[],
  subject         TEXT,
  body_text       TEXT,
  body_html       TEXT,
  direction       TEXT NOT NULL,                 -- inbound, outbound
  sent_at         TIMESTAMPTZ NOT NULL,
  attachment_file_ids UUID[],                    -- references to files table
  raw_file_id     UUID REFERENCES files(id),     -- full raw email stored in MinIO
  created_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_em_thread ON email_messages(thread_id);
```

### reminders

```sql
CREATE TABLE reminders (
  id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  port_id             UUID NOT NULL REFERENCES ports(id),
  title               TEXT NOT NULL,
  note                TEXT,
  due_at              TIMESTAMPTZ NOT NULL,
  priority            TEXT NOT NULL DEFAULT 'medium', -- low, medium, high, urgent
  status              TEXT NOT NULL DEFAULT 'pending', -- pending, snoozed, completed, dismissed
  assigned_to         TEXT,                           -- user ID
  created_by          TEXT NOT NULL,
  client_id           UUID REFERENCES clients(id),
  interest_id         UUID REFERENCES interests(id),
  berth_id            UUID REFERENCES berths(id),
  auto_generated      BOOLEAN NOT NULL DEFAULT false, -- true if created by follow-up reminder system
  google_calendar_event_id TEXT,                      -- Google Calendar event ID (if synced)
  google_calendar_synced BOOLEAN NOT NULL DEFAULT false,
  snoozed_until       TIMESTAMPTZ,                    -- if snoozed, when to resurface
  completed_at        TIMESTAMPTZ,
  created_at          TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at          TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_reminders_port ON reminders(port_id);
CREATE INDEX idx_reminders_assigned ON reminders(assigned_to, status);
CREATE INDEX idx_reminders_due ON reminders(port_id, due_at) WHERE status IN ('pending', 'snoozed');
```

### google_calendar_tokens

```sql
CREATE TABLE google_calendar_tokens (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id         TEXT NOT NULL UNIQUE,
  access_token    TEXT NOT NULL,                    -- encrypted (pgcrypto)
  refresh_token   TEXT NOT NULL,                    -- encrypted (pgcrypto)
  token_expiry    TIMESTAMPTZ NOT NULL,
  calendar_id     TEXT NOT NULL DEFAULT 'primary',  -- which calendar to sync
  connected_at    TIMESTAMPTZ NOT NULL DEFAULT now(),
  last_sync_at    TIMESTAMPTZ,
  sync_enabled    BOOLEAN NOT NULL DEFAULT true,
  created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);
```

### google_calendar_cache

```sql
CREATE TABLE google_calendar_cache (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id         TEXT NOT NULL,
  event_id        TEXT NOT NULL,                    -- Google Calendar event ID
  title           TEXT NOT NULL,
  start_at        TIMESTAMPTZ NOT NULL,
  end_at          TIMESTAMPTZ,
  location        TEXT,
  description     TEXT,
  is_crm_pushed   BOOLEAN NOT NULL DEFAULT false,   -- true if this event originated from a CRM reminder
  reminder_id     UUID REFERENCES reminders(id),     -- linked CRM reminder (if is_crm_pushed)
  fetched_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE(user_id, event_id)
);
CREATE INDEX idx_gcal_cache_user ON google_calendar_cache(user_id, start_at);
```

### notifications

```sql
CREATE TABLE notifications (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  port_id         UUID NOT NULL REFERENCES ports(id),
  user_id         TEXT NOT NULL,
  type            TEXT NOT NULL,                  -- reminder_due, reminder_overdue, new_registration, eoi_signed, eoi_completed, email_received, duplicate_alert, invoice_overdue, waiting_list, system_alert, follow_up_created, tenure_expiring
  title           TEXT NOT NULL,
  description     TEXT,
  link            TEXT,                           -- URL to relevant record
  entity_type     TEXT,                           -- client, interest, berth, invoice, etc.
  entity_id       UUID,
  is_read         BOOLEAN NOT NULL DEFAULT false,
  email_sent      BOOLEAN NOT NULL DEFAULT false,
  created_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_notif_user ON notifications(user_id, is_read);
CREATE INDEX idx_notif_port ON notifications(port_id);
```

### audit_logs

```sql
CREATE TABLE audit_logs (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  port_id         UUID REFERENCES ports(id),      -- null for system-level events
  user_id         TEXT,                            -- null for system-generated events
  action          TEXT NOT NULL,                   -- create, update, delete, archive, restore, merge, login, logout, revert
  entity_type     TEXT NOT NULL,                   -- client, interest, berth, expense, invoice, file, user, role, etc.
  entity_id       UUID,
  field_changed   TEXT,                            -- specific field for update actions
  old_value       JSONB,
  new_value       JSONB,
  ip_address      TEXT,
  user_agent      TEXT,
  reverted_by     TEXT,                            -- user ID if this change was reverted
  reverted_at     TIMESTAMPTZ,
  revert_of       UUID REFERENCES audit_logs(id),  -- points to the audit entry this reverts
  metadata        JSONB DEFAULT '{}',              -- extra context
  created_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_al_port ON audit_logs(port_id, created_at DESC);
CREATE INDEX idx_al_entity ON audit_logs(entity_type, entity_id);
CREATE INDEX idx_al_user ON audit_logs(user_id, created_at DESC);
CREATE INDEX idx_al_created ON audit_logs(created_at DESC);
```

### tags

```sql
CREATE TABLE tags (
  id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  port_id     UUID NOT NULL REFERENCES ports(id),
  name        TEXT NOT NULL,
  color       TEXT NOT NULL DEFAULT '#6B7280',   -- hex color
  created_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE(port_id, name)
);
```

### client_tags

```sql
CREATE TABLE client_tags (
  client_id   UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
  tag_id      UUID NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
  PRIMARY KEY (client_id, tag_id)
);
```

### interest_tags

```sql
CREATE TABLE interest_tags (
  interest_id UUID NOT NULL REFERENCES interests(id) ON DELETE CASCADE,
  tag_id      UUID NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
  PRIMARY KEY (interest_id, tag_id)
);
```

### berth_tags

```sql
CREATE TABLE berth_tags (
  berth_id    UUID NOT NULL REFERENCES berths(id) ON DELETE CASCADE,
  tag_id      UUID NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
  PRIMARY KEY (berth_id, tag_id)
);
```

### files

```sql
CREATE TABLE files (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  port_id         UUID NOT NULL REFERENCES ports(id),
  client_id       UUID REFERENCES clients(id),
  filename        TEXT NOT NULL,
  original_name   TEXT NOT NULL,
  mime_type       TEXT,
  size_bytes      BIGINT,
  storage_path    TEXT NOT NULL,                   -- MinIO object key
  storage_bucket  TEXT NOT NULL DEFAULT 'crm-files',
  category        TEXT,                            -- eoi, contract, image, receipt, correspondence, misc
  uploaded_by     TEXT NOT NULL,
  created_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_files_port ON files(port_id);
CREATE INDEX idx_files_client ON files(client_id);
```

### webhooks

```sql
CREATE TABLE webhooks (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  port_id         UUID NOT NULL REFERENCES ports(id),
  name            TEXT NOT NULL,
  url             TEXT NOT NULL,
  secret          TEXT,                            -- HMAC signing secret
  events          TEXT[] NOT NULL,                  -- array of event types to subscribe to
  is_active       BOOLEAN NOT NULL DEFAULT true,
  created_by      TEXT NOT NULL,
  created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);
```

### webhook_deliveries

```sql
CREATE TABLE webhook_deliveries (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  webhook_id      UUID NOT NULL REFERENCES webhooks(id) ON DELETE CASCADE,
  event_type      TEXT NOT NULL,
  payload         JSONB NOT NULL,
  response_status INTEGER,
  response_body   TEXT,
  attempt         INTEGER NOT NULL DEFAULT 1,
  status          TEXT NOT NULL DEFAULT 'pending', -- pending, success, failed, dead_letter
  delivered_at    TIMESTAMPTZ,
  created_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_wd_webhook ON webhook_deliveries(webhook_id, created_at DESC);
```

### scheduled_reports

```sql
CREATE TABLE scheduled_reports (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  port_id         UUID NOT NULL REFERENCES ports(id),
  name            TEXT NOT NULL,
  report_type     TEXT NOT NULL,                   -- pipeline_summary, expense_summary, berth_occupancy, activity_log, overdue_items, revenue_forecast
  schedule        TEXT NOT NULL,                   -- cron expression
  last_run_at     TIMESTAMPTZ,
  next_run_at     TIMESTAMPTZ,
  is_active       BOOLEAN NOT NULL DEFAULT true,
  config          JSONB DEFAULT '{}',              -- report-specific config options
  created_by      TEXT NOT NULL,
  created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);
```

### report_recipients

```sql
CREATE TABLE report_recipients (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  report_id       UUID NOT NULL REFERENCES scheduled_reports(id) ON DELETE CASCADE,
  email           TEXT NOT NULL,
  user_id         TEXT,                            -- null for external recipients
  UNIQUE(report_id, email)
);
```

### system_settings

```sql
CREATE TABLE system_settings (
  key         TEXT PRIMARY KEY,
  value       JSONB NOT NULL,
  port_id     UUID REFERENCES ports(id),          -- null for global settings
  updated_by  TEXT,
  updated_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE(key, port_id)
);
```

**Key system_settings entries include:**

- `berth_status_rules` (per-port) — JSON array of `{ trigger, mode, target_status }` objects defining berth auto-status transition behavior. Modes: `auto`, `suggest`, `off`. See BR-001.
- `follow_up_defaults` (per-port) — Default reminder_days, send window hours, cooldown period
- `eoi_reminder_settings` (per-port) — EOI signing reminder schedule, cooldown, send window
- `currency_display` (global) — Primary currency, secondary currency, decimal places

### saved_views

```sql
CREATE TABLE saved_views (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  port_id         UUID NOT NULL REFERENCES ports(id),
  user_id         TEXT NOT NULL,
  entity_type     TEXT NOT NULL,                   -- clients, interests, berths, expenses, invoices
  name            TEXT NOT NULL,
  filters         JSONB NOT NULL,
  sort_config     JSONB,
  column_config   JSONB,                           -- which columns visible, column order
  is_shared       BOOLEAN NOT NULL DEFAULT false,
  is_default      BOOLEAN NOT NULL DEFAULT false,
  created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_sv_user ON saved_views(user_id, entity_type);
```

### scratchpad_notes

```sql
CREATE TABLE scratchpad_notes (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id         TEXT NOT NULL,
  content         TEXT NOT NULL,
  linked_client_id UUID REFERENCES clients(id),    -- null until dragged onto a client
  linked_at       TIMESTAMPTZ,
  created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_sp_user ON scratchpad_notes(user_id);
```

### user_notification_preferences

```sql
CREATE TABLE user_notification_preferences (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id         TEXT NOT NULL,
  port_id         UUID NOT NULL REFERENCES ports(id),
  notification_type TEXT NOT NULL,
  in_app          BOOLEAN NOT NULL DEFAULT true,
  email           BOOLEAN NOT NULL DEFAULT true,
  UNIQUE(user_id, port_id, notification_type)
);
```

### currency_rates

```sql
CREATE TABLE currency_rates (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  base_currency   TEXT NOT NULL,
  target_currency TEXT NOT NULL,
  rate            NUMERIC NOT NULL,
  source          TEXT NOT NULL DEFAULT 'frankfurter', -- frankfurter, manual
  fetched_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE(base_currency, target_currency)
);
```

### custom_fields

```sql
CREATE TABLE custom_field_definitions (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  port_id         UUID NOT NULL REFERENCES ports(id),
  entity_type     TEXT NOT NULL,                   -- client, interest, berth
  field_name      TEXT NOT NULL,
  field_label     TEXT NOT NULL,
  field_type      TEXT NOT NULL,                   -- text, number, date, boolean, select
  select_options  JSONB,                           -- for select type: ["option1", "option2"]
  is_required     BOOLEAN NOT NULL DEFAULT false,
  sort_order      INTEGER NOT NULL DEFAULT 0,
  created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE(port_id, entity_type, field_name)
);

CREATE TABLE custom_field_values (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  field_id        UUID NOT NULL REFERENCES custom_field_definitions(id) ON DELETE CASCADE,
  entity_id       UUID NOT NULL,                   -- references the client/interest/berth ID
  value           JSONB NOT NULL,
  created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE(field_id, entity_id)
);
CREATE INDEX idx_cfv_entity ON custom_field_values(entity_id);
```

---

## Table Count Summary

| Category             | Tables                                                                                                                                                                                              | Count         |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| Multi-Tenancy Core   | ports, user_profiles, roles, port_role_overrides, user_port_roles                                                                                                                                   | 5             |
| Client Domain        | clients, client_contacts, client_relationships, client_merge_log, client_notes, client_tags                                                                                                         | 6             |
| Interest Domain      | interests, interest_notes, interest_tags                                                                                                                                                            | 3             |
| Berth Domain         | berths, berth_map_data, berth_recommendations, berth_waiting_list, berth_maintenance_log, berth_tags                                                                                                | 6             |
| Document Domain      | documents, document_signers, document_events, document_templates, form_templates, form_submissions                                                                                                  | 6             |
| Financial Domain     | expenses, invoices, invoice_line_items, invoice_expenses                                                                                                                                            | 4             |
| Communication Domain | email_accounts, email_threads, email_messages                                                                                                                                                       | 3             |
| Operations Domain    | reminders, google_calendar_tokens, google_calendar_cache, notifications, scheduled_reports, report_recipients                                                                                       | 6             |
| System Domain        | audit_logs, tags, files, webhooks, webhook_deliveries, system_settings, saved_views, scratchpad_notes, user_notification_preferences, currency_rates, custom_field_definitions, custom_field_values | 12            |
| **TOTAL**            |                                                                                                                                                                                                     | **51 tables** |
