Chinese version:
docs/zh-Hans/design/06-developer-experience.md
Benchmarked against Clerk, the industry reference for developer experience. The goal is full ecosystem
coverage: the server side covers the mainstream runtimes and languages, and the client side covers web
frameworks, mobile, and desktop. The TypeScript SDKs are all current packages, while the Go, Java,
Rust, PHP, Ruby, Python, .NET, Flutter, iOS, Android, macOS, Windows, Linux, and React Native SDKs
have all completed a local build or unit tests. Every SDK still lacks a real IdP round trip, and the
mobile and desktop SDKs still lack device or simulator platform-channel verification, so they MUST
NOT be described as production SDKs. The complete status is governed by
docs/sdks/platform-matrix.md.
@xid-kit/core Browser core (session state/tokens/user and organization info/calls, i.e. vanilla JS)
@xid-kit/backend Server core (web-standard runtimes: Workers/Node/Bun/Deno, networkless JWT verification)
@xid-kit/react React provider/hooks/components (current, the priority)
@xid-kit/nextjs Next.js middleware and server helpers (current)
@xid-kit/react-native React Native redirect/storage (implemented, local tests pass)
Web framework layer @xid-kit/{vue,nuxt,svelte,angular,remix,astro,solid} (current package)
Server native layer sdk/{go,java,rust,php,ruby,python,dotnet} (implemented, local build or tests pass)
Mobile @xid-kit/expo (current package), sdk/{flutter,ios,android} (implemented, local tests pass)
Desktop sdk/{macos,windows,linux} (implemented, local tests pass), @xid-kit/{electron,tauri} (current package)
The complete status table is in docs/sdks/platform-matrix.md.
@xid-kit/core has two explicit browser modes:
oidcis the default for a developer application on a different origin. It requires the registered OAuthclientId,issuer, and exactredirectUri; starts/authorizewith state, nonce, and PKCE S256; validates the callback; exchanges the code at/token; verifies the ID token against JWKS; and calls/userinfowith the access token. The publicclient_idis the SDK identifier. There is no separatepk_live_orpk_test_publishable-key database, and Management API keys MUST NOT be reused as browser identifiers. The first browser release does not requestoffline_access, so it stores no bearer refresh token and reauthorizes after the access session expires. Reauthorization goes throughsignInSilent(): a best-effort hidden-iframeprompt=noneattempt, then a top-level redirectprompt=nonefallback, then interactive login (chapter 03 section 6).intent: "sign-up"in an OIDC SDK call is an RP user-registration hint, not XID product onboarding. The SDK emitsxid_intent=sign-up; afterclient_idvalidation the Core maps it to the internal Hosted Authapplication-sign-upflow. The resulting user and default Membership stay in the Application owner's existing Tenant and the flow resumes/authorize. Only the same-origin product route/sign-upusesintent=sign-upto create a new top-level Organization.same-originis only for Core-owned UI, Console on the same hostname, or a deployment that deliberately reverse-routes Core authentication endpoints onto the application's exact origin. It uses HttpOnly opaque__Host-xid.rt.*cookies, supports multi-session account switching, and exchanges the active cookie session throughPOST /v1/sessions/token. An absoluteapiUrlwhose origin differs from the current page is rejected in this mode; a browser MUST NOT attempt to make Core cookies into third-party application credentials.
Both modes expose the same reactive signed-in state and getToken() baseline. Cookie-only
capabilities such as multi-session switching, direct credential calls, guest creation, and
same-origin account-management mutations are explicitly unavailable in oidc mode until a bearer
self-service contract exists. Browser or framework code never parses an opaque refresh cookie as a
JWT.
Full ecosystem coverage (benchmarked against Clerk), split into server and client. The complete status
table is in docs/sdks/platform-matrix.md, and the two MUST stay consistent.
Server side (networkless JWT verification / request auth / webhook verification):
- current:
@xid-kit/backend(web-standard runtimes Cloudflare Workers / Node.js / Bun / Deno, using Web Crypto) - implemented (local build or tests pass):
sdk/go,sdk/java,sdk/rust,sdk/php,sdk/ruby,sdk/python,sdk/dotnet; the real IdP round trip L4 is not complete
Client-side web frameworks (provider / hooks / prebuilt components, on top of @xid-kit/core):
- current:
@xid-kit/core(vanilla JS),@xid-kit/react,@xid-kit/nextjs - current package:
@xid-kit/vue,@xid-kit/nuxt,@xid-kit/svelte,@xid-kit/angular,@xid-kit/remix,@xid-kit/astro,@xid-kit/solid
Client-side mobile:
- implemented:
@xid-kit/react-native,sdk/flutter,sdk/ios(Swift),sdk/android(Kotlin); current package:@xid-kit/expo. Real device or simulator platform channels and IdP L4 are not complete
Client-side desktop:
- implemented:
sdk/macos,sdk/windows,sdk/linux; current package:@xid-kit/electron,@xid-kit/tauri. Real desktop runtimes, OS secure storage, and IdP L4 are not complete
The cross-origin Web and native SDKs all reuse Hosted Auth plus the OIDC Authorization Code + PKCE
S256 flow. A public client uses token_endpoint_auth_method=none, never receives or stores a client
secret, cannot disable PKCE, does not use the implicit flow or password grant, and does not duplicate
SAML, SCIM, or Management API business logic. Public clients that need refresh tokens use DPoP;
without sender binding they register authorization code only.
Native API surface:
configure(options)
signIn(options)
handleRedirect(url)
getSession()
getAccessToken(options)
signOut()
setTokenStorage(adapter)
<SignIn /> <SignUp /> <GoogleOneTap /> <Waitlist /> <TaskChooseOrganization />
<TaskResetPassword /> <TaskSetupMFA />
<UserButton /> (avatar button, multi-session switching plus sign-out), <UserProfile /> (account
management: email, phone, security, connected accounts), <UserAvatar />
<OrganizationSwitcher /> <OrganizationProfile /> (members, roles, SSO, domains)
<CreateOrganization /> <OrganizationList />
<PricingTable /> <CheckoutButton /> <PlanDetailsButton /> <SubscriptionDetailsButton />
<XidProvider /> <Show when="signed-in|signed-out" /> <XidLoaded /> <XidLoading />
<XidFailed /> <XidDegraded /> <AuthenticateWithRedirectCallback /> <RedirectToSignIn />
<RedirectToSignUp /> <RedirectToUserProfile /> <RedirectToOrganizationProfile />
<RedirectToCreateOrganization />
<SignInButton /> <SignUpButton /> <SignOutButton />
Authentication and session: useAuth() (isSignedIn/userId/sessionId/getToken/signOut), useUser(),
useXid() (the full instance, with imperative methods such as openSignIn), useSession(),
useSessionList(), useSignIn() (unified low-level control over sign-in and user creation)
Organization: useOrganization(), useOrganizationList(), useOrganizationCreationDefaults()
Advanced (not started, planned): useReverification() (re-verification for sensitive operations),
useWaitlist()
Billing/API key: useCheckout, usePlans, useSubscription, usePaymentMethods, useAPIKeys, and
others (iterative)
xidMiddleware(): edge middleware that protects routes, reads the auth state, and sets the localeauth(): App Router server-side session access (no request argument needed; React cache deduplicates automatically)currentUser(): server-side access to the full UsergetAuth(): Pages Router getServerSidePropsxidClient: the server-side entry point to the Management API
Zero-configuration hosted pages, with 12 public authentication pages: sign-in (/sign-in), sign-up
(/sign-up, a redirect shell), forgot password (/forgot-password), reset password
(/reset-password), email verification (/verify-email), MFA challenge (/mfa), consent
(/consent), activate (/activate), CIBA activation (/ciba-activation), accept invitation
(/accept-invitation), create organization (/create-organization), and select organization
(/select-organization). There are also 5 account self-service pages (account settings and
self-management).
The Hosted UI is a React 19 SPA built inside the same apps/server project as the Worker, with
@cloudflare/vite-plugin producing both the Worker bundle and the static SPA assets. It is not hosted
separately on R2; the assets are served directly by Cloudflare Workers Static Assets (the ASSETS
binding), configured in wrangler.jsonc. This Core SPA contains sign-in, consent, MFA, organization
selection, and account self-service. It does not contain the public site, public docs, or management
Console.
run_worker_first=true sends document requests through Hono before Static Assets. The Worker serves
the SPA shell only for the exact paths in CORE_SPA_ROUTE_PATHS, including /sign-in, /account,
and the other declared Hosted UI routes. It rewrites those GET or HEAD requests to the / asset
entry while preserving the browser URL. Unknown paths are not SPA fallbacks: Core performs an exact
asset lookup and preserves the missing asset's real 404 response. More-specific Cloudflare Worker
Routes select the Nimbus Site and Console before Core. Inside the Core SPA,
@tanstack/react-router handles the declared routes, and the router compatibility layer lets
existing pages keep their navigation API.
Build output directories:
dist/
client/ Vite SPA build output (static assets)
worker/ Worker bundle
Public presentation is one Nimbus 0.9.0 static Site deployment, not a separate marketing runtime.
Nimbus owns the canonical apex product landing page, the /docs documentation hub, 8-locale docs,
Pagefind, OG images, sitemaps, Markdown twins, LLM documents, and structured metadata. English uses
/ for the product landing page, /docs for the documentation hub, and /{slug} for document
details. The other 7 locales use /{locale-segment}, /{locale-segment}/docs, and
/{locale-segment}/{slug} respectively. The published surface contains one product landing page,
one hub, 41 documents, and one status page per locale, for 352 pages total. The locale-neutral
documents.json AST is the content source for the 336 generated collection pages; product landing
and localized status routes are explicit Astro pages. Generated localized MDX is a build artifact
and is not an authoring source.
The installed Nimbus Registry feature set is:
pagefind-search: indexes all 352 localized Site pages.ai-native: emits one downleveled.mdtwin and one source-preserving.mdxtwin for every published page. Global/llms.txtand/llms-full.txtcover all 352 pages. English locale endpoints under/en/and the other 7 locale endpoints under their locale segment each cover 44 pages. Each locale also emits a Nimbus-compatible SDK section index and corpus at/sdks/llms*.txtor/{locale-segment}/sdks/llms*.txt, covering exactly its 29 SDK pages. It also emits robots and sitemaps.404-page: returns a localized terminal Site 404 for unknown public routes without entering Core.mermaid: converts AST CodeBlock entries whose language ismermaidinto theme-aware diagrams with a full-screen dialog. Diagram source stays intact in both Markdown twins.lint-prose-textlint: regenerates the content collection and applies the English prose gate only to the generated English docs subtree.
Every published HTML page carries canonical and hreflang links, Open Graph metadata, and JSON-LD.
The availability of another upstream Registry recipe does not enable it in XID. In particular,
changelog, new-version, and new-collection are not part of this shipped surface.
/docs is the canonical English documentation hub. Registered historical detail paths such as
/docs/getting-started return a 308 to the flat canonical document tree. An unknown /docs/* path
returns the Nimbus 404 and never falls through to Hosted Auth. The English SCIM documentation uses
exact /scim routes only, so /scim/v2/* remains owned by Core.
The org and instance management surfaces remain one unified React Console product, but their static
assets are deployed by a separate Console Worker. That Worker has only an ASSETS binding and owns
/console and /console/* on both the apex and tenant hosts. It has no database, cache, object
storage, Durable Object, queue, secret, protocol, or Management API binding.
The browser keeps the original host for Console document navigation and same-origin API calls. Sign
in, MFA, account, /auth/*, and /v1/* continue to resolve to Core. This preserves the host-only
__Host- session cookies on both apex and tenant hosts. Cross-runtime links use document navigation
instead of client-router navigation.
Cloudflare Worker Routes provide the split. More-specific Site and Console routes take precedence over the Core Custom Domain and tenant wildcard fallback. Neither frontend Worker claims a broad apex catch-all, and there is no front proxy Worker.
Because Cloudflare matches the complete URL including the query string, exact Site and Console patterns can fall through to Core when a query is present. Core uses the shared ownership contract and one-way Service Bindings to delegate only those exact frontend requests with the original URL unchanged. Unknown and typo paths remain Core 404s, and neither frontend Worker binds back to Core.
vite.config.ts (apps/server/vite.config.ts) uses a standard Vite configuration, with this plugin
order:
import react from '@vitejs/plugin-react'
import { cloudflare } from '@cloudflare/vite-plugin'
import { lingui } from '@lingui/vite-plugin'
import babel from '@rolldown/plugin-babel'
import { linguiTransformerBabelPreset } from '@lingui/vite-plugin'
plugins: [react(), cloudflare(), lingui(), babel({ presets: [linguiTransformerBabelPreset()] })]The Core Worker mounts every protocol endpoint and the Management API through Hono. Core-owned human paths flow like this:
- The request does not match a more-specific Site or Console Worker Route.
- Hono handles registered protocol and API routes. The unmatched-protocol blocker returns a real protocol-shaped 404 for reserved protocol and API prefixes.
registerPublicAssetRoutes()rejects paths owned by Site or Console, then evaluatesisCoreSpaRoute(pathname)against the shared exact route manifest.- An exact Hosted UI route with GET or HEAD is rewritten to the
/asset entry and receives the SPA shell. The browser URL is unchanged; non-document methods return 404. - Every other Core-owned path is fetched from ASSETS without rewriting. A real asset is served, and a missing asset or unknown document path remains a real 404. It never receives the SPA shell.
The relevant fallback shape inside the Worker is:
// worker/public-assets.ts
if (isCoreSpaRoute(url.pathname)) {
if (c.req.method !== 'GET' && c.req.method !== 'HEAD') {
return new Response(null, { status: 404 })
}
return serveSpaAsset(c, spaEntryRequest(c.req.raw))
}
return serveSpaAsset(c)The ASSETS binding is declared in wrangler.jsonc. Core intentionally calls env.ASSETS.fetch
after ownership and exact-route checks because the Worker, not the Static Assets SPA mode, owns the
fallback decision.
The OIDC /authorize endpoint is handled at the Worker layer. When a request arrives without a valid
session, it redirects to the SPA sign-in page as follows, and after sign-in the session comes back and
the OIDC flow resumes:
Step 1: the Worker saves the OIDC request context
When /authorize receives an unauthenticated request, it encodes the complete original query string
(response_type, client_id, scope, redirect_uri, state, code_challenge,
code_challenge_method, nonce, and so on) and stores it temporarily in OAuthFlowDO (key =
authz:{applicationTenantId}:{authz_request_id}, where authz_request_id is a random UUID, with a
10-minute TTL and single-use consumption). The Application Tenant is resolved from the validated
client_id before the browser cookie and is fixed for the complete transaction. The Durable Object
holds the protocol recovery context and the interaction start time; it never lets an identifier,
cookie, or Organization hint replace the Application-owner Tenant.
Step 2: the Worker returns a 302 to the SPA sign-in route
HTTP/1.1 302 Found
Location: /sign-in?authz_request_id=<uuid>&client_id=<client_id>&organization_id=<application_tenant_id>
The full OIDC parameters are not appended to the redirect URL, which keeps sensitive parameters such
as redirect_uri out of browser history and referrers.
Step 3: the SPA sign-in page renders and authenticates
The SPA /sign-in route reads the opaque request id plus the validated Application hints and calls
GET /auth/config. Every credential method carries those hints back to Core, where client_id is
resolved again and must agree with the Tenant hint. The user completes authentication (magic link,
email OTP, password, passkey, SSO, and so on), and the Worker issues a session cookie
(HttpOnly; Secure; SameSite=Lax) in the Application Tenant.
Step 4: after sign-in the SPA returns to /authorize
On a successful sign-in, the SPA reads the current authz_request_id and navigates to:
/authorize?authz_request_id=<uuid>&client_id=<client_id>
When the Worker /authorize endpoint receives an authz_request_id parameter, it restores the
original OIDC request parameters from OAuthFlowDO (single-use consumption: the Durable Object record
is deleted on restore), verifies the session cookie, and continues with the normal authorization code
issuance flow (PKCE validation, consent check, code issuance, 302 to redirect_uri).
For prompt=login, the interaction start time is retained with the pending request. Resume is allowed
only when the new session's authenticated_at is at or after that time; Core then removes the
already-satisfied login prompt before reevaluation. This prevents both silent prompt bypass and an
infinite /authorize -> /sign-in loop.
Key constraints
- The authorization request in OAuthFlowDO is consumed exactly once: it is deleted as soon as
/authorizerestores it, which prevents replay. - 10-minute TTL: after it expires,
/authorize?authz_request_id=xxxis treated asinvalid_requestbecause the Durable Object record is gone, and the user is asked to start over. - User cancels sign-in: the SPA navigates to
redirect_uri?error=access_denied&state=<original_state>(reading the state from the restored authorization request) rather than redirecting to/authorize, which avoids a redirect loop. - The consent page is also an SPA route: when
/authorizehas a valid session but no consent, it uses the same 302 mechanism to redirect to/consent?authz_request_id=<uuid>. The consent page callsGET /auth/consent-params?prompt_id=<uuid>(whereprompt_idequalsauthz_request_id) to fetch the client display data and the localized scope list./mfaand/select-organizationuse the same 302 mechanism.
Complete flow sequence
RP -> GET /authorize?... (not signed in)
Worker -> store OAuthFlowDO (key=authz:{tenantId}:{uuid}, pendingParams, TTL 10min)
-> 302 /sign-in?authz_request_id=uuid
Browser -> GET /sign-in?authz_request_id=uuid
ASSETS -> index.html (SPA bootstrap)
SPA -> GET /auth/config (fetch the tenant authentication configuration)
user completes authentication
SPA -> POST the authentication endpoint (the Worker issues the session cookie)
-> the frontend navigates to /authorize?authz_request_id=uuid
Worker -> GET /authorize?authz_request_id=uuid
verify the session cookie
restore pendingParams from OAuthFlowDO (single-use consumption, the DO record is deleted)
run the consent check / issue the code
-> 302 <redirect_uri>?code=...&state=...
XID currently ships redirect-hosted authentication only. A developer application begins at
/authorize; the complete credential interaction then stays inside the Core Worker and Hosted UI
until Core redirects the authorization code to the exact registered application callback. SDK
components may render branded redirect controls, but they MUST NOT iframe Hosted UI or claim
hash/path embedded routing. Core deliberately emits frame-ancestors 'self', and WebAuthn is not
offered through a cross-origin frame.
A future truly embeddable form surface would require an explicit trusted-origin model, WebAuthn
cross-origin design, isolated credential APIs, and dedicated threat review. It is not represented by
relaxing CSP or by pointing an iframe at /sign-in.
- Hosted pages: dashboard configuration (colors and logo); arbitrary CSS is not supported
- Embedded components: the appearance prop plus a theme (including shadcn themes), with variable overrides and CSS class names
- Fully custom: build your own UI with the hooks
Runtimes: Node (express/fastify/nextjs), Cloudflare Workers/Pages (networkless), Vercel Edge, Bun, and Deno. Built on standard V8 web APIs.
Token and session verification:
authenticateRequest(request, options): checksAuthorization: Bearer, or an application-owned short-lived JWT cookie only whenjwtCookieNameis explicit, then verifies signature, exp, and azpsessionTokenExchange: for same-origin framework deployments, forwards the incoming Cookie header only to an exact same-origin CorePOST /v1/sessions/tokenendpoint and verifies the returned JWT; it never validates__Host-xid.rt.*locallyverifyToken(token, options): the low-level call; passing jwtKey skips the network requestverifyWebhook(request, options): HMAC signature verification
This request-authentication contract is shared by @xid-kit/backend and the native server SDKs in
sdk/{go,rust,python,ruby,php,java,dotnet}:
Authorization: Beareris the only default credential source. Cookie fallback is disabled until the application explicitly supplies the name of its own short-lived JWT cookie.__Host-xid.rt.*values are Core-owned opaque refresh credentials. An SDK never scans that prefix, parses one as a JWT, or silently treats a legacy__sessioncookie as an access token.- Core browser-session exchange is a separate, explicit operation. The caller supplies the
absolute incoming request URL, the complete incoming
Cookieheader, and the Core endpoint. The SDK requires the endpoint to have the exact same origin as the incoming request and the exact/v1/sessions/tokenpath, with no userinfo, query, or fragment. - The exchange sends
POST, forwards the completeCookieheader only to that validated endpoint, never follows a redirect, and accepts only HTTP 200 JSON whose complete object shape is{ "token": "<non-empty JWT>" }. Cross-origin endpoints, redirects, malformed JSON, extra response fields, and empty or non-string tokens fail closed before JWT verification. - A runtime that does not own an HTTP client may accept an injected transport adapter, but the SDK, not the adapter, still enforces origin, endpoint, redirect, status, and response-shape checks.
Networkless mode: pass jwtKey (the JWKS public key) and no API request is needed, which suits edge
cold starts. This applies to Bearer or application JWT handoffs. A Core browser session requires the
one same-origin cookie-to-JWT exchange first. If Core and the application do not share an origin, the
application must establish an explicit Bearer/JWT handoff; it MUST NOT copy or forward Core opaque
refresh cookies across origins.
The table below is the current implemented surface, not a roadmap:
| Resource | Operations |
|---|---|
| users | CRUD, search, ban/unban, bulk metadata, export, soft delete, and restore |
| organizations | CRUD, logo, domain verification, branding and policy, nested enterprise resources, and restore |
| memberships | list/create/update role/delete/restore |
| invitations | create (bulk rate-limited), revoke, list |
| sessions | list/get/revoke/revoke all |
| applications/clients | CRUD, secret rotate, delete, and restore |
| connections (SSO) | CRUD, delete, and restore |
| directories (SCIM) | CRUD, token rotate, delete, and restore |
| projects | CRUD, soft delete, restore, and active/deleted/all listing |
| roles/permissions | CRUD, delete, and restore |
| role-permissions | list/create/update/delete with same-Project and ABAC validation |
| manager-assignments | tenant-scoped list/provision/revoke; instance managers use the separate platform path |
| org-units | tree CRUD, move, archive, and member placement (see below) |
| access-requests | list/get within an Organization, with status and project filters (see below) |
| project-grants | list/get/create/revoke/delete |
| user-grants | list/get/create/reactivate/revoke/delete |
| webhooks | CRUD, delete, and restore |
| apiKeys | create/list/revoke |
Authentication uses Authorization: Bearer sk_live_xxx or sk_test_xxx. M2M clients use
client_credentials at the root token endpoint, POST /token; there is no /oauth/token route.
Pagination is cursor-only through { data, next_cursor, has_more }, with a maximum and default page
size of 100. Bulk invitations are limited to 50 per hour through RATE_LIMITER. The design target
of 10 metadata PATCH requests per 10 seconds per user is not implemented and MUST NOT be claimed.
Versioning uses the /v1/ URL prefix.
The Project resources also support same-origin Console cookie sessions through exact
ManagerAssignment boundaries. project_manager can mutate only its assigned Project.
project_grant_manager can read only its exact active Grant and granted Role/Permission definitions,
and can manage UserGrants only under that Grant. The recipient Organization owner/admin has the same
UserGrant assignment boundary for its own active members. These paths do not promote either Project
manager role to Organization Admin.
Project deletion is a reversible control-plane operation. projects.status = deleted makes the
Project unavailable to every Project-linked client lookup, including authorization, token refresh,
client credentials, userinfo, CIBA, consent display, Role/Permission management, and grant mutation,
while preserving dependent rows. POST /v1/projects/:id/restore re-enables the same
namespace. GET /v1/projects defaults to status=active and accepts deleted or all so the
Console recycle view survives navigation and refresh.
Role and Permission lists use the same recycle contract: status=active|deleted|all, defaulting to
active, and return status plus deleted_at. Cookie-session reads still require the exact
project_id and Project authorization boundary.
ManagerAssignment provisioning is explicit. Tenant roles use /v1/manager-assignments; only the
fixed pairs org_manager/org, project_manager/project, and
project_grant_manager/grant are accepted. The cookie-only
/v1/platform/manager-assignments path owns instance_manager provisioning and remains separate
from tenant-scoped APIs.
OrgUnit management lives under /v1/organizations/:orgId/units, guarded by an API key with
org-units:read / org-units:write or an org manager cookie session:
| Method | Path | Notes |
|---|---|---|
| GET | /v1/organizations/:orgId/units |
?parent_unit_id= filter; defaults to the whole tree in path order |
| POST | /v1/organizations/:orgId/units |
Create a node (depth cap 8) |
| GET | /v1/organizations/:orgId/units/:unitId |
Detail including depth/path |
| PATCH | /v1/organizations/:orgId/units/:unitId |
Update name/slug/manager_user_id |
| POST | /v1/organizations/:orgId/units/:unitId/move |
Move the subtree under a new parent |
| DELETE | /v1/organizations/:orgId/units/:unitId |
Archive (leaves only), not a physical delete |
| GET | /v1/organizations/:orgId/units/:unitId/members |
Members; include_descendants defaults to true |
| PUT | /v1/organizations/:orgId/units/:unitId/members/:userId |
Place a member, optionally as the primary post |
| DELETE | /v1/organizations/:orgId/units/:unitId/members/:userId |
Remove a member |
Project access requests are read-only on the Management API: GET /v1/organizations/:orgId/access-requests (with ?status= and ?project_id= filters, cursor
pagination) and GET /v1/organizations/:orgId/access-requests/:id, guarded by
access-requests:read or an org manager cookie session. Approve/deny never run through /v1 --
they MUST bind to the resolved approver's real identity for audit, and the org manager fallback is
direct user_grants management. PATCH /v1/projects/:projectId accepts the access_policy field
(open/restricted/approval_required, projects:write). The scope lexicon gains the
org-units and access-requests resource names.
The requester and approver flows are cookie-session endpoints under /auth/, same pattern as
consent: POST /auth/access-requests, GET /auth/access-requests,
POST /auth/access-requests/:id/cancel, GET /auth/access-approvals,
POST /auth/access-approvals/:id/approve, and POST /auth/access-approvals/:id/deny. Their
semantics (state machine, approver resolution, JIT grants) are defined in chapter 02 section 7.5.
The following remain explicit design targets and do not have tenant-scoped Management API
resources: emailAddresses, phoneNumbers, allowlistIdentifiers, oauthApplications,
redirectUrls, and billing CRUD. User impersonation is intentionally not a tenant-scoped
Management API resource; it is an implemented Instance Manager platform operation using
POST /v1/platform/impersonation/start and the cookie handoff lifecycle at
POST /auth/impersonation/{handoff,consume,end}. The read-only platform billing overview is a
separate /v1/platform/* console surface, not billing CRUD.
The list below is the design target catalog, not the shipped event list. The exact currently emitted
names are maintained in webhook-event-contract; the Nimbus public page lists only those names.
- user: created/updated/deleted
- guest: created/converted/gc_deleted (see chapter 01 section 8 and section 10 below)
- session: created/ended/removed/revoked
- organization: created/updated/deleted
- organizationMembership: created/updated/deleted
- organizationInvitation: created/accepted/revoked
- organizationDomain: created/updated/deleted/verified/verification_failed
- authentication: password_succeeded/failed, passkey__, mfa__, oauth__, sso__, magic_auth__, email_verification__, radar_risk_detected
- connection (SSO): activated/deactivated/deleted/saml_certificate_renewed/renewal_required
- dsync (directory sync): activated/deleted, user.created/updated/deleted, group.created/updated/deleted, group.user_added/removed
- role/permission: created/updated/deleted
- access_request: created/approved/denied/cancelled/expired
- project: access_policy_changed
- org_unit: created/updated/moved/archived/member_added/member_removed/primary_changed
- email/sms: created (when the developer takes over sending)
- billing: subscription., paymentAttempt.
- An in-house delivery layer (Svix-style) or an integration with Svix
- Retries: exponential backoff with automatic retries; dead letters go to D1
- Manual replay by message or time range is a design target and is not implemented. Queue-level dead-letter replay is an Instance Manager operational surface, not product-level webhook replay.
- Signature verification: the
{ type, data }body is signed with HMAC-SHA256 and carriessvix-id,svix-timestamp, andsvix-signatureheaders, using a 5-minute replay window. - Idempotency: developers handle duplicate events themselves
- Delivery goes through Cloudflare Queues for decoupling and never blocks the sign-in path
- The ordered, pull-based Events API remains a design target and is not implemented.
- API keys are a first-class resource with scoped permissions, managed from the frontend through
useAPIKeysand through backend CRUD - Structured errors: XidAPIError (code/message/longMessage/meta.paramName), mapping precisely onto form fields
- Local development: a dev instance with
sk_test_Management API keys, OAuthclient_idvalues for public clients, certificate-free localhost (through an HTTPS proxy), and testing tokens that bypass bot detection - Documentation target: publish a dedicated page per component and hook, with a props table, examples, a playground, and shadcn/Tailwind integration examples. The current Nimbus Site publishes the product and protocol documentation corpus; the component showcase remains a design target.
The design contract is chapter 01 section 8. Status: implemented (endpoint, conversion routing,
GuestStore DO, GC cron, React and native SDK APIs); the per-platform status lives in
docs/sdks/platform-matrix.md.
- Endpoint:
POST /auth/guest, an unauthenticated private extension (not a standard OIDC capability). It creates the anonymous user plus session, sets the HttpOnly session cookie, and returns exactly{ sessionId, redirectUrl }. The response does not embed a User, Organization, or expiry object; the client followsredirectUrland obtains current user and organization state from/v1/me. A request that already carries a valid guest session gets a 200 response with the same wire shape and no new user. The endpoint is guarded by Turnstile plus RateLimitStore and a per-tenant daily mint cap; chapter 01 section 8 holds the full four-layer anti-duplicate contract. - SDK API:
signInAnonymously()creates a guest, or lazily reuses the local guest credential when one is still valid (the SDK does not call the endpoint in that case, the Firebase semantics).isAnonymousreflects whether the current tokenamrcarriesguest. Upgrade guidance keeps prompting the user to convert.upgradeGuestWithPasskey()runs the passkey conversion ceremony from inside the SDK (same-origin mode only), without a Hosted Auth redirect. Credential linking and pending Email verification convert the guest in place, so the next token keeps the samesub. Email uniqueness is Tenant-local: an account with the same Email in another Tenant remains independent, onboarding never performs a cross-Tenant merge, and a same-Tenant collision is not a normal branch for the fresh Tenant. - Management API: the
/v1/userslist supports the?provisioned_by=anonymousfilter; no new endpoint is added. - Audit and webhook event names (see section 8):
guest.created,guest.converted, andguest.gc_deleted.
Status: the TypeScript release graph can produce and consume audited 0.1.0-alpha.0 tarballs, but
no npm publication has been performed or authorized. Registry availability is therefore
UNKNOWN, not supported.
- The public graph is the 15 SDK packages plus
@xid-kit/types,@xid-kit/crypto, and@xid-kit/protocol. Those kernels are public release dependencies because emitted SDK runtime or declaration files import them. A public artifact must never depend on a private workspace-only package. - Source manifests use
workspace:^; packed manifests must contain concrete^0.1.0-alpha.0ranges and noworkspace:orcatalog:protocol. pnpm run sdk:distribution:verifyis the release artifact gate. It builds withvp pack, creates and audits all tarballs, and installs them into a fresh out-of-workspace consumer for TypeScript and runtime import checks. It never publishes or reads registry credentials.- The 13 native SDKs remain source-only. Their manifests and README distribution claims have a
static gate, while actual registry publication, package-name ownership, signing, provenance, and
per-platform release automation remain external
UNKNOWNwork.
The complete package graph, gate behavior, and manual tarball command are documented in
docs/sdks/distribution.md.
{ "main": "worker/index.ts", "assets": { "binding": "ASSETS", "run_worker_first": true, }, }