-- ================================================== -- VIBN UNIFIED SCHEMA FOR COOLIFY -- Combines Firebase collections + Railway PostgreSQL -- ================================================== -- Extensions CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- ================================================== -- USERS (replaces Firebase Auth + Firestore users) -- ================================================== CREATE TABLE users ( id SERIAL PRIMARY KEY, uid VARCHAR(255) UNIQUE NOT NULL, -- Firebase UID or generated email VARCHAR(255) UNIQUE NOT NULL, password_hash VARCHAR(255), -- For email/password auth display_name VARCHAR(255), photo_url VARCHAR(500), workspace VARCHAR(255) UNIQUE NOT NULL, -- URL slug: "marks-account" -- OAuth providers google_id VARCHAR(255) UNIQUE, github_id VARCHAR(255) UNIQUE, -- Metadata created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), last_login TIMESTAMP, settings JSONB DEFAULT '{}'::jsonb ); CREATE INDEX idx_users_email ON users(email); CREATE INDEX idx_users_workspace ON users(workspace); -- ================================================== -- CLIENTS (Organizations/Companies) -- ================================================== CREATE TABLE clients ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, slug VARCHAR(255) UNIQUE NOT NULL, -- URL-friendly owner_user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, -- Subscription/Billing subscription_tier VARCHAR(50) DEFAULT 'free', -- free, starter, pro, enterprise stripe_customer_id VARCHAR(255), max_users INTEGER DEFAULT 5, max_projects INTEGER DEFAULT 10, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), metadata JSONB DEFAULT '{}'::jsonb ); CREATE INDEX idx_clients_owner ON clients(owner_user_id); CREATE INDEX idx_clients_slug ON clients(slug); -- ================================================== -- PROJECTS (replaces Firebase projects collection) -- ================================================== CREATE TABLE projects ( id SERIAL PRIMARY KEY, firebase_id VARCHAR(255) UNIQUE, -- For migration compatibility client_id INTEGER REFERENCES clients(id) ON DELETE CASCADE, user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, -- Owner -- Project Identity name VARCHAR(255) NOT NULL, slug VARCHAR(255) NOT NULL, workspace VARCHAR(255) NOT NULL, -- User workspace slug -- Product Details product_name VARCHAR(255) NOT NULL, product_vision TEXT, project_type VARCHAR(50) DEFAULT 'scratch', -- scratch, existing -- Status status VARCHAR(50) DEFAULT 'active', -- active, on_hold, completed, archived current_phase VARCHAR(50) DEFAULT 'collection', -- collection, extraction, vision, mvp, marketing phase_status VARCHAR(50) DEFAULT 'not_started', -- not_started, in_progress, completed -- Integrations workspace_path VARCHAR(500), -- Local directory path workspace_name VARCHAR(255), is_for_client BOOLEAN DEFAULT false, has_logo BOOLEAN DEFAULT false, has_domain BOOLEAN DEFAULT false, has_website BOOLEAN DEFAULT false, has_github BOOLEAN DEFAULT false, has_chatgpt BOOLEAN DEFAULT false, github_repo VARCHAR(500), chatgpt_project_id VARCHAR(255), -- Git/Deployment gitea_repo_url VARCHAR(500), coolify_app_id VARCHAR(255), coolify_project_uuid VARCHAR(255), deployment_url VARCHAR(500), -- Phase Data phase_data JSONB DEFAULT '{}'::jsonb, -- Stores all phase artifacts phase_history JSONB DEFAULT '[]'::jsonb, phase_scores JSONB DEFAULT '{}'::jsonb, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), UNIQUE(client_id, slug) ); CREATE INDEX idx_projects_client ON projects(client_id); CREATE INDEX idx_projects_user ON projects(user_id); CREATE INDEX idx_projects_workspace ON projects(workspace); CREATE INDEX idx_projects_status ON projects(status); CREATE INDEX idx_projects_firebase_id ON projects(firebase_id); -- ================================================== -- PROJECT_CONTRIBUTORS (Team collaboration) -- ================================================== CREATE TABLE project_contributors ( id SERIAL PRIMARY KEY, project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE, user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, role VARCHAR(50) DEFAULT 'developer', -- owner, admin, developer, viewer permissions JSONB DEFAULT '{"read":true,"write":true,"admin":false}'::jsonb, joined_at TIMESTAMP DEFAULT NOW(), total_time_minutes INTEGER DEFAULT 0, UNIQUE(project_id, user_id) ); CREATE INDEX idx_contributors_project ON project_contributors(project_id); CREATE INDEX idx_contributors_user ON project_contributors(user_id); -- ================================================== -- SESSIONS (replaces both Firebase sessions + Railway logs) -- ================================================== CREATE TABLE sessions ( id SERIAL PRIMARY KEY, session_id VARCHAR(255) UNIQUE NOT NULL, firebase_id VARCHAR(255), -- For migration project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE, user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, -- Timing started_at TIMESTAMP NOT NULL DEFAULT NOW(), last_updated TIMESTAMP NOT NULL DEFAULT NOW(), ended_at TIMESTAMP, duration_minutes INTEGER DEFAULT 0, -- Status status VARCHAR(50) DEFAULT 'active', -- active, idle, completed needs_project_association BOOLEAN DEFAULT false, -- Context workspace_path VARCHAR(500), workspace_name VARCHAR(255), ide_name VARCHAR(50) DEFAULT 'Cursor', -- Cursor, VS Code, etc. -- Content conversation JSONB DEFAULT '[]'::jsonb, -- Full chat history file_changes JSONB DEFAULT '[]'::jsonb, -- Files modified files_modified TEXT[], -- Quick array for queries -- Metrics message_count INTEGER DEFAULT 0, user_message_count INTEGER DEFAULT 0, assistant_message_count INTEGER DEFAULT 0, file_change_count INTEGER DEFAULT 0, -- AI Usage & Cost total_tokens INTEGER DEFAULT 0, prompt_tokens INTEGER DEFAULT 0, completion_tokens INTEGER DEFAULT 0, estimated_cost_usd NUMERIC(10,6) DEFAULT 0, model VARCHAR(100), -- AI Insights summary TEXT, conversation_summary TEXT, tasks_identified JSONB DEFAULT '[]'::jsonb, decisions_made JSONB DEFAULT '[]'::jsonb, technologies_used JSONB DEFAULT '[]'::jsonb, metadata JSONB DEFAULT '{}'::jsonb ); CREATE INDEX idx_sessions_project ON sessions(project_id); CREATE INDEX idx_sessions_user ON sessions(user_id); CREATE INDEX idx_sessions_status ON sessions(status); CREATE INDEX idx_sessions_started ON sessions(started_at); CREATE INDEX idx_sessions_session_id ON sessions(session_id); CREATE INDEX idx_sessions_firebase_id ON sessions(firebase_id); -- ================================================== -- WORK_COMPLETED (AI-extracted accomplishments) -- ================================================== CREATE TABLE work_completed ( id SERIAL PRIMARY KEY, session_id INTEGER REFERENCES sessions(id) ON DELETE CASCADE, project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE, title VARCHAR(500) NOT NULL, description TEXT, category VARCHAR(100), -- frontend, backend, database, deployment, testing, docs, bugfix files_modified JSONB DEFAULT '[]'::jsonb, completed_at TIMESTAMP DEFAULT NOW(), extracted_by_ai BOOLEAN DEFAULT true ); CREATE INDEX idx_work_completed_project ON work_completed(project_id); CREATE INDEX idx_work_completed_session ON work_completed(session_id); CREATE INDEX idx_work_completed_category ON work_completed(category); -- ================================================== -- ARCHITECTURAL_DECISIONS (Design decisions) -- ================================================== CREATE TABLE architectural_decisions ( id SERIAL PRIMARY KEY, project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE, identified_by_session_id INTEGER REFERENCES sessions(id) ON DELETE SET NULL, title VARCHAR(500) NOT NULL, context TEXT, decision TEXT NOT NULL, consequences TEXT, status VARCHAR(50) DEFAULT 'accepted', -- proposed, accepted, deprecated, superseded tags JSONB DEFAULT '[]'::jsonb, decided_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); CREATE INDEX idx_arch_decisions_project ON architectural_decisions(project_id); CREATE INDEX idx_arch_decisions_status ON architectural_decisions(status); -- ================================================== -- ARCHITECTURE_DOCS (Auto-maintained documentation) -- ================================================== CREATE TABLE architecture_docs ( id SERIAL PRIMARY KEY, project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE, doc_type VARCHAR(100) NOT NULL, -- tech_stack, data_model, api_design, deployment content TEXT NOT NULL, version INTEGER DEFAULT 1, last_ai_update TIMESTAMP, last_manual_update TIMESTAMP, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), UNIQUE(project_id, doc_type) ); CREATE INDEX idx_arch_docs_project ON architecture_docs(project_id); CREATE INDEX idx_arch_docs_type ON architecture_docs(doc_type); -- ================================================== -- TASKS (Project tasks) -- ================================================== CREATE TABLE tasks ( id SERIAL PRIMARY KEY, project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE, title VARCHAR(500) NOT NULL, description TEXT, status VARCHAR(50) DEFAULT 'todo', -- todo, in_progress, review, done, blocked priority VARCHAR(50) DEFAULT 'medium', -- low, medium, high, urgent assigned_to INTEGER REFERENCES users(id) ON DELETE SET NULL, created_by INTEGER REFERENCES users(id) ON DELETE SET NULL, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), completed_at TIMESTAMP, identified_by_session_id INTEGER REFERENCES sessions(id) ON DELETE SET NULL, related_sessions JSONB DEFAULT '[]'::jsonb, metadata JSONB DEFAULT '{}'::jsonb ); CREATE INDEX idx_tasks_project ON tasks(project_id); CREATE INDEX idx_tasks_status ON tasks(status); CREATE INDEX idx_tasks_assigned ON tasks(assigned_to); -- ================================================== -- ANALYSES (replaces Firebase analyses collection) -- ================================================== CREATE TABLE analyses ( id SERIAL PRIMARY KEY, firebase_id VARCHAR(255), -- For migration project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE, type VARCHAR(50) NOT NULL, -- code, chatgpt, github, combined summary TEXT, tech_stack JSONB DEFAULT '[]'::jsonb, features JSONB DEFAULT '[]'::jsonb, raw_data JSONB DEFAULT '{}'::jsonb, created_at TIMESTAMP DEFAULT NOW() ); CREATE INDEX idx_analyses_project ON analyses(project_id); CREATE INDEX idx_analyses_type ON analyses(type); -- ================================================== -- API_KEYS (User API keys for extension) -- ================================================== CREATE TABLE api_keys ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, key_hash VARCHAR(255) UNIQUE NOT NULL, key_prefix VARCHAR(20), -- First few chars for display name VARCHAR(255), -- User-friendly name last_used TIMESTAMP, created_at TIMESTAMP DEFAULT NOW(), expires_at TIMESTAMP, revoked BOOLEAN DEFAULT false ); CREATE INDEX idx_api_keys_user ON api_keys(user_id); CREATE INDEX idx_api_keys_hash ON api_keys(key_hash); -- ================================================== -- USER_SESSIONS (Authentication sessions) -- ================================================== CREATE TABLE user_sessions ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, token VARCHAR(500) UNIQUE NOT NULL, refresh_token VARCHAR(500), created_at TIMESTAMP DEFAULT NOW(), expires_at TIMESTAMP, last_activity TIMESTAMP DEFAULT NOW(), ip_address VARCHAR(45), user_agent TEXT ); CREATE INDEX idx_user_sessions_token ON user_sessions(token); CREATE INDEX idx_user_sessions_user ON user_sessions(user_id); CREATE INDEX idx_user_sessions_expires ON user_sessions(expires_at); -- ================================================== -- KNOWLEDGE_ITEMS (Chat/Code context for AI) -- ================================================== CREATE TABLE knowledge_items ( id SERIAL PRIMARY KEY, project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE, item_type VARCHAR(50) NOT NULL, -- chat_history, code_file, github_issue, documentation source VARCHAR(100), -- cursor, chatgpt, github, manual title VARCHAR(500), content TEXT, metadata JSONB DEFAULT '{}'::jsonb, -- Status processed BOOLEAN DEFAULT false, chunk_count INTEGER DEFAULT 0, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); CREATE INDEX idx_knowledge_items_project ON knowledge_items(project_id); CREATE INDEX idx_knowledge_items_type ON knowledge_items(item_type); CREATE INDEX idx_knowledge_items_processed ON knowledge_items(processed); -- ================================================== -- ANALYTICS VIEWS -- ================================================== -- Project health overview CREATE OR REPLACE VIEW project_health AS SELECT p.id as project_id, p.name as project_name, p.status, p.current_phase, c.name as client_name, COUNT(DISTINCT s.id) as session_count, COUNT(DISTINCT s.user_id) as active_users, SUM(s.total_tokens) as total_tokens, SUM(s.estimated_cost_usd) as total_cost, COUNT(DISTINCT t.id) FILTER (WHERE t.status = 'done') as completed_tasks, COUNT(DISTINCT t.id) FILTER (WHERE t.status != 'done') as pending_tasks, MAX(s.last_updated) as last_activity FROM projects p LEFT JOIN clients c ON p.client_id = c.id LEFT JOIN sessions s ON s.project_id = p.id LEFT JOIN tasks t ON t.project_id = p.id GROUP BY p.id, p.name, p.status, p.current_phase, c.name; -- User activity summary CREATE OR REPLACE VIEW user_activity AS SELECT u.id as user_id, u.email, u.display_name, COUNT(DISTINCT s.id) as session_count, COUNT(DISTINCT s.project_id) as projects_worked_on, SUM(s.total_tokens) as total_tokens, SUM(s.estimated_cost_usd) as total_cost, MAX(s.last_updated) as last_activity FROM users u LEFT JOIN sessions s ON s.user_id = u.id GROUP BY u.id, u.email, u.display_name; -- Organization billing summary CREATE OR REPLACE VIEW organization_billing AS SELECT c.id as client_id, c.name as organization_name, c.subscription_tier, COUNT(DISTINCT u.id) as total_users, COUNT(DISTINCT p.id) as total_projects, COUNT(DISTINCT s.id) as total_sessions, SUM(s.total_tokens) as total_tokens, SUM(s.estimated_cost_usd) as total_cost, SUM(s.estimated_cost_usd) FILTER ( WHERE s.started_at >= DATE_TRUNC('month', CURRENT_DATE) ) as current_month_cost FROM clients c LEFT JOIN projects p ON p.client_id = c.id LEFT JOIN sessions s ON s.project_id = p.id LEFT JOIN users u ON u.id = c.owner_user_id OR EXISTS ( SELECT 1 FROM project_contributors pc WHERE pc.project_id = p.id AND pc.user_id = u.id ) GROUP BY c.id, c.name, c.subscription_tier; -- ================================================== -- HELPER FUNCTIONS -- ================================================== -- Auto-update updated_at timestamps CREATE OR REPLACE FUNCTION update_updated_at_column() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ language 'plpgsql'; -- Apply to tables CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER update_clients_updated_at BEFORE UPDATE ON clients FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER update_projects_updated_at BEFORE UPDATE ON projects FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER update_sessions_updated_at BEFORE UPDATE ON sessions FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -- ================================================== -- SEED DATA (Optional) -- ================================================== -- Create default user (you) INSERT INTO users (uid, email, display_name, workspace) VALUES ('mark-admin-001', 'mark@getacquired.com', 'Mark Henderson', 'marks-account') ON CONFLICT (email) DO NOTHING; -- Create default client (your organization) INSERT INTO clients (name, slug, owner_user_id, subscription_tier) VALUES ('VIBN', 'vibn', 1, 'enterprise') ON CONFLICT (slug) DO NOTHING; COMMENT ON TABLE users IS 'VIBN users (developers, team members)'; COMMENT ON TABLE clients IS 'Organizations/Companies using VIBN'; COMMENT ON TABLE projects IS 'Development projects under organizations'; COMMENT ON TABLE sessions IS 'AI coding sessions with full conversation logs'; COMMENT ON TABLE work_completed IS 'AI-extracted accomplishments from sessions'; COMMENT ON TABLE architectural_decisions IS 'Design decisions made during development';