-- Idempotency key for /join, supplied by the client. -- -- The failure this closes (H16) is the single most likely failure of the evening, on step one -- of the product. `/join` commits the user row AND the bcrypt hash of the PIN, but the PLAINTEXT -- PIN exists nowhere except the HTTP response body. So: -- -- 1. guest scans the QR in the venue car park, taps "Beitreten" -- 2. the server creates the account and hashes the PIN -- 3. the response is lost on the way back — the 5G-to-nothing transition every wedding venue -- has, or the AP handing off -- 4. the client retries; the name is now taken, so it 409s -- 5. the client shows a PIN entry form for a PIN THAT WAS NEVER DISPLAYED -- -- The guest is locked out of their own brand-new account, and the only recovery is finding a -- host with a dashboard open. `/upload` already solved exactly this with `client_upload_id`; -- join never got the same treatment. -- -- With a key, a retry is recognised as the same join and answered with a usable PIN. We do NOT -- store the plaintext to replay it — see the handler: a retry ROTATES the PIN. That is sound -- precisely because the original was never shown to anybody, so there is nothing to preserve, -- and it keeps this table free of recoverable credentials. ALTER TABLE "user" ADD COLUMN client_join_id UUID; -- Partial, for the same reasons as `upload_client_upload_id_key`: index only the rows that -- carry a key, and state the rule exactly. NULL is allowed and unconstrained, so any client -- that does not send one (and every row that predates this column) behaves exactly as before. -- -- Scoped per event as well as per key. The key is a client-generated v4 UUID so a cross-event -- collision is not realistic, but a reused install genuinely has two events in one table and -- "this join belongs to that event" is the property we actually mean. CREATE UNIQUE INDEX user_client_join_id_key ON "user" (event_id, client_join_id) WHERE client_join_id IS NOT NULL;