CREATE TABLE IF NOT EXISTS gateway.prompt_categories ( id uuid PRIMARY KEY, name text NOT NULL UNIQUE, description text NOT NULL DEFAULT '', created_at timestamptz NOT NULL DEFAULT clock_timestamp() ); CREATE TABLE IF NOT EXISTS gateway.prompt_templates ( id uuid PRIMARY KEY, name text NOT NULL UNIQUE, description text NOT NULL DEFAULT '', category_id uuid REFERENCES gateway.prompt_categories(id) ON DELETE SET NULL, tags text[] NOT NULL DEFAULT '{}', department_ids uuid[] NOT NULL DEFAULT '{}', enabled boolean NOT NULL DEFAULT true, current_version integer, revision bigint NOT NULL DEFAULT 1, created_by uuid REFERENCES gateway.admin_accounts(id), created_at timestamptz NOT NULL DEFAULT clock_timestamp(), updated_at timestamptz NOT NULL DEFAULT clock_timestamp() ); CREATE TABLE IF NOT EXISTS gateway.prompt_versions ( id uuid PRIMARY KEY, template_id uuid NOT NULL REFERENCES gateway.prompt_templates(id) ON DELETE CASCADE, version integer NOT NULL CHECK (version > 0), content text NOT NULL CHECK (length(content) BETWEEN 1 AND 100000), variables jsonb NOT NULL DEFAULT '[]'::jsonb CHECK (jsonb_typeof(variables) = 'array'), change_note text NOT NULL DEFAULT '', created_by uuid REFERENCES gateway.admin_accounts(id), created_at timestamptz NOT NULL DEFAULT clock_timestamp(), UNIQUE (template_id, version) ); ALTER TABLE gateway.prompt_templates ADD CONSTRAINT prompt_templates_current_version_fk FOREIGN KEY (id, current_version) REFERENCES gateway.prompt_versions(template_id, version) DEFERRABLE INITIALLY DEFERRED; CREATE INDEX IF NOT EXISTS prompt_templates_visible_idx ON gateway.prompt_templates (enabled, updated_at DESC); CREATE TABLE IF NOT EXISTS gateway.knowledge_bases ( id uuid PRIMARY KEY, name text NOT NULL UNIQUE, description text NOT NULL DEFAULT '', retrieval_mode text NOT NULL DEFAULT 'postgres_fts' CHECK (retrieval_mode IN ('postgres_fts')), chunk_size integer NOT NULL DEFAULT 800 CHECK (chunk_size BETWEEN 200 AND 8000), chunk_overlap integer NOT NULL DEFAULT 100 CHECK (chunk_overlap >= 0 AND chunk_overlap <= chunk_size / 2), department_ids uuid[] NOT NULL DEFAULT '{}', enabled boolean NOT NULL DEFAULT true, revision bigint NOT NULL DEFAULT 1, created_by uuid REFERENCES gateway.admin_accounts(id), created_at timestamptz NOT NULL DEFAULT clock_timestamp(), updated_at timestamptz NOT NULL DEFAULT clock_timestamp() ); CREATE TABLE IF NOT EXISTS gateway.knowledge_documents ( id uuid PRIMARY KEY, knowledge_base_id uuid NOT NULL REFERENCES gateway.knowledge_bases(id) ON DELETE CASCADE, title text NOT NULL, source_type text NOT NULL DEFAULT 'text' CHECK (source_type IN ('text', 'url', 'import')), source_uri text NOT NULL DEFAULT '', content text NOT NULL CHECK (octet_length(content) <= 2097152), content_sha256 char(64) NOT NULL, status text NOT NULL DEFAULT 'ready' CHECK (status IN ('ready', 'failed')), status_message text NOT NULL DEFAULT '', chunk_count integer NOT NULL DEFAULT 0 CHECK (chunk_count >= 0), created_by uuid REFERENCES gateway.admin_accounts(id), created_at timestamptz NOT NULL DEFAULT clock_timestamp(), updated_at timestamptz NOT NULL DEFAULT clock_timestamp(), UNIQUE (knowledge_base_id, content_sha256) ); CREATE TABLE IF NOT EXISTS gateway.knowledge_chunks ( id uuid PRIMARY KEY, knowledge_base_id uuid NOT NULL REFERENCES gateway.knowledge_bases(id) ON DELETE CASCADE, document_id uuid NOT NULL REFERENCES gateway.knowledge_documents(id) ON DELETE CASCADE, chunk_index integer NOT NULL CHECK (chunk_index >= 0), content text NOT NULL, search_vector tsvector GENERATED ALWAYS AS (to_tsvector('simple', content)) STORED, created_at timestamptz NOT NULL DEFAULT clock_timestamp(), UNIQUE (document_id, chunk_index) ); CREATE INDEX IF NOT EXISTS knowledge_chunks_search_idx ON gateway.knowledge_chunks USING gin (search_vector); CREATE INDEX IF NOT EXISTS knowledge_chunks_base_idx ON gateway.knowledge_chunks (knowledge_base_id, document_id, chunk_index); CREATE TABLE IF NOT EXISTS gateway.tool_definitions ( id uuid PRIMARY KEY, code text NOT NULL UNIQUE CHECK (code ~ '^[a-z][a-z0-9_-]{1,63}$'), name text NOT NULL, description text NOT NULL DEFAULT '', endpoint_url text NOT NULL, http_method text NOT NULL DEFAULT 'POST' CHECK (http_method IN ('GET','POST','PUT','PATCH','DELETE')), encrypted_headers bytea NOT NULL DEFAULT ''::bytea, headers_kek_version integer NOT NULL DEFAULT 1, input_schema jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(input_schema) = 'object'), timeout_seconds integer NOT NULL DEFAULT 15 CHECK (timeout_seconds BETWEEN 1 AND 120), department_ids uuid[] NOT NULL DEFAULT '{}', enabled boolean NOT NULL DEFAULT true, revision bigint NOT NULL DEFAULT 1, created_by uuid REFERENCES gateway.admin_accounts(id), created_at timestamptz NOT NULL DEFAULT clock_timestamp(), updated_at timestamptz NOT NULL DEFAULT clock_timestamp() ); CREATE TABLE IF NOT EXISTS gateway.applications ( id uuid PRIMARY KEY, code text NOT NULL UNIQUE CHECK (code ~ '^[a-z][a-z0-9_-]{2,63}$'), name text NOT NULL, description text NOT NULL DEFAULT '', department_ids uuid[] NOT NULL DEFAULT '{}', status text NOT NULL DEFAULT 'draft' CHECK (status IN ('draft','active','suspended','retired')), draft_config jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(draft_config) = 'object'), published_version integer CHECK (published_version > 0), revision bigint NOT NULL DEFAULT 1, created_by uuid REFERENCES gateway.admin_accounts(id), created_at timestamptz NOT NULL DEFAULT clock_timestamp(), updated_at timestamptz NOT NULL DEFAULT clock_timestamp() ); CREATE TABLE IF NOT EXISTS gateway.tool_runs ( id uuid PRIMARY KEY, tool_id uuid NOT NULL REFERENCES gateway.tool_definitions(id) ON DELETE CASCADE, api_key_id uuid REFERENCES gateway.api_keys(id) ON DELETE SET NULL, request_id text NOT NULL, status text NOT NULL CHECK (status IN ('success','error')), response_status integer, latency_ms bigint NOT NULL DEFAULT 0, error text NOT NULL DEFAULT '', created_at timestamptz NOT NULL DEFAULT clock_timestamp() ); CREATE INDEX IF NOT EXISTS tool_runs_tool_time_idx ON gateway.tool_runs (tool_id, created_at DESC); CREATE TABLE IF NOT EXISTS gateway.application_versions ( id uuid PRIMARY KEY, application_id uuid NOT NULL REFERENCES gateway.applications(id) ON DELETE CASCADE, version integer NOT NULL CHECK (version > 0), config jsonb NOT NULL CHECK (jsonb_typeof(config) = 'object'), change_note text NOT NULL DEFAULT '', published_by uuid REFERENCES gateway.admin_accounts(id), created_at timestamptz NOT NULL DEFAULT clock_timestamp(), UNIQUE (application_id, version) ); ALTER TABLE gateway.applications ADD CONSTRAINT applications_published_version_fk FOREIGN KEY (id, published_version) REFERENCES gateway.application_versions(application_id, version) DEFERRABLE INITIALLY DEFERRED; CREATE TABLE IF NOT EXISTS gateway.application_runs ( id uuid PRIMARY KEY, application_id uuid NOT NULL REFERENCES gateway.applications(id) ON DELETE CASCADE, version integer NOT NULL, api_key_id uuid REFERENCES gateway.api_keys(id) ON DELETE SET NULL, request_id text NOT NULL, status text NOT NULL CHECK (status IN ('success','error')), latency_ms bigint NOT NULL DEFAULT 0, retrieval_count integer NOT NULL DEFAULT 0, tool_count integer NOT NULL DEFAULT 0, error text NOT NULL DEFAULT '', created_at timestamptz NOT NULL DEFAULT clock_timestamp() ); CREATE INDEX IF NOT EXISTS application_runs_app_time_idx ON gateway.application_runs (application_id, created_at DESC); CREATE TABLE IF NOT EXISTS gateway.notification_channels ( id uuid PRIMARY KEY, name text NOT NULL UNIQUE, webhook_url text NOT NULL, encrypted_signing_secret bytea NOT NULL DEFAULT ''::bytea, signing_secret_kek_version integer NOT NULL DEFAULT 1, event_patterns text[] NOT NULL DEFAULT '{}', enabled boolean NOT NULL DEFAULT true, revision bigint NOT NULL DEFAULT 1, created_by uuid REFERENCES gateway.admin_accounts(id), created_at timestamptz NOT NULL DEFAULT clock_timestamp(), updated_at timestamptz NOT NULL DEFAULT clock_timestamp() ); CREATE TABLE IF NOT EXISTS gateway.notification_deliveries ( id uuid PRIMARY KEY, channel_id uuid NOT NULL REFERENCES gateway.notification_channels(id) ON DELETE CASCADE, event_id uuid NOT NULL, event_type text NOT NULL, payload jsonb NOT NULL DEFAULT '{}'::jsonb, status text NOT NULL CHECK (status IN ('pending','delivered','failed')), attempts integer NOT NULL DEFAULT 0, response_status integer, last_error text NOT NULL DEFAULT '', delivered_at timestamptz, created_at timestamptz NOT NULL DEFAULT clock_timestamp(), updated_at timestamptz NOT NULL DEFAULT clock_timestamp(), UNIQUE (channel_id, event_id) ); CREATE INDEX IF NOT EXISTS notification_deliveries_status_idx ON gateway.notification_deliveries (status, updated_at DESC); COMMENT ON TABLE gateway.knowledge_documents IS 'Baseline stores bounded extracted text in PostgreSQL; blob/object storage is deliberately not required.'; COMMENT ON TABLE gateway.application_versions IS 'Immutable published application compositions referencing prompt, knowledge and tool assets.';