-- Chat Tables Migration -- Creates tables for chatbot functionality with visitor sessions and message history -- Version: 001 -- Date: 2026-01-25 -- ============================================================================ -- Table: chat_sessions -- Stores chatbot session information for visitors -- ============================================================================ CREATE TABLE IF NOT EXISTS chat_sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, visitor_id TEXT NOT NULL, metadata JSONB DEFAULT '{}'::jsonb, updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL ); -- Index for quick lookup by visitor_id (used for session continuity) CREATE INDEX IF NOT EXISTS idx_chat_sessions_visitor_id ON chat_sessions(visitor_id); -- Index for ordering sessions by creation time CREATE INDEX IF NOT EXISTS idx_chat_sessions_created_at ON chat_sessions(created_at DESC); -- Comment on table COMMENT ON TABLE chat_sessions IS 'Stores chatbot sessions for website visitors'; COMMENT ON COLUMN chat_sessions.visitor_id IS 'Unique identifier for the visitor (generated client-side or from fingerprint)'; COMMENT ON COLUMN chat_sessions.metadata IS 'Additional session data (locale, user_agent, referrer, etc.)'; -- ============================================================================ -- Table: chat_messages -- Stores individual chat messages within a session -- ============================================================================ CREATE TABLE IF NOT EXISTS chat_messages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), session_id UUID NOT NULL REFERENCES chat_sessions(id) ON DELETE CASCADE, role TEXT NOT NULL CHECK (role IN ('user', 'assistant', 'system')), content TEXT NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL ); -- Index for fetching messages by session (primary query pattern) CREATE INDEX IF NOT EXISTS idx_chat_messages_session_id ON chat_messages(session_id); -- Composite index for fetching messages in order within a session CREATE INDEX IF NOT EXISTS idx_chat_messages_session_created ON chat_messages(session_id, created_at ASC); -- Comment on table COMMENT ON TABLE chat_messages IS 'Stores individual messages in chatbot conversations'; COMMENT ON COLUMN chat_messages.role IS 'Message sender role: user, assistant (AI), or system (instructions)'; COMMENT ON COLUMN chat_messages.content IS 'The message text content'; -- ============================================================================ -- Updated At Trigger for chat_sessions -- Automatically updates the updated_at column when a session is modified -- ============================================================================ CREATE OR REPLACE FUNCTION update_updated_at_column() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER update_chat_sessions_updated_at BEFORE UPDATE ON chat_sessions FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -- ============================================================================ -- Row Level Security (RLS) -- Enabled for production security - server uses service role key to bypass -- ============================================================================ -- Enable RLS on chat_sessions ALTER TABLE chat_sessions ENABLE ROW LEVEL SECURITY; -- Enable RLS on chat_messages ALTER TABLE chat_messages ENABLE ROW LEVEL SECURITY; -- ============================================================================ -- RLS Policies for chat_sessions -- ============================================================================ -- Policy: Allow service role full access (for server-side API routes) -- Note: Service role key automatically bypasses RLS, but this is explicit CREATE POLICY "Service role has full access to chat_sessions" ON chat_sessions FOR ALL TO service_role USING (true) WITH CHECK (true); -- Policy: Allow visitors to read their own sessions (for potential future client-side access) CREATE POLICY "Visitors can read their own sessions" ON chat_sessions FOR SELECT TO anon, authenticated USING (visitor_id = current_setting('request.headers')::json->>'x-visitor-id'); -- Policy: Allow visitors to create sessions (for potential future client-side access) CREATE POLICY "Visitors can create sessions" ON chat_sessions FOR INSERT TO anon, authenticated WITH CHECK (visitor_id = current_setting('request.headers')::json->>'x-visitor-id'); -- ============================================================================ -- RLS Policies for chat_messages -- ============================================================================ -- Policy: Allow service role full access (for server-side API routes) CREATE POLICY "Service role has full access to chat_messages" ON chat_messages FOR ALL TO service_role USING (true) WITH CHECK (true); -- Policy: Allow visitors to read messages from their sessions CREATE POLICY "Visitors can read messages from their sessions" ON chat_messages FOR SELECT TO anon, authenticated USING ( EXISTS ( SELECT 1 FROM chat_sessions WHERE chat_sessions.id = chat_messages.session_id AND chat_sessions.visitor_id = current_setting('request.headers')::json->>'x-visitor-id' ) ); -- Policy: Allow visitors to insert messages into their sessions CREATE POLICY "Visitors can insert messages into their sessions" ON chat_messages FOR INSERT TO anon, authenticated WITH CHECK ( EXISTS ( SELECT 1 FROM chat_sessions WHERE chat_sessions.id = chat_messages.session_id AND chat_sessions.visitor_id = current_setting('request.headers')::json->>'x-visitor-id' ) ); -- ============================================================================ -- Grants -- ============================================================================ -- Grant usage on tables to authenticated and anon roles GRANT SELECT, INSERT ON chat_sessions TO anon, authenticated; GRANT SELECT, INSERT ON chat_messages TO anon, authenticated; -- Grant all to service role (for server-side operations) GRANT ALL ON chat_sessions TO service_role; GRANT ALL ON chat_messages TO service_role;