← back to AbramsOS
db/schema.sql
118 lines
-- AbramsOS — declarative current schema (v0.1, Phase 0)
-- Apply: psql -d abrams_os -f db/schema.sql
-- Idempotent. For deltas after this, use db/migrations/NNNN_*.sql.
BEGIN;
CREATE TABLE IF NOT EXISTS user_account (
id text PRIMARY KEY,
email text NOT NULL UNIQUE,
display_name text,
locale text DEFAULT 'en-US',
timezone text DEFAULT 'America/Los_Angeles',
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS consent_grant (
id text PRIMARY KEY,
user_id text NOT NULL REFERENCES user_account(id) ON DELETE CASCADE,
connector_type text NOT NULL, -- 'gmail', 'outlook', 'plaid', etc.
scopes_json jsonb NOT NULL,
granted_at timestamptz NOT NULL DEFAULT now(),
revoked_at timestamptz
);
CREATE INDEX IF NOT EXISTS consent_grant_user_idx ON consent_grant (user_id, connector_type);
CREATE TABLE IF NOT EXISTS connector_account (
id text PRIMARY KEY,
user_id text NOT NULL REFERENCES user_account(id) ON DELETE CASCADE,
consent_grant_id text NOT NULL REFERENCES consent_grant(id) ON DELETE CASCADE,
provider text NOT NULL, -- 'google'
external_subject_id text NOT NULL, -- Google `sub` or email
refresh_token_encrypted bytea NOT NULL, -- AES-256-GCM
refresh_token_iv bytea NOT NULL,
refresh_token_tag bytea NOT NULL,
last_sync_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (provider, external_subject_id)
);
CREATE INDEX IF NOT EXISTS connector_account_user_idx ON connector_account (user_id);
CREATE TABLE IF NOT EXISTS source_message (
id text PRIMARY KEY,
connector_id text NOT NULL REFERENCES connector_account(id) ON DELETE CASCADE,
source_type text NOT NULL, -- 'gmail'
external_id text NOT NULL, -- Gmail message id
thread_id text,
received_at timestamptz NOT NULL,
subject text,
sender text,
recipient text,
payload_jsonb jsonb NOT NULL,
payload_hash text NOT NULL,
ingested_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (connector_id, source_type, external_id)
);
CREATE INDEX IF NOT EXISTS source_message_received_idx ON source_message (received_at DESC);
CREATE INDEX IF NOT EXISTS source_message_connector_idx ON source_message (connector_id, received_at DESC);
CREATE TABLE IF NOT EXISTS document (
id text PRIMARY KEY,
source_message_id text REFERENCES source_message(id) ON DELETE CASCADE,
user_id text NOT NULL REFERENCES user_account(id) ON DELETE CASCADE,
kind text NOT NULL, -- 'receipt', 'warranty', 'invoice', 'attachment', 'screenshot'
mime text,
hash text NOT NULL,
object_path text, -- relative to uploads/
parsed_status text NOT NULL DEFAULT 'pending',
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS document_user_idx ON document (user_id, created_at DESC);
CREATE TABLE IF NOT EXISTS purchase (
id text PRIMARY KEY,
user_id text NOT NULL REFERENCES user_account(id) ON DELETE CASCADE,
source_message_id text REFERENCES source_message(id) ON DELETE SET NULL,
merchant_name text NOT NULL,
merchant_domain text,
order_number text,
purchase_date timestamptz NOT NULL,
total_amount numeric(12,2),
currency text DEFAULT 'USD',
confidence numeric(3,2), -- 0..1
raw_extract jsonb,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS purchase_user_date_idx ON purchase (user_id, purchase_date DESC);
CREATE INDEX IF NOT EXISTS purchase_merchant_idx ON purchase (user_id, merchant_name);
CREATE TABLE IF NOT EXISTS purchase_item (
id text PRIMARY KEY,
purchase_id text NOT NULL REFERENCES purchase(id) ON DELETE CASCADE,
category text,
brand text,
model text,
gtin text,
serial text,
quantity integer DEFAULT 1,
unit_price numeric(12,2),
raw_text text,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS purchase_item_purchase_idx ON purchase_item (purchase_id);
CREATE TABLE IF NOT EXISTS audit_log (
id bigserial PRIMARY KEY,
occurred_at timestamptz NOT NULL DEFAULT now(),
actor_type text NOT NULL, -- 'user', 'system', 'agent'
actor_id text,
object_type text NOT NULL, -- 'consent_grant', 'connector_account', 'source_message', 'purchase', etc.
object_id text,
event_type text NOT NULL, -- 'oauth_grant', 'sync_started', 'message_persisted', 'purchase_extracted', 'connector_revoked'
metadata_jsonb jsonb NOT NULL DEFAULT '{}'::jsonb
);
CREATE INDEX IF NOT EXISTS audit_log_object_idx ON audit_log (object_type, object_id, occurred_at DESC);
CREATE INDEX IF NOT EXISTS audit_log_event_idx ON audit_log (event_type, occurred_at DESC);
COMMIT;