-- +goose Up -- gen_random_uuid() is built-in since PostgreSQL 13, no extension needed -- Admin authentication (single row) CREATE TABLE admin ( id SERIAL PRIMARY KEY, password_hash BYTEA, -- NULL until first login() sets password created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Insert the single admin row INSERT INTO admin (id) VALUES (1); -- Sessions for admin authentication CREATE TABLE session ( id BIGSERIAL PRIMARY KEY, session_id TEXT NOT NULL UNIQUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), expires_at TIMESTAMPTZ NOT NULL ); CREATE INDEX idx_session_expires_at ON session(expires_at); -- Items (the main content) CREATE TABLE item ( id BIGSERIAL PRIMARY KEY, pub_id UUID NOT NULL DEFAULT gen_random_uuid() UNIQUE, title TEXT, description TEXT, link_url TEXT, -- Source URL (optional) item_type TEXT NOT NULL, -- 'image', 'video', 'quote', 'embed' embed_provider TEXT, -- 'youtube', 'vimeo', NULL embed_video_id TEXT, -- Video ID for embeds embed_html TEXT, -- Cached embed iframe HTML created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), deleted_at TIMESTAMPTZ -- Soft delete ); CREATE INDEX idx_item_deleted ON item(deleted_at); CREATE INDEX idx_item_created ON item(created_at DESC); CREATE INDEX idx_item_pub_id ON item(pub_id); -- Media blobs (stored in DB) CREATE TABLE media ( id BIGSERIAL PRIMARY KEY, item_id BIGINT NOT NULL REFERENCES item(id) ON DELETE CASCADE, media_type TEXT NOT NULL, -- 'original', 'thumbnail' content_type TEXT NOT NULL, -- MIME type data BYTEA NOT NULL, width INT, height INT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_media_item_id ON media(item_id); -- Tags CREATE TABLE tag ( id BIGSERIAL PRIMARY KEY, name TEXT NOT NULL UNIQUE ); CREATE INDEX idx_tag_name ON tag(name); -- Item-Tag junction CREATE TABLE item_tag ( item_id BIGINT NOT NULL REFERENCES item(id) ON DELETE CASCADE, tag_id BIGINT NOT NULL REFERENCES tag(id) ON DELETE CASCADE, PRIMARY KEY (item_id, tag_id) ); CREATE INDEX idx_item_tag_tag_id ON item_tag(tag_id); -- +goose Down DROP TABLE IF EXISTS item_tag; DROP TABLE IF EXISTS tag; DROP TABLE IF EXISTS media; DROP TABLE IF EXISTS item; DROP TABLE IF EXISTS session; DROP TABLE IF EXISTS admin;