CREATE EXTENSION IF NOT EXISTS pgcrypto; CREATE TABLE IF NOT EXISTS schema_migrations ( version text PRIMARY KEY, applied_at timestamptz NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS organizations ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), slug text NOT NULL UNIQUE, name text NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS users ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), organization_id uuid NOT NULL REFERENCES organizations(id), username text NOT NULL, password_hash text NOT NULL, role text NOT NULL DEFAULT 'admin' CHECK (role = 'admin'), is_active boolean NOT NULL DEFAULT true, failed_login_count integer NOT NULL DEFAULT 0, locked_until timestamptz, last_login_at timestamptz, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), UNIQUE (organization_id, username) ); CREATE TABLE IF NOT EXISTS sessions ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, token_hash bytea NOT NULL UNIQUE, csrf_token_hash bytea NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), last_seen_at timestamptz NOT NULL DEFAULT now(), expires_at timestamptz NOT NULL, idle_expires_at timestamptz NOT NULL, revoked_at timestamptz, ip_address inet, user_agent text ); CREATE INDEX IF NOT EXISTS sessions_active_idx ON sessions (token_hash, expires_at, idle_expires_at) WHERE revoked_at IS NULL; CREATE TABLE IF NOT EXISTS tasks ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), organization_id uuid NOT NULL REFERENCES organizations(id), task_id text NOT NULL, original_text_ciphertext text, operation_ciphertext text, parse_response_ciphertext text, execution_result_ciphertext text, operation jsonb, parse_response jsonb, summary jsonb, status text NOT NULL, stage text NOT NULL, message text, error text, handoff_status text, execution_result jsonb, created_by uuid REFERENCES users(id), confirmed_by uuid REFERENCES users(id), confirmed_at timestamptz, idempotency_key text, lease_owner text, lease_expires_at timestamptz, last_event_id bigint, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), UNIQUE (organization_id, task_id), UNIQUE (organization_id, idempotency_key) ); CREATE INDEX IF NOT EXISTS tasks_queue_idx ON tasks (organization_id, status, lease_expires_at, created_at); CREATE TABLE IF NOT EXISTS task_events ( id bigserial PRIMARY KEY, organization_id uuid NOT NULL REFERENCES organizations(id), task_id uuid NOT NULL REFERENCES tasks(id) ON DELETE CASCADE, status text NOT NULL, stage text NOT NULL, message text, payload jsonb, actor_user_id uuid REFERENCES users(id), created_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS task_events_task_idx ON task_events (task_id, id); CREATE TABLE IF NOT EXISTS task_attempts ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), organization_id uuid NOT NULL REFERENCES organizations(id), task_id uuid NOT NULL REFERENCES tasks(id) ON DELETE CASCADE, attempt_no integer NOT NULL, phase text NOT NULL, status text NOT NULL, request_hash text, response_hash text, error_code text, details jsonb, started_at timestamptz NOT NULL DEFAULT now(), finished_at timestamptz, UNIQUE (task_id, attempt_no, phase) ); CREATE TABLE IF NOT EXISTS idempotency_keys ( id bigserial PRIMARY KEY, organization_id uuid NOT NULL REFERENCES organizations(id), scope text NOT NULL, idempotency_key text NOT NULL, request_hash text NOT NULL, task_id uuid REFERENCES tasks(id) ON DELETE CASCADE, created_at timestamptz NOT NULL DEFAULT now(), expires_at timestamptz, UNIQUE (organization_id, scope, idempotency_key) ); CREATE TABLE IF NOT EXISTS audit_events ( id bigserial PRIMARY KEY, organization_id uuid NOT NULL REFERENCES organizations(id), actor_user_id uuid REFERENCES users(id), event_type text NOT NULL, entity_type text NOT NULL, entity_id text, request_id text, metadata jsonb, created_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS audit_events_created_idx ON audit_events (organization_id, created_at DESC); CREATE TABLE IF NOT EXISTS browser_connections ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), organization_id uuid NOT NULL REFERENCES organizations(id), user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, connection_id text NOT NULL, extension_version text, status text NOT NULL DEFAULT 'connected', last_seen_at timestamptz NOT NULL DEFAULT now(), metadata jsonb, UNIQUE (organization_id, connection_id) ); CREATE TABLE IF NOT EXISTS outbox_events ( id bigserial PRIMARY KEY, organization_id uuid NOT NULL REFERENCES organizations(id), topic text NOT NULL, aggregate_type text NOT NULL, aggregate_id text NOT NULL, payload jsonb NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), published_at timestamptz ); CREATE INDEX IF NOT EXISTS outbox_pending_idx ON outbox_events (organization_id, created_at) WHERE published_at IS NULL;