Skip to content
This repository was archived by the owner on Oct 31, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 32 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -79,34 +80,46 @@ 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

The admin functions are internal: `npx convex run --prod identityBroker:<name>`, 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
Expand Down
9 changes: 5 additions & 4 deletions convex/identityBroker.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { broker } from "./http";

// Administration for `npx convex run identityBroker:<name>`:
// 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
// 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,
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
21 changes: 11 additions & 10 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading