-- Ported from the SQLite migration of the same number. Same version, same -- meaning, different spelling: SQLite's storage classes and PostgreSQL's types -- do not line up, and one shared file would be a promise neither could keep. -- -- Times are milliseconds since the epoch in BIGINT, exactly as in SQLite, -- rather than TIMESTAMPTZ. The Go code reads and writes int64 milliseconds -- everywhere; converting here would mean two time representations in one -- codebase, and the conversion would be the thing that is wrong at 2am. -- A key has to reach the person it was issued for, and a key pasted into a chat -- window stays in that chat window forever. A claim is a one-time link instead: -- the operator sends the link, the person opens it once, and the link dies. -- -- The key is not stored here in the clear. It is sealed with a key derived from -- the claim token, and the token is never written down — only its hash is. So a -- database leak yields two useless hashes and a blob nothing in the database can -- open, which keeps the property the api_keys table has. CREATE TABLE key_claims ( token_hash BYTEA PRIMARY KEY, key_id TEXT NOT NULL REFERENCES api_keys(id), -- The sealed key. Emptied the moment it is claimed, so a claimed link leaves -- nothing behind even before the row is purged. sealed BYTEA NOT NULL, created_at BIGINT NOT NULL, expires_at BIGINT NOT NULL, claimed_at BIGINT ); CREATE INDEX key_claims_by_key ON key_claims(key_id); CREATE INDEX key_claims_by_expiry ON key_claims(expires_at);