From f376deed874fdf02ad9d0dda86cf80b30a9c52f7 Mon Sep 17 00:00:00 2001 From: Nicu Chiciuc Date: Fri, 30 Oct 2026 12:03:40 +0200 Subject: [PATCH 1/4] Apps register through the Convex credential (broker 0.7.0) Co-Authored-By: Claude Opus 5.5 --- README.md | 51 +++++++++++++++++++++++++--------------- convex/identityBroker.ts | 11 +++------ 2 files changed, 35 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index f6f8b13..2de6ef6 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,8 @@ The hub answers at the deployment's `.convex.site` URL and at the custom domain The stack sets `ISSUER` to that domain: the issuer is the name every app trusts, in discovery, in every `id_token`, and in the callback URLs registered with GitHub and Google. Changing it later means changing `HUB_ISSUER` on every app's deployments and the callback URLs at every provider in -one go; the app ids, the clients and the salt stay, so the subjects do not change. +one go; the app ids, the clients and the salt stay, so the subjects do not change. An app reads the +issuer from the reply of its registration, so it needs no setting of its own. ## Connectors @@ -79,21 +80,31 @@ adds email codes, with a sender behind it and the abuse controls a public mail e ## Register an app -```sh -npx convex run --prod identityBroker:createApp '{"name":"Bristol","firstParty":true,"allowedCallbackHosts":["*.convex.site","127.0.0.1"]}' -npx convex run --prod identityBroker:mintRegistrationToken '{"appId":"bristol-ab12c","expiresInDays":3650}' +An app needs no step on the hub. Each deployment registers itself through the `Convex.HubClient` +resource of `@samebase/alchemy-convex` in its Alchemy stack, which also writes the client into the +deployment's variables: + +```ts +const client = + yield * + Convex.HubClient("HubClient", { + team, + app: "bristol", + redirectUri: `${siteUrl}/oauth/hub/callback`, + deploymentType: "preview", + clientName: stage, + }); ``` -`firstParty: true` sends the user straight to the connector without the chooser: it is for your own -apps. `allowedCallbackHosts` is where that app's deployments may live; `*.convex.site` admits every -Convex deployment, so narrow it to a domain when the app has one. Narrowing later does not change -clients that already exist. A token registers any number of clients for its app until it expires (30 -days unless `expiresInDays` says otherwise), so CI holds one long-lived token per app. Deleting an -app removes its tokens and clients; recreating it gives every user a new subject. +The resource finds this base by the name of its project, and it runs `identityBroker:registerClient` +with a short-lived key that the Convex credential of the stack mints. So the app has no token of its +own. The first client of an `app` creates the app as a first-party app, which sends the user +straight to the connector without the chooser. For an app that is not the team's own, run +`createApp` with `firstParty` false first. A development deployment has no stack: the command +`alchemy-convex hub-client` does the same with the Convex CLI login. Deleting an app removes its +clients. Recreating it with the same id keeps the subjects of its users. -Give the token to the app's CI as a secret. Each deployment of the app registers itself with it -through the `IdentityHub.Client` resource of `@samebase/alchemy-identity-hub` in its Alchemy stack, -which also writes the client into the deployment's variables; see the component's +See the component's [consumer guide](https://github.com/samebase/identity-broker/blob/main/docs/consumers.md). ## Operate @@ -101,12 +112,14 @@ which also writes the client into the deployment's variables; see the component' The admin functions are internal: `npx convex run --prod identityBroker:`, from a checkout that selects the project `samebase`. The deploy key of the base is an administrator credential: it can read and replace the keys, read every secret in the Alchemy state of the team, and run every -internal function. Keep it out of every app's CI. The Convex CLI ignores `--prod` when -`CONVEX_DEPLOY_KEY` names another deployment, so unset it before an admin command. A cron runs the -broker's cleanup daily: expired requests and codes, and sign-in records older than 90 days. Idle -preview clients are kept: the broker prunes them only when the host sets `previewClientIdleDays`, -and this hub does not, because a consumer's registration resource does not register a pruned client -again. +internal function. Every stack of the team mints keys of the base with the team's Convex token, for +its state and for its hub client. These keys can run internal functions only, and they end with the +run. The team token itself reaches the base, so treat it as an administrator credential too. The +Convex CLI ignores `--prod` when `CONVEX_DEPLOY_KEY` names another deployment, so unset it before an +admin command. A cron runs the broker's cleanup daily: expired requests and codes, and sign-in +records older than 90 days. Idle preview clients are kept: the broker prunes them only when the host +sets `previewClientIdleDays`, and this hub does not, because a consumer's `Convex.HubClient` does +not register a pruned client again. Never run `npx convex import --replace-all` on this deployment, also not with `--component`: it also deletes the rows of the other components. To restore the tables of one component, import a ZIP of diff --git a/convex/identityBroker.ts b/convex/identityBroker.ts index bb5e5c2..ee16359 100644 --- a/convex/identityBroker.ts +++ b/convex/identityBroker.ts @@ -1,17 +1,12 @@ import { broker } from "./http"; // Administration for `npx convex run identityBroker:`: -// createApp '{"name":"Bristol","firstParty":true,"allowedCallbackHosts":["*.convex.site","127.0.0.1"]}' -// mintRegistrationToken '{"appId":"bristol-ab12c","expiresInDays":3650}' -// listRegistrationTokens '{"appId":"..."}', revokeRegistrationToken '{"tokenHash":"..."}' -// setAllowedCallbackHosts '{"appId":"...","allowedCallbackHosts":["*.convex.site","app.example.com"]}' +// createApp '{"name":"Bristol","firstParty":false}', for an app that is not the team's own +// registerClient, which Convex.HubClient of @samebase/alchemy-convex runs at deploy time // listApps, listClients '{"appId":"..."}', deleteClient '{"clientId":"..."}', deleteApp '{"appId":"..."}' export const { createApp, - mintRegistrationToken, - listRegistrationTokens, - revokeRegistrationToken, - setAllowedCallbackHosts, + registerClient, deleteApp, listApps, listClients, From 951af75de7852bd5f593a99c24aec2e9c7468a34 Mon Sep 17 00:00:00 2001 From: Nicu Chiciuc Date: Fri, 30 Oct 2026 12:10:19 +0200 Subject: [PATCH 2/4] Keep the token functions until the apps have moved Co-Authored-By: Claude Opus 5.5 --- convex/identityBroker.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/convex/identityBroker.ts b/convex/identityBroker.ts index ee16359..e20e2f3 100644 --- a/convex/identityBroker.ts +++ b/convex/identityBroker.ts @@ -3,10 +3,16 @@ import { broker } from "./http"; // Administration for `npx convex run identityBroker:`: // createApp '{"name":"Bristol","firstParty":false}', for an app that is not the team's own // registerClient, which Convex.HubClient of @samebase/alchemy-convex runs at deploy time +// mintRegistrationToken, listRegistrationTokens, revokeRegistrationToken, +// setAllowedCallbackHosts: deprecated, for the old route until the apps have moved // listApps, listClients '{"appId":"..."}', deleteClient '{"clientId":"..."}', deleteApp '{"appId":"..."}' export const { createApp, registerClient, + mintRegistrationToken, + listRegistrationTokens, + revokeRegistrationToken, + setAllowedCallbackHosts, deleteApp, listApps, listClients, From 3d8b53ef86395bda53ca52e85b7940836e8612ca Mon Sep 17 00:00:00 2001 From: Nicu Chiciuc Date: Fri, 30 Oct 2026 12:21:49 +0200 Subject: [PATCH 3/4] Move to identity-broker 0.7.0 and alchemy-convex 0.15.0 Co-Authored-By: Claude Opus 5.5 --- package.json | 4 ++-- pnpm-workspace.yaml | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index dd0acdb..0720f5b 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,8 @@ "lint": "vp lint" }, "dependencies": { - "@samebase/alchemy-convex": "0.14.1", - "@samebase/identity-broker": "0.6.1", + "@samebase/alchemy-convex": "0.15.0", + "@samebase/identity-broker": "0.7.0", "convex": "1.48.0" }, "devDependencies": { diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 42dc1e8..bbd810e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -6,10 +6,10 @@ minimumReleaseAgeIgnoreMissingTime: false # Keep release-age exceptions exact so later releases do not bypass the cooling period. minimumReleaseAgeExclude: # Published 2026-10-30. Remove on 2026-11-06, when it is older than the seven-day release age. - - "@samebase/alchemy-convex@0.14.1" - # Reviewed first-party release, published 2026-10-29 by samebase/identity-broker release.yml. - # Remove after 2026-11-05, when the 7-day cooling period is complete. - - "@samebase/identity-broker@0.6.1" + - "@samebase/alchemy-convex@0.15.0" + # Reviewed first-party release, published 2026-10-30 by samebase/identity-broker release.yml. + # Remove after 2026-11-06, when the 7-day cooling period is complete. + - "@samebase/identity-broker@0.7.0" # Reviewed releases: the Alchemy that alchemy-convex 0.11.0 needs, published 2026-10-24 00:43 UTC. # Remove after 2026-10-31 00:44 UTC. - "alchemy@2.0.0-beta.84" From bb25c4c7b148166fa3a23b195592c5ea4fe010ce Mon Sep 17 00:00:00 2001 From: Nicu Chiciuc Date: Fri, 30 Oct 2026 12:22:17 +0200 Subject: [PATCH 4/4] Update the lockfile Co-Authored-By: Claude Opus 5.5 --- pnpm-lock.yaml | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a81e1e0..00844e7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -192,11 +192,11 @@ importers: .: dependencies: '@samebase/alchemy-convex': - specifier: 0.14.1 - version: 0.14.1(@effect/platform-node@4.0.5(effect@4.0.5)(redis@6.3.0))(alchemy@2.0.0-beta.84(@effect/platform-node@4.0.5(effect@4.0.5)(redis@6.3.0))(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.19.2)(esbuild@0.27.0)(typescript@7.0.2)(yaml@2.9.1))(effect@4.0.5)(ws@8.22.0))(convex@1.48.0)(effect@4.0.5) + specifier: 0.15.0 + version: 0.15.0(@effect/platform-node@4.0.5(effect@4.0.5)(redis@6.3.0))(alchemy@2.0.0-beta.84(@effect/platform-node@4.0.5(effect@4.0.5)(redis@6.3.0))(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.19.2)(esbuild@0.27.0)(typescript@7.0.2)(yaml@2.9.1))(effect@4.0.5)(ws@8.22.0))(convex@1.48.0)(effect@4.0.5) '@samebase/identity-broker': - specifier: 0.6.1 - version: 0.6.1(convex@1.48.0) + specifier: 0.7.0 + version: 0.7.0(convex@1.48.0) convex: specifier: 1.48.0 version: 1.48.0 @@ -860,17 +860,18 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} - '@samebase/alchemy-convex@0.14.1': - resolution: {integrity: sha512-9K8OmSt5mhrSseOuVuBWQHLsm9GqNsXezFd8Is52OV+cuEgwkrDehoAp1f3qiob2kqLFk6o/PYHE0srV2W9n+Q==} + '@samebase/alchemy-convex@0.15.0': + resolution: {integrity: sha512-Pt3hZhiDjQQTHNJy+/avvyBlJ/U/z7BiNIQ+JhHQ+3zglydtdo6QNGHORplOlYr5fgvT9UU50g/oJDB/KaGV+A==} engines: {node: '>=22'} + hasBin: true peerDependencies: '@effect/platform-node': ^4.0.5 alchemy: 2.0.0-beta.84 convex: ^1.48.0 effect: ^4.0.5 - '@samebase/identity-broker@0.6.1': - resolution: {integrity: sha512-k9Xy5D7aqB89VChutRVtku1AMhnRLjS3Rw2IiO5qJYEALwS7OEtVbb6WCYqggx7GmKQTWZqM2WRovL+JbTjX9A==} + '@samebase/identity-broker@0.7.0': + resolution: {integrity: sha512-YWE4Tp1+DrQvaehSblD31CPm0gBKzDmPqnj1eK1EqR98CXGuN3oobztetL3lGN1p92I48fYcOwAk6+7t0LU3gA==} hasBin: true peerDependencies: convex: ^1.47.0 @@ -2042,7 +2043,7 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} - '@samebase/alchemy-convex@0.14.1(@effect/platform-node@4.0.5(effect@4.0.5)(redis@6.3.0))(alchemy@2.0.0-beta.84(@effect/platform-node@4.0.5(effect@4.0.5)(redis@6.3.0))(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.19.2)(esbuild@0.27.0)(typescript@7.0.2)(yaml@2.9.1))(effect@4.0.5)(ws@8.22.0))(convex@1.48.0)(effect@4.0.5)': + '@samebase/alchemy-convex@0.15.0(@effect/platform-node@4.0.5(effect@4.0.5)(redis@6.3.0))(alchemy@2.0.0-beta.84(@effect/platform-node@4.0.5(effect@4.0.5)(redis@6.3.0))(@voidzero-dev/vite-plus-core@1.3.0(@types/node@24.19.2)(esbuild@0.27.0)(typescript@7.0.2)(yaml@2.9.1))(effect@4.0.5)(ws@8.22.0))(convex@1.48.0)(effect@4.0.5)': dependencies: '@convex-dev/platform': 0.1.11 '@effect/platform-node': 4.0.5(effect@4.0.5)(redis@6.3.0) @@ -2050,7 +2051,7 @@ snapshots: convex: 1.48.0 effect: 4.0.5 - '@samebase/identity-broker@0.6.1(convex@1.48.0)': + '@samebase/identity-broker@0.7.0(convex@1.48.0)': dependencies: convex: 1.48.0 jose: 6.2.13