-- MicroDAO core schema (DDL) -- Requires: PostgreSQL 14+, extensions pgcrypto and pgvector -- Safe to run multiple times (uses IF NOT EXISTS where possible) -- 0) Extensions CREATE EXTENSION IF NOT EXISTS pgcrypto; CREATE EXTENSION IF NOT EXISTS vector; -- 1) Enum types DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'org_type') THEN CREATE TYPE org_type AS ENUM ('solo', 'dao'); END IF; IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'visibility') THEN CREATE TYPE visibility AS ENUM ('private', 'dao', 'public'); END IF; IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'notification_status') THEN CREATE TYPE notification_status AS ENUM ('queued', 'sent', 'failed', 'read'); END IF; END $$; -- 2) Helpers CREATE OR REPLACE FUNCTION set_updated_at() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN NEW.updated_at := NOW(); RETURN NEW; END $$; -- 3) Users and Orgs CREATE TABLE IF NOT EXISTS users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email TEXT UNIQUE NOT NULL, name TEXT, plan TEXT DEFAULT 'free', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TRIGGER trg_users_updated_at BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION set_updated_at(); CREATE TABLE IF NOT EXISTS orgs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, type org_type NOT NULL DEFAULT 'solo', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TRIGGER trg_orgs_updated_at BEFORE UPDATE ON orgs FOR EACH ROW EXECUTE FUNCTION set_updated_at(); -- (optional) membership table for future use CREATE TABLE IF NOT EXISTS org_members ( org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, role TEXT NOT NULL DEFAULT 'member', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY (org_id, user_id) ); -- 4) Channels and Messages CREATE TABLE IF NOT EXISTS channels ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE, name TEXT NOT NULL, visibility visibility NOT NULL DEFAULT 'dao', meta JSONB NOT NULL DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_channels_org ON channels(org_id); CREATE INDEX IF NOT EXISTS idx_channels_meta_gin ON channels USING GIN (meta); CREATE TRIGGER trg_channels_updated_at BEFORE UPDATE ON channels FOR EACH ROW EXECUTE FUNCTION set_updated_at(); CREATE TABLE IF NOT EXISTS messages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE, author_id UUID NOT NULL REFERENCES users(id) ON DELETE SET NULL, content TEXT NOT NULL, meta JSONB NOT NULL DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_messages_channel_created ON messages(channel_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_messages_meta_gin ON messages USING GIN (meta); -- 5) Documents and USDO entities CREATE TABLE IF NOT EXISTS documents ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), source TEXT NOT NULL, -- e.g., 'upload', 'ipfs://cid', 'arxiv:XXXX' owner_type TEXT NOT NULL CHECK (owner_type IN ('user','org')), owner_id UUID NOT NULL, format TEXT NOT NULL, -- 'pdf','html','image','md' meta JSONB NOT NULL DEFAULT '{}', -- e.g., filename, hashes, policyId created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT fk_documents_owner_user FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED -- If owner_type='org', create a partial FK via trigger or accept soft constraint ); -- Optional soft constraint: ensure owner exists in either table (implemented via trigger in app-layer). CREATE INDEX IF NOT EXISTS idx_documents_owner ON documents(owner_id); CREATE INDEX IF NOT EXISTS idx_documents_meta_gin ON documents USING GIN (meta); CREATE TRIGGER trg_documents_updated_at BEFORE UPDATE ON documents FOR EACH ROW EXECUTE FUNCTION set_updated_at(); CREATE TABLE IF NOT EXISTS usdo_entities ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), doc_id UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE, kind TEXT NOT NULL, -- 'section','equation','table','figure','code','reference', etc. payload JSONB NOT NULL, -- normalized USDO fragment meta JSONB NOT NULL DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_usdo_doc_kind ON usdo_entities(doc_id, kind); CREATE INDEX IF NOT EXISTS idx_usdo_payload_gin ON usdo_entities USING GIN (payload); CREATE INDEX IF NOT EXISTS idx_usdo_meta_gin ON usdo_entities USING GIN (meta); -- 6) Chunks and RAG index -- pgvector storage for text/image embeddings; adjust dimensions to your model CREATE TABLE IF NOT EXISTS chunks ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), doc_id UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE, text TEXT NOT NULL, meta JSONB NOT NULL DEFAULT '{}', -- offsets, section ref, citations, policy, etc. acl visibility NOT NULL DEFAULT 'dao', vec VECTOR(1536), -- set to your embedding dimension created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_chunks_doc ON chunks(doc_id); CREATE INDEX IF NOT EXISTS idx_chunks_acl ON chunks(acl); CREATE INDEX IF NOT EXISTS idx_chunks_meta_gin ON chunks USING GIN (meta); -- IVF index for ANN search; tune lists per dataset size DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND indexname = 'idx_chunks_vec_ivfflat' ) THEN CREATE INDEX idx_chunks_vec_ivfflat ON chunks USING ivfflat (vec vector_l2_ops) WITH (lists = 100); END IF; END $$; CREATE TABLE IF NOT EXISTS rag_index ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), chunk_id UUID NOT NULL UNIQUE REFERENCES chunks(id) ON DELETE CASCADE, space visibility NOT NULL, -- private/dao/public space meta JSONB NOT NULL DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_rag_space ON rag_index(space); CREATE INDEX IF NOT EXISTS idx_rag_meta_gin ON rag_index USING GIN (meta); -- 7) Agent events, Policies, Rewards, Notifications, Audit CREATE TABLE IF NOT EXISTS agent_events ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), run_id UUID NOT NULL, type TEXT NOT NULL, -- e.g., 'run.started','run.completed','run.failed','policy.blocked' payload JSONB NOT NULL DEFAULT '{}', ts TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_agent_events_run ON agent_events(run_id, ts DESC); CREATE INDEX IF NOT EXISTS idx_agent_events_type ON agent_events(type); CREATE INDEX IF NOT EXISTS idx_agent_events_payload_gin ON agent_events USING GIN (payload); CREATE TABLE IF NOT EXISTS policies ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), scope TEXT NOT NULL, -- e.g., 'org:{id}', 'channel:{id}', 'global' rules JSONB NOT NULL, -- structured policy rules meta JSONB NOT NULL DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_policies_scope ON policies(scope); CREATE INDEX IF NOT EXISTS idx_policies_rules_gin ON policies USING GIN (rules); CREATE TRIGGER trg_policies_updated_at BEFORE UPDATE ON policies FOR EACH ROW EXECUTE FUNCTION set_updated_at(); CREATE TABLE IF NOT EXISTS rewards ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), actor_id UUID NOT NULL REFERENCES users(id) ON DELETE SET NULL, kind TEXT NOT NULL, -- e.g., 'ingest','compute','contribution' amount NUMERIC(18,6) NOT NULL CHECK (amount >= 0), meta JSONB NOT NULL DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_rewards_actor ON rewards(actor_id); CREATE INDEX IF NOT EXISTS idx_rewards_kind ON rewards(kind); CREATE INDEX IF NOT EXISTS idx_rewards_meta_gin ON rewards USING GIN (meta); CREATE TABLE IF NOT EXISTS notifications ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, kind TEXT NOT NULL, -- e.g., 'ingest.completed','quota.exceeded' payload JSONB NOT NULL DEFAULT '{}', status notification_status NOT NULL DEFAULT 'queued', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), sent_at TIMESTAMPTZ ); CREATE INDEX IF NOT EXISTS idx_notifications_user_status ON notifications(user_id, status); CREATE INDEX IF NOT EXISTS idx_notifications_kind ON notifications(kind); CREATE TABLE IF NOT EXISTS audit_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), actor UUID, -- user id or service principal action TEXT NOT NULL, -- e.g., 'policy.update','channel.visibility.changed' target JSONB NOT NULL DEFAULT '{}', -- embeds ids, kinds ts TIMESTAMPTZ NOT NULL DEFAULT NOW(), ip INET ); CREATE INDEX IF NOT EXISTS idx_audit_ts ON audit_logs(ts DESC); CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_logs(action); CREATE INDEX IF NOT EXISTS idx_audit_target_gin ON audit_logs USING GIN (target); -- 8) Basic RLS scaffolding (disabled by default) -- Enable and tailor at the application stage. Left commented intentionally. -- ALTER TABLE users ENABLE ROW LEVEL SECURITY; -- ALTER TABLE orgs ENABLE ROW LEVEL SECURITY; -- ALTER TABLE channels ENABLE ROW LEVEL SECURITY; -- ALTER TABLE messages ENABLE ROW LEVEL SECURITY; -- ALTER TABLE documents ENABLE ROW LEVEL SECURITY; -- ALTER TABLE chunks ENABLE ROW LEVEL SECURITY; -- ALTER TABLE rag_index ENABLE ROW LEVEL SECURITY; -- Example policy patterns (to customize): -- CREATE POLICY org_member_can_read_channels ON channels -- FOR SELECT USING (EXISTS (SELECT 1 FROM org_members m WHERE m.org_id = channels.org_id AND m.user_id = auth.uid())); -- CREATE POLICY author_can_edit_message ON messages -- FOR UPDATE USING (author_id = auth.uid()); -- 9) Comments for maintainability COMMENT ON TABLE users IS 'End users of MicroDAO. Plan used for quotas.'; COMMENT ON TABLE orgs IS 'Organizations/DAOs.'; COMMENT ON TABLE channels IS 'Chat channels per org with visibility.'; COMMENT ON TABLE messages IS 'Messages in channels with JSONB meta for mentions, citations.'; COMMENT ON TABLE documents IS 'Ingested sources. USDO is derived from here.'; COMMENT ON TABLE usdo_entities IS 'USDO fragments: sections, equations, tables, figures, code, references.'; COMMENT ON TABLE chunks IS 'RAG chunks with embeddings and ACL.'; COMMENT ON COLUMN chunks.vec IS 'Embedding vector; dimension must match your model.'; COMMENT ON TABLE rag_index IS 'Space mapping for chunks (private/dao/public).'; COMMENT ON TABLE agent_events IS 'Operational events for agent runs.'; COMMENT ON TABLE policies IS 'Policy documents and metadata.'; COMMENT ON TABLE rewards IS 'Rewards issued to actors for contributions/compute.'; COMMENT ON TABLE notifications IS 'User notifications across channels.'; COMMENT ON TABLE audit_logs IS 'Immutable audit trail for compliance.';