Re-authorising an account revokes the connection before it
Disconnecting inside ChatGPT tells us nothing. The only signal we ever get that a connection is over is the person making a new one, so making a new one has to end the old one.
Until recently the table behind a connection recorded a display name somebody typed and nothing else. Connect PersistMemory to ChatGPT three times and you had three rows called ChatGPT. They were indistinguishable, which meant neither of the two questions a person actually asks could be answered: which of my accounts is connected, and is this the same connection I made before.
Two columns answer both. A key now records the client it was issued to, which is one of chatgpt, claude, cursor or api, and the account inside that client that authorised it, which is normally an email address. Both are nullable. Every key issued before the change has no client, and backfilling a guess would be a claim about history that nobody can check.
The signal we never get
The interesting part is not the columns, it is what they made possible. A connection is a bearer credential we hand to a client we cannot see. When someone removes the connector inside ChatGPT, nothing reaches us. There is no callback, no revocation channel, no heartbeat that stops. The key we issued keeps working, and it will keep working for as long as the row exists.
So a credential outlives the client that held it, and it outlives it invisibly. The dashboard shows a live connection to a client the person stopped using months ago. Worse, from the user's side there is now a working key to their memory store in a place they believe they closed, and nothing in the product ever contradicts that belief. The only signal we ever reliably get that a connection is finished is the person making a new one.
So making a new one ends the old one
Connecting a platform revokes whatever that platform had before — whichever account it belonged to — in the same transaction as the insert. Not afterwards, and not on a sweep.
// Connecting a PLATFORM revokes whatever that platform had before —
// whichever account it belonged to. Keyed on the client alone: matching
// the account too let one person hold three live ChatGPT connections.
if (key.client) {
await executor
.update(apiKeys)
.set({ revokedAt: new Date() })
.where(
and(
eq(apiKeys.userId, key.userId),
eq(apiKeys.client, key.client),
isNull(apiKeys.revokedAt)
)
);
}Doing it in the transaction matters because the alternative orders are both wrong. Revoke first and commit, and a failed insert leaves the person with no connection at all after an action that was supposed to give them one. Insert first and revoke later, and there is a window with two live keys, which is exactly the state the change exists to prevent.
The index is a backstop, and it is partial
Application code that revokes is a mechanism. An invariant needs something that does not depend on every future code path remembering.
-- ONE live key per account per client.
--
-- Partial on `revoked_at IS NULL`, which is what makes re-authorising work:
-- revoking the old row takes it out of the index, so the new one inserts.
CREATE UNIQUE INDEX IF NOT EXISTS "api_keys_live_client_account_idx"
ON "api_keys" ("user_id", "client", "client_account")
WHERE "revoked_at" IS NULL
AND "client" IS NOT NULL
AND "client_account" IS NOT NULL;Partial rather than total, on purpose. A total unique index over the same three columns would refuse the second connection outright, and the person reconnecting ChatGPT would be told their own account is already in use by themselves. The partial predicate makes revocation the thing that frees the slot, so the replacement is a legal insert and a forgotten revocation is a constraint violation rather than a second live credential.
It is also deliberately not the mechanism. The service revokes explicitly, so the old key stops working at a moment we choose and can log. The index exists so that a path which forgets fails loudly instead of leaving two keys behind for a year.
What replacement is not
Replacement is scoped to one client and one account, and the scope is the whole design. Verified against the live database: three connections held at once, ChatGPT with a work address, ChatGPT with a personal address, and Claude with the same work address. Re-authorising the ChatGPT work connection revoked exactly that one row. The personal ChatGPT connection and Claude's connection to the same address both kept working.
That is the behaviour a person can predict without reading anything. Connecting an account you have already connected replaces that connection. It does not sign you out of your other assistant, and it does not touch your other account in the same assistant. Anything broader would make reconnecting a risky act, and an action people are afraid to take is one they will avoid, which puts us back where we started with credentials nobody ever retires.