164 lines
5.9 KiB
SQL
164 lines
5.9 KiB
SQL
-- Dealspace initial schema
|
|
-- All content fields (search_key, search_key2, summary, data) are encrypted blobs.
|
|
-- Plain fields (entry_id, project_id, parent_id, type, depth, stage, timestamps, IDs) are indexed.
|
|
|
|
PRAGMA journal_mode = WAL;
|
|
PRAGMA foreign_keys = ON;
|
|
|
|
-- Users
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
user_id TEXT PRIMARY KEY,
|
|
email TEXT NOT NULL UNIQUE,
|
|
name TEXT NOT NULL,
|
|
password TEXT NOT NULL, -- bcrypt hash
|
|
org_id TEXT NOT NULL DEFAULT '',
|
|
org_name TEXT NOT NULL DEFAULT '',
|
|
mfa_secret TEXT NOT NULL DEFAULT '', -- TOTP secret, encrypted
|
|
active INTEGER NOT NULL DEFAULT 1,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
|
CREATE INDEX IF NOT EXISTS idx_users_org ON users(org_id);
|
|
|
|
-- Entries: the single tree
|
|
CREATE TABLE IF NOT EXISTS entries (
|
|
entry_id TEXT PRIMARY KEY,
|
|
project_id TEXT NOT NULL,
|
|
parent_id TEXT NOT NULL DEFAULT '',
|
|
type TEXT NOT NULL,
|
|
depth INTEGER NOT NULL DEFAULT 0,
|
|
search_key BLOB, -- blind index (HMAC-SHA256)
|
|
search_key2 BLOB, -- blind index (HMAC-SHA256)
|
|
summary BLOB, -- packed: zstd + AES-256-GCM
|
|
data BLOB, -- packed: zstd + AES-256-GCM
|
|
stage TEXT NOT NULL DEFAULT 'pre_dataroom',
|
|
assignee_id TEXT NOT NULL DEFAULT '',
|
|
return_to_id TEXT NOT NULL DEFAULT '',
|
|
origin_id TEXT NOT NULL DEFAULT '',
|
|
version INTEGER NOT NULL DEFAULT 1,
|
|
deleted_at INTEGER,
|
|
deleted_by TEXT,
|
|
key_version INTEGER NOT NULL DEFAULT 1,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL,
|
|
created_by TEXT NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_entries_project ON entries(project_id);
|
|
CREATE INDEX IF NOT EXISTS idx_entries_parent ON entries(parent_id);
|
|
CREATE INDEX IF NOT EXISTS idx_entries_type ON entries(type);
|
|
CREATE INDEX IF NOT EXISTS idx_entries_stage ON entries(stage);
|
|
CREATE INDEX IF NOT EXISTS idx_entries_assignee ON entries(assignee_id);
|
|
CREATE INDEX IF NOT EXISTS idx_entries_return ON entries(return_to_id);
|
|
CREATE INDEX IF NOT EXISTS idx_entries_origin ON entries(origin_id);
|
|
CREATE INDEX IF NOT EXISTS idx_entries_search_key ON entries(search_key);
|
|
CREATE INDEX IF NOT EXISTS idx_entries_deleted ON entries(deleted_at);
|
|
|
|
-- Answer ↔ Request links (many-to-many)
|
|
CREATE TABLE IF NOT EXISTS answer_links (
|
|
answer_id TEXT NOT NULL REFERENCES entries(entry_id),
|
|
request_id TEXT NOT NULL REFERENCES entries(entry_id),
|
|
linked_by TEXT NOT NULL,
|
|
linked_at INTEGER NOT NULL,
|
|
confirmed INTEGER NOT NULL DEFAULT 0,
|
|
ai_score REAL,
|
|
status TEXT NOT NULL DEFAULT 'pending', -- pending | confirmed | rejected
|
|
reviewed_by TEXT,
|
|
reviewed_at INTEGER,
|
|
reject_reason TEXT,
|
|
PRIMARY KEY (answer_id, request_id)
|
|
);
|
|
|
|
-- Access / RBAC
|
|
CREATE TABLE IF NOT EXISTS access (
|
|
id TEXT PRIMARY KEY,
|
|
project_id TEXT NOT NULL,
|
|
workstream_id TEXT,
|
|
user_id TEXT NOT NULL,
|
|
role TEXT NOT NULL,
|
|
ops TEXT NOT NULL,
|
|
can_grant INTEGER NOT NULL DEFAULT 0,
|
|
granted_by TEXT NOT NULL,
|
|
granted_at INTEGER NOT NULL,
|
|
revoked_at INTEGER,
|
|
revoked_by TEXT
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_access_user ON access(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_access_project ON access(project_id);
|
|
|
|
-- Entry events (workflow thread)
|
|
CREATE TABLE IF NOT EXISTS entry_events (
|
|
id TEXT PRIMARY KEY,
|
|
entry_id TEXT NOT NULL REFERENCES entries(entry_id),
|
|
actor_id TEXT NOT NULL,
|
|
channel TEXT NOT NULL,
|
|
action BLOB NOT NULL, -- packed
|
|
data BLOB NOT NULL, -- packed
|
|
ts INTEGER NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_events_entry ON entry_events(entry_id);
|
|
CREATE INDEX IF NOT EXISTS idx_events_actor ON entry_events(actor_id);
|
|
|
|
-- Channel threads (email/Slack/Teams → entry mapping)
|
|
CREATE TABLE IF NOT EXISTS channel_threads (
|
|
id TEXT PRIMARY KEY,
|
|
entry_id TEXT NOT NULL REFERENCES entries(entry_id),
|
|
channel TEXT NOT NULL,
|
|
thread_id TEXT NOT NULL,
|
|
project_id TEXT NOT NULL,
|
|
UNIQUE(channel, thread_id)
|
|
);
|
|
|
|
-- Embeddings (AI matching)
|
|
CREATE TABLE IF NOT EXISTS embeddings (
|
|
entry_id TEXT PRIMARY KEY REFERENCES entries(entry_id),
|
|
vector BLOB NOT NULL
|
|
);
|
|
|
|
-- Audit log
|
|
CREATE TABLE IF NOT EXISTS audit (
|
|
id TEXT PRIMARY KEY,
|
|
project_id TEXT NOT NULL,
|
|
actor_id TEXT NOT NULL,
|
|
action BLOB NOT NULL, -- packed
|
|
target_id TEXT,
|
|
details BLOB, -- packed
|
|
ip TEXT,
|
|
ts INTEGER NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_audit_project ON audit(project_id);
|
|
CREATE INDEX IF NOT EXISTS idx_audit_actor ON audit(actor_id);
|
|
CREATE INDEX IF NOT EXISTS idx_audit_ts ON audit(ts);
|
|
|
|
-- Sessions
|
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
id TEXT PRIMARY KEY,
|
|
user_id TEXT NOT NULL REFERENCES users(user_id),
|
|
fingerprint TEXT NOT NULL DEFAULT '',
|
|
created_at INTEGER NOT NULL,
|
|
expires_at INTEGER NOT NULL,
|
|
revoked INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id);
|
|
|
|
-- Auth challenges (email OTP)
|
|
CREATE TABLE IF NOT EXISTS challenges (
|
|
challenge_id TEXT PRIMARY KEY,
|
|
email TEXT NOT NULL,
|
|
code TEXT NOT NULL,
|
|
created_at INTEGER NOT NULL,
|
|
expires_at INTEGER NOT NULL,
|
|
used INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_challenges_email ON challenges(email);
|
|
|
|
-- Broadcasts (idempotency)
|
|
CREATE TABLE IF NOT EXISTS broadcasts (
|
|
id TEXT PRIMARY KEY,
|
|
answer_id TEXT NOT NULL,
|
|
request_id TEXT NOT NULL,
|
|
recipient_id TEXT NOT NULL,
|
|
sent_at INTEGER NOT NULL,
|
|
UNIQUE(answer_id, request_id, recipient_id)
|
|
);
|