-- v1.1.9: durable per-app named queues (queue::*). -- -- Producer: queue::enqueue(name, msg) — INSERTs into this table. -- Consumer: a registered queue:receive trigger fires when a message is -- available; the dispatcher claims with FOR UPDATE SKIP LOCKED + a -- visibility-timeout window. -- -- "The queue table IS the outbox" — there is no double-buffering. The -- dispatcher's queue arm claims directly from queue_messages; the -- visibility-timeout reclaim task resets stale claims so crashed -- consumers don't lose work. -- -- Identity tuple: (app_id, queue_name). Queue names are implicit — no -- queue registry table; a queue exists once the first message is -- enqueued under that name. CREATE TABLE queue_messages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), app_id UUID NOT NULL REFERENCES apps(id) ON DELETE CASCADE, queue_name TEXT NOT NULL, payload JSONB NOT NULL, enqueued_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- deliver_after: NULL = immediate; else dispatcher won't claim until NOW() >= deliver_after. deliver_after TIMESTAMPTZ NULL, -- claim_token: NULL = unclaimed; UUID = currently leased by a dispatcher. claim_token UUID NULL, claimed_at TIMESTAMPTZ NULL, attempt INT NOT NULL DEFAULT 0, max_attempts INT NOT NULL DEFAULT 3, -- Forensic: who enqueued. NEVER used for authz — the trigger fires -- as the principal that REGISTERED the consumer (design notes §4). enqueued_by_principal UUID NULL ); -- Dispatch hot path: scan for unclaimed messages in (app_id, queue_name) -- order by enqueued_at. The (deliver_after IS NULL OR deliver_after <= NOW()) -- condition cannot live in the partial WHERE (NOW() is non-immutable); -- Postgres applies it as a filter on the partial result set, which is -- already small. CREATE INDEX idx_queue_messages_dispatch ON queue_messages (app_id, queue_name, enqueued_at) WHERE claim_token IS NULL; -- depth() / depth_pending() helpers and the dashboard's queue list view. CREATE INDEX idx_queue_messages_app_queue ON queue_messages (app_id, queue_name); -- Reclaim-task scan: find currently-leased messages whose claim is older -- than the per-queue visibility_timeout_secs. Bounded by the number of -- in-flight messages, which is small. CREATE INDEX idx_queue_messages_claimed ON queue_messages (claimed_at) WHERE claim_token IS NOT NULL;