# 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 - `client_contacts` — Multi-channel contact entries per client - `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 ### Interest Domain - `interests` — Per-berth pipeline records, each belonging to a client (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** |