-- Add migration script here -- Add migration script here CREATE TYPE user_role AS ENUM ('user', 'admin'); CREATE TYPE totp_status AS ENUM ('disabled', 'pending', 'enabled'); CREATE TABLE users ( id BIGSERIAL PRIMARY KEY NOT NULL, role user_role NOT NULL DEFAULT 'user', -- profile nickname VARCHAR(255), -- basic auth username VARCHAR(255) UNIQUE NOT NULL, passhash VARCHAR(255) NOT NULL, -- email email VARCHAR(255), email_verified BOOLEAN DEFAULT FALSE, -- 2fa totp_secret VARCHAR(255), totp_status totp_status NOT NULL DEFAULT 'disabled', -- update tracking created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), deleted_at TIMESTAMPTZ ); CREATE TABLE access_tokens ( id BIGSERIAL PRIMARY KEY NOT NULL, creator_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, code VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, uses INTEGER NOT NULL DEFAULT 0, max_uses INTEGER NOT NULL DEFAULT 1, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), expires_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '24 hours', updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE refresh_tokens ( id BIGSERIAL PRIMARY KEY NOT NULL, user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, token_hash VARCHAR(255) NOT NULL, revoked BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), expires_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '7 days' ); CREATE TABLE spaces ( id BIGSERIAL PRIMARY KEY NOT NULL, name VARCHAR(255) NOT NULL, description TEXT, owner_id BIGINT NOT NULL REFERENCES users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE channels ( id BIGSERIAL PRIMARY KEY NOT NULL, name VARCHAR(255) NOT NULL, description TEXT, space_id BIGINT NOT NULL REFERENCES spaces(id) ON DELETE CASCADE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE space_members ( space_id BIGINT NOT NULL REFERENCES spaces(id) ON DELETE CASCADE, user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), role user_role DEFAULT 'user', PRIMARY KEY (space_id, user_id) ); CREATE TABLE messages ( id BIGSERIAL PRIMARY KEY NOT NULL, channel_id BIGINT NOT NULL REFERENCES channels(id) ON DELETE CASCADE, user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, content TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), is_edited BOOLEAN DEFAULT FALSE ); CREATE TABLE attachments ( id BIGSERIAL PRIMARY KEY NOT NULL, message_id BIGINT NOT NULL REFERENCES messages(id) ON DELETE CASCADE, filename VARCHAR(255) NOT NULL, content_type VARCHAR(100) NOT NULL, -- mime type e.g. image/png, video/mp4 size_bytes BIGINT NOT NULL, url TEXT NOT NULL, -- path to file on your CDN/storage width INTEGER, -- null for non-image/video height INTEGER, -- null for non-image/video duration_ms INTEGER, -- null for non-audio/video created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TYPE relationship_status AS ENUM ('pending', 'accepted', 'blocked'); CREATE TABLE relationships ( id BIGSERIAL PRIMARY KEY NOT NULL, from_user BIGINT NOT NULL REFERENCES users(id), to_user BIGINT NOT NULL REFERENCES users(id), status relationship_status NOT NULL DEFAULT 'pending', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT no_self_relationship CHECK (from_user != to_user), CONSTRAINT unique_relationship UNIQUE (from_user, to_user) ); CREATE INDEX idx_messages_channel_id ON messages(channel_id, created_at DESC); CREATE INDEX idx_messages_user_id ON messages(user_id); CREATE INDEX idx_attachments_message ON attachments(message_id); CREATE INDEX idx_channels_space_id ON channels(space_id); CREATE INDEX idx_space_members_user ON space_members(user_id); CREATE INDEX idx_refresh_tokens_hash ON refresh_tokens(token_hash); CREATE INDEX idx_relationships_from ON relationships(from_user, to_user); CREATE INDEX idx_relationships_to ON relationships(to_user); CREATE INDEX idx_access_tokens_code ON access_tokens(code); CREATE OR REPLACE FUNCTION update_updated_at() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER users_updated_at BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE TRIGGER spaces_updated_at BEFORE UPDATE ON spaces FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE TRIGGER channels_updated_at BEFORE UPDATE ON channels FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE TRIGGER messages_updated_at BEFORE UPDATE ON messages FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE TRIGGER relationships_updated_at BEFORE UPDATE ON relationships FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE TRIGGER access_tokens_updated_at BEFORE UPDATE ON access_tokens FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE OR REPLACE FUNCTION add_owner_to_space() RETURNS TRIGGER AS $$ BEGIN INSERT INTO space_members (space_id, user_id, role, joined_at) VALUES (NEW.id, NEW.owner_id, 'admin', NOW()); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER space_owner_becomes_member AFTER INSERT ON spaces FOR EACH ROW EXECUTE FUNCTION add_owner_to_space();