@flow-industries/id
v0.24.2
Published
Passkey-first identity for Flow applications. One passkey bound to `id.flow.industries`, usable across Flow apps with audience-bound JWTs and optional Tempo signing.
Readme
Flow ID
Passkey-first identity for Flow applications. One passkey bound to id.flow.industries, usable across Flow apps with audience-bound JWTs and optional Tempo signing.
Develop
cp .env.example .env
bun install
bun run devbun run dev is the only supported local application launcher. It starts the
API, dialog, playground, and landing page on the first free block of
four consecutive ports. Configure DATABASE_URL and BETTER_AUTH_SECRET in
.env first.
The launcher validates locally stored JWT signing keys before starting Auth.
If BETTER_AUTH_SECRET changed, only keys that can no longer be decrypted are
rotated so local session creation keeps working after environment changes.
It also refuses to start when migrations fail instead of serving against a
partially migrated schema.
AT Protocol identity
Bluesky and other AT Protocol accounts can sign in to Flow ID or connect to an existing account. Flow user IDs remain stable; the provider DID identifies the connection. New accounts do not need an email, passkey, or wallet to enroll.
Set ATPROTO_PUBLIC_URL, ATPROTO_PRIVATE_KEY (an ES256 private JWK with a stable
kid), and ATPROTO_STORAGE_KEY (a base64-encoded 32-byte encryption key) to
enable the integration. Leave all three unset to disable it. Production uses
HTTPS client metadata at /api/auth/atproto/client-metadata.json and public keys
at /api/auth/atproto/jwks.json. Provider tokens and DPoP keys are encrypted in
Postgres and stay on the server. Back up the storage key with the database;
changing it requires re-encrypting stored credentials.
For local OAuth, set ATPROTO_PUBLIC_URL=http://127.0.0.1:8183 and launch
./dev.sh 8180. The hosted sign-in page uses http://localhost:8183 and the
launcher sets the WebAuthn relying party to localhost. Use the printed URLs;
existing passkeys remain bound to their original relying party.
Without an email provider configured, the local launcher also prints sign-in
codes in the browser console. This requires development mode, the launcher's
FLOW_DEV_OTP_CONSOLE=1 opt-in, and a loopback issuer; the API then binds only to
127.0.0.1. Production responses never include these codes.
AT Protocol requires an IP loopback callback URI. The development landing server
redirects loopback GET requests to its fixed localhost origin before handling
them, retaining the callback query so OAuth resumes with the same browser cookies
and session storage as sign-in. The SDK still exchanges the code using the
registered IP callback URI. If that port block is occupied, update
ATPROTO_PUBLIC_URL to the selected landing port and restart. Production clients
use HTTPS metadata and authenticate with the configured signing key.
Provider sign-in resumes the bound /authorize transaction. Flow installs a
first-party session, then returns the existing one-use PKCE code to the app.
Account linking returns to /account; no provider or Flow tokens travel in
redirect URLs or window messages.
The account connection view reads saved identities and credentials locally. It does not refresh provider tokens or resolve handles on page load; reconnect performs a new provider authorization when access needs to be restored.
Connected accounts
Better Auth owns the shared account model. External identities are keyed by
providerId and accountId, and link to the canonical Flow userId. For AT
Protocol these values are atproto and the verified DID. Handles are display
metadata, never an identity key. A Flow account can connect different providers
without adding provider-specific identity tables.
Provider IDs identify a trusted provider configuration: separate OIDC issuers
must use separate IDs, even if both run the same identity-server software.
Provider credentials and temporary authorization state live in the encrypted,
provider-scoped oauth_session and oauth_state stores. The AT SDK uses an
adapter over those stores; its DPoP and discovery behavior stays within the AT
integration. Provider tokens never become Flow session tokens.
Better Auth supports built-in social providers and its Generic OAuth plugin. Flow currently uses version 1.4.17, whose default OAuth enrollment requires an email and creates its own sessions. A new provider must supply a verified stable subject to Flow's shared account enrollment, then use Flow's audience-bound session issuance. Register usable sign-in providers explicitly; arbitrary account rows do not count as recovery methods. No new identity table is needed.
Automatic email-based account linking is disabled. Disconnect through Flow's account controls so credential cleanup and last-sign-in-method checks run in one transaction; the stock Better Auth unlink endpoint is blocked. Its account listing remains available and excludes credentials.
Migration 0040 preserves existing AT links and encrypted sessions, then removes
the earlier AT-only tables. It invalidates in-flight authorization state, so
unfinished attempts must restart. Deploy the complete revised Auth stack before
enabling AT production configuration. After this migration, the intermediate
AT-only builds from PRs #170–#172 are not rollback targets; use the pre-AT main
build or a compatible forward fix. Existing pre-AT Flow user/session writes
remain supported.
npm: @flow-industries/id
Installation
bun add @flow-industries/id viemFor Wagmi apps:
bun add @flow-industries/id wagmi viem @tanstack/react-queryDocumentation
- Integration guide — server setup, canonical sign-in, React, and account navigation.
- Guest sessions — silent guest creation and preserving identity during signup.
- Action sessions — activity timing and the inline action widget.
The Auth pages in the separate documentation repository are archived. Use the guides here for the current SDK integration.
Sign-in and account pages
The SDK starts sign-in through your app's first-party /flow/session handler
and navigates to Flow ID's /authorize page. Completion returns to the saved
app path; read the new session after navigation instead of continuing after
await flow.login().
ProfileButton and flow.openAccount() open /account in a normal new tab.
The source app stays loaded and refreshes its session on refocus. Sign-in and
own-account management no longer use an embedded dialog. The remaining dialog supports signing, public profiles, and widgets. SDK ConnectResponse remains a deprecated type alias for source compatibility; sign-in itself returns through the first-party callback. See the
integration guide for server wiring and migration.
Public profile overlays
import { closeProfile, openProfile } from "@flow-industries/id";
import { isValidUsername } from "@flow-industries/id/usernames";
if (isValidUsername(username)) {
openProfile(username, { onClose: () => resumeInput() });
}
closeProfile();@flow-industries/id/usernames also exports USERNAME_REGEX and
MAX_USERNAME_LENGTH. It has no browser or server dependencies.
OpenProfileOptions.onClose?: () => void fires once after the overlay stops
accepting input, before its exit animation finishes. It covers the close button,
backdrop, Escape, closeProfile(), and sign-in/sign-out handoffs. The latest
successful openProfile call owns the callback, even when switching profiles
while open; an omitted callback clears the previous one. Invalid usernames and
SSR return false without replacing it. Closing an already closed overlay is a
no-op. A callback may open another profile.
React callers pass the same option to
useOpenProfile({ onClose: () => resumeInput() }) from
@flow-industries/id/react; the callback is captured when its returned opener
is called. The overlay belongs to the page and survives component unmounts.
Open from the document that owns the full viewport. Canvas hosts must release
pointer lock before opening, then use onClose to restore their input state.
License
MIT
Migration compatibility checks
bun run db:check regenerates into a temporary directory and refuses schema drift or edits
to previously committed SQL. CI compares against the PR base or previous main commit.
bun run test:migrations exercises the committed chain, the supported previous schema,
current projections, older writes after upgrade, and deliberate missing-migration failures.
MIGRATION_TEST_DATABASE_URL=postgres://... bun scripts/postgres-acceptance.ts additionally
creates a unique disposable database on a test PostgreSQL server, runs the real migration
entrypoint, rejects a changed applied hash, and proves a failing final migration rolls back
both its schema changes and ledger entry. The test deletes only its own generated database.
The initial compatibility baseline is source 1bda372b69e2f66a3dc0cbb244bcbe7cb858f493 (schema through 0035).
The tests freeze representative older SQL reads/writes; they do not boot every historical
application version. Advance this baseline deliberately when retiring support, and extend
the fixtures when changing a persisted contract. Migration failures block the image build;
production's PreSync job also refuses an applied ledger that is not an exact committed prefix.
Application rollback keeps the expanded schema. Pin the latest verified migration image via
Mesh's migrate.image while restoring the previous compatible application image. Do not run
down migrations or point an older migration image at a newer ledger: that is refused. A
destructive contract migration needs an explicit support-boundary change after older writers
and browser clients have drained.
Versioned consumer contracts
bun run contracts:generate produces the published
@flow-industries/id/contracts/v1/reports.json and openapi.json artifacts from
the same schemas used by the XP, action, presence and position handlers.
bun run contracts:check rejects stale output and incompatible changes to the
existing v1 request schema against CONTRACT_BASE_REF (default origin/main).
Keep existing v1 fields compatible; a breaking boundary needs a new version and
an overlap plan for deployed consumers.
Trusted service consumers can call createReportsApi(host, serviceToken) from
@flow-industries/id. The returned function accepts the operation name and its
typed request, validates both the outgoing request and successful response, and
throws ReportRequestError for non-success responses. Use the XP reporting
credential for action/XP operations and the room credential for presence/position;
never ship either service credential to a browser.
bun run test:sdk-package builds, packs and installs the actual SDK artifact in
a disposable consumer, checks public imports and declarations, and proves a
removed contract export fails. Producer tests execute the typed client against
real report handlers and validate minted guest/full session and JWT shapes.
CI gates both container and package publication on these checks.
The current OpenAPI artifact describes the service report boundary. The remaining account, room management, study, settings, session and passkey HTTP operations still need complete request/response descriptions and generated client coverage before the full HTTP contract work is complete.
Server deployments may set FLOW_ID_API_URL to a trusted internal HTTP(S) origin for API and JWKS requests, or pass apiUrl to server session/verification helpers. issuerUrl remains the public JWT issuer and browser navigation origin. The transport URL must contain no credentials, path, query, or fragment.
