Skip to content

v0.14.0

Release Date: 2026-10-11

A breaking release with five themes. Principal is the sole identity root: the legacy User model and its tables are gone, so password login, social login, federation sync, and SCIM all resolve a person to the same principal (ADR-004). Managed principals let an application create humans without an email, such as young learners, whom guardian and educator managers control and sign in with QR-friendly login tokens (ADR-005), and management is now derived from manager relationships (ADR-009). A security pass makes the marketplace, organization, and invite APIs act for the authenticated caller and check organization permissions, instead of trusting the IDs a request names. The organization model is completed: organizations without a plan, signup on first login, slugs, application roles and entitlements carried in an orgs claim, collaborators and resource grants (ADR-006 to ADR-008), and real-time invalidation of access changes (ADR-010). Finally, the authorization modes are named rbac and rebac and chosen by configuration.

This release covers everything since v0.13.0. Read the upgrade notes before deploying; they are in two parts, one for the identity and security changes and one for the organization model.

Highlights

  • One identity model — Principal(type=human) + Human everywhere; systemauth.Storage works on HumanPrincipal.
  • Managed principals — opt-in per deployment and per client; guardian and educator managers; login tokens, a rate-limited exchange endpoint, manager visibility and revocation, guardian claims, and attaching an email later. See Managed Principals.
  • Caller-bound, authorized APIs — the marketplace, organization, and invite APIs identify the caller through requestprincipal and check organization permissions; without an authorizer they refuse rather than trust the request.
  • Provider parity — the simple and SpiceDB providers grant identical organization and manager permissions, enforced by a parity test against the base schema and a shared conformance suite.
  • AI agents at resource servers — identity/requestprincipal with an AAuth authenticator (aauthrs) next to bearer, API-key, and BFF-session authentication.
  • No organization plan — sf_organizations.plan and organization.Plan* are gone; organization responses carry entitlements derived from the marketplace. See Application roles and entitlements.
  • Slugs — reserved names, rename with a 7-day cooling-off period, aliases for old links, and personal-to-team conversion. See Organization slugs and Converting a personal organization to a team.
  • Application roles — assigned per principal, organization and application, limited to entitled applications and seat limits, and granted by invites. See Invites.
  • orgs claim and membership sources — ClaimsMemberships is the default relying-party source; RemoteMemberships reads GET /oauth/userinfo/memberships.
  • Real-time invalidation — access tokens default to 10 minutes (at most 15), every access-reducing change is recorded in sf_access_changes, and relying parties refuse earlier tokens; polling, replay and back-channel delivery cover replicas and separate deployments. See Access-change invalidation.
  • Collaborators and resource grants — viewer, contributor and maintainer on one resource, in rbac and rebac, and invites that target a resource. See Collaborators and resource grants.
  • Authorization mode as configuration — the authz key and authz/mode.New. See Configuring the authorization mode.
  • Management is a relationship — release, manager invitations (accept or decline), authorized managing operations and deletion.

Security

These fix vulnerabilities present in v0.13.0. Upgrade promptly.

  • Marketplace checkout took the purchaser from the request body, so anyone could start a purchase in another principal's name for any organization.
  • Marketplace listing, license, and seat endpoints acted on any IDs a request named.
  • The organization API listed every organization and let any caller read, change, or delete organizations, change members, and transfer ownership.
  • Invite management needed no permission, and anyone holding an invite link could accept it as any principal.
  • Invite tokens were stored in plain text; they are now SHA-256 hashes.
  • SpiceDB principal and organization cleanup ignored failures and missed editor and manager relations, leaving access behind while reporting success.
  • Refresh tokens could be used more than once, and refreshing near expiry extended a session indefinitely; rotation is now atomic, a session keeps its original expiry, and reusing a rotated refresh token revokes the whole token family.
  • Stripe checkout granted a license before a delayed payment cleared, an invalid seat count produced an unlimited seat-based license, and cancelling a subscription left its license in place.
  • Deactivating a principal did not stop its tokens: access tokens stayed valid and refresh tokens kept issuing new ones for up to 30 days. Tokens of an inactive principal are now refused wherever they are used or refreshed.
  • Agent delegation failed open: revoking a delegation gave the agent unrestricted access, constraints that did not overlap allowed everything, and expiry and action limits were never stored.
  • Revoking platform administration through principal.UpdateHuman was not synced to SpiceDB.
  • Signing up with an invite accepted any email, and SignupOrGetExisting returned existing accounts for unverified emails.
  • Anyone who could reach SystemAuth's authorization endpoint, or identity/oauth's, could obtain tokens for any user by sending an X-User-ID header. Header identity is now an explicit, gated opt-in, and identity/oauth also requires consent for third-party clients.
  • Rate limits, BFF sessions, and API key usage records believed client-supplied X-Forwarded-For, X-Real-IP, and Cloudflare headers, so a client could evade a limit or forge its address. They now key on the direct peer and believe forwarding headers only from proxies you configure.
  • SCIM listings skipped authorization, and a SCIM API mounted without a hook was open; the federation identity sync endpoint trusted its request body from anyone; token introspection accepted any token or public client as the caller; and the default storage accepted replayed client assertions.
  • API keys could be managed by ID alone and granted any scope when no allow-list was set; the BFF proxy could forward the browser's session cookie upstream; concurrent logins could exceed the lockout limit; and multiapp trusted the X-App-ID header.
  • BFF cookies could lose Secure and HttpOnly; cookie-authenticated writes skipped the same-origin check; JWTs without exp were accepted; client secret checks revealed which client IDs exist; and multiapp could leave one application's schema on a pooled connection.

Upgrade Notes

1. Database

Run these against an existing PostgreSQL database; a fresh database needs only systemauth migrate.

Script When What
migrations/0002_drop_legacy_user_tables.sql Before starting this release Creates the system service principal, reassigns OAuth apps owned by legacy users, drops the legacy tables
migrations/0003_hash_invite_tokens.sql When deploying Hashes stored invite tokens so outstanding invite links keep working
migrations/0004_drop_organization_plan.sql When deploying Drops sf_organizations.plan
migrations/0005_subscriptions_per_listing.sql When deploying Adds listing_id to subscriptions and allows one subscription per listing beside the platform subscription
migrations/0006_invite_app_roles.sql When deploying Adds sf_invites.app_roles
migrations/0007_oauth_app_backchannel_logout_uri.sql When deploying Adds sf_oauth_apps.backchannel_logout_uri
migrations/0008_drop_editor_viewer_roles.sql When deploying Maps editor to member and viewer to collaborator in memberships and pending invites, recording an access change for each active membership it changes
migrations/0009_invite_resource_target.sql When deploying Adds the resource target columns to sf_invites

Run them in order; each is safe to run more than once. Then run systemauth migrate (or systemauthsvc.Service.Migrate) for the new tables and columns: sf_manager_relationships, sf_manager_claims, sf_client_assertion_jtis, the login_token credential type, and OAuthApp.managed_principals_enabled and login_token_ttl.

The organization model adds further tables, created by the same command: sf_app_roles, sf_slug_reservations, sf_access_changes, sf_resource_grants and sf_manager_invites.

2. Configure authorization for the HTTP APIs

The marketplace, organization, and invite APIs now require an authenticated caller, set by requestprincipal.Middleware, and an authorizer. Until the authorizer is configured, organization-scoped endpoints answer 501.

orgs := organization.NewService(db)
authorizer := rbac.New(
    rbac.WithRoleGetter(organization.MembershipRoleGetter(orgs)),
    rbac.WithPlatformAdminChecker(isPlatformAdmin), // optional
)

orgCfg := organization.DefaultAPIConfig()
orgCfg.Authorizer = authorizer
orgAPI := organization.NewAPI(orgs, orgCfg)

inviteCfg := invite.DefaultAPIConfig()
inviteCfg.Authorizer = authorizer
inviteAPI := invite.NewAPI(invite.NewService(db, baseURL), orgs, inviteCfg)

marketplace.NewAPI(humaAPI, marketplace.APIConfig{}, svc,
    marketplace.WithAuthorizer(authorizer))

// Mount each behind requestprincipal.Middleware so requests carry the caller.

With SpiceDB, pass the SpiceDB provider instead. MembershipRoleGetter counts only active memberships; a role getter built on GetMembership directly would also count deactivated ones.

3. Client changes

  • Request fields that named the acting principal are optional and deprecated, and when sent must equal the caller: marketplace purchaser_id, owner_id, assigned_by; invite inviter_principal_id and principal_id; the email query on pending invites.
  • Accepting or declining an invite requires the caller's email to match the invite.
  • Listing organizations returns the caller's organizations (all of them for platform administrators).
  • Creating an organization makes the caller its owner.
  • Unpublished marketplace listings are visible only to the creator organization.

4. Go API changes

  • Removed Ent schemas and packages: user, membership, oauthaccount, refreshtoken, apikey, serviceaccount, serviceaccountkeypair, and mixin.UserBase / mixin.MembershipBase.
  • systemauth.Storage user methods became CreateHuman, GetHuman, GetHumanByEmail, GetHumanByExternalIdentity, LinkExternalIdentity, UpdateHuman, DeleteHuman on HumanPrincipal; systemauth.User and ErrUserNotFound / ErrUserExists are replaced by HumanPrincipal, ErrPrincipalNotFound, ErrPrincipalExists.
  • OAuth tokens and authorization codes use the subject column; OAuthConsent.user_id is principal_id; OAuthApp.owner references Principal.
  • Human.email is optional; HumanPrincipal.Email, principal.Human.Email, and contract identity types may be empty for a managed principal. Existing creation paths still require an email.
  • invite.Invite has no Token field; the token is only in InviteResult.InviteURL.
  • The authz providers are named for the mode they evaluate: authz/simple is now authz/rbac (role-based: roles plus direct grants), and authz/spicedb is now authz/rebac/spicedb (relationship-based, with SpiceDB as the engine). The old paths remain as deprecated forwarding packages for this release; change imports to the new paths, and staticcheck reports any you miss.
  • spicedb.BaseSchema equals authz.BaseSpiceDBSchema and is deprecated: members hold contribute, not edit, on an organization, and editors are recognized.
  • Interfaces gained methods: authz.RelationshipSyncer (AddPrincipalManager, RemovePrincipalManager) and credential.Service (IssueLoginToken, VerifyLoginToken).
  • organization.Service ListMembers and ListMemberships exclude inactive memberships unless IncludeInactive is passed.
  • identity/token returns typed errors: ErrInvalidAccessToken, ErrInvalidRefreshToken, ErrRefreshTokenReused, ErrDPoPMismatch, ErrScopeNotGranted.
  • marketplace.LicenseService gains GetByStripeSubscriptionID.
  • identity/delegation returns typed errors, Link gains Active, and an agent without a delegation is denied.
  • signup.SignupOrGetExisting returns an existing account only when SignupInput.EmailVerified is set; SignupWithInvite requires the invited email.
  • apikey.Service Get, Revoke, and Delete take the owning principal first; use RevokeInOrganization for an administrator of an organization-scoped key. A service with no AllowedScopes refuses keys that request scopes unless AllowAnyScope is set, and MaxKeysPerUser is enforced.
  • rls.Helper.SetTenant and SetUser return ErrNotInTransaction for *sql.DB and *sql.Conn, because SET LOCAL outside a transaction scopes nothing. rls.Middleware.SetRLSContext only puts the caller on the request context; run queries through Middleware.WithTx or rls.WithTenant.
  • principal lookups return principal.ErrNotFound; type-specific updates return ErrWrongType; CreateAgent returns ErrInvalidDelegator.

5. Behavior changes

  • The default BFF session cookie is sf_session (was cf_session); browsers holding the old cookie sign in again.
  • SCIM users are human principals; users provisioned before this release are not visible through SCIM. Managed principals are never SCIM users.

6. Stripe webhooks

Subscribe your Stripe webhook endpoint to checkout.session.async_payment_succeeded in addition to checkout.session.completed: licenses for delayed payment methods are now granted when the payment clears, not when checkout completes. Subscribing to customer.subscription.deleted makes cancellation revoke the license.

7. User authentication for the authorization endpoint

systemauth.WithUserIDHeader is removed, and DefaultSessionProvider no longer believes any header on its own: it sends every authorization request to the login URL. A deployment that relied on a gateway setting X-User-ID must say so, and say which requests may be believed:

gate, err := systemauth.TrustedProxyNetworks("10.0.0.0/8") // the gateway's addresses
if err != nil { return err }
provider := systemauth.NewDefaultSessionProvider(
    systemauth.WithTrustedUserIDHeader("X-User-ID", gate),
)

Use systemauth.TrustAllRequests only when the server is reachable solely through a gateway that overwrites the header; the gate judges the direct peer address, never X-Forwarded-For. Social login and custom SessionProviders need no change. identity/oauth takes oauth.WithUserAuthenticator (or WithTrustedUserIDHeader) and WithConsent; without them its authorization endpoint answers server_error or access_denied.

8. Proxies, SCIM, federation, and introspection

  • Behind a reverse proxy, tell SystemForge which proxies to believe, or every client will share the proxy's address: ratelimit.ForwardedIPKey("10.0.0.0/8") for limits, bff.ClientIPConfig{TrustProxy: true, TrustedProxies: ...} for sessions (and CloudflareIPRanges for Cloudflare), systemauth.WithLoginTokenClientIP(...), and an IPExtractor for the API key middleware.
  • SCIM now answers 403 until you pass scim.WithAuthorizationHook (for example scim.NewScopedAuthorizationHook(true)).
  • Federation: pass systemauth.WithFederationAuthenticator(systemauth.SharedSecretAuthenticator(secret)) to NewFederationEndpoints, or the sync and lookup routes are not registered.
  • Introspection: resource servers must authenticate as a confidential client or present a token carrying the introspect scope.
  • Rate limiting fails closed: a storage outage answers 503. Pass ratelimit.WithFailOpen() to keep the old behavior.
  • Tokens: session/jwt and the contract middleware require exp; use jwt.ValidateAccessTokenForAudience or middleware.HTTPAuthForAudience on routes that serve one kind of client.
  • Lockout: call Lockout.Begin before verifying a password and Attempt.Success or Failure after; CheckAndRecord is deprecated.
  • multiapp apps that share a signing key should set MiddlewareConfig.AppAudiences and mint tokens with jwt.TokenOptions{Audience: ...}; apps with their own keys need nothing.
  • multiapp: AppAwareDB.ForSchema returns an error; SchemaDB.AcquireConn is deprecated in favor of Begin.

9. Managed principals (optional)

Nothing changes unless you opt in. To enable, set managed_principals.enabled in the SystemAuth configuration and managed_principals: true on each client that may use them, then wire identity/managedprincipal. If you use the marketplace, add stripe.WithPurchaserChecker(managedPrincipalService) so managed principals cannot purchase.

Organization model

The next sections cover the organization model; they apply to every deployment.

10. Organization roles and the plan

  • editor and viewer are no longer organization roles. The roles are owner, admin, member and collaborator; any other value is refused with ErrInvalidRole. Replace them in your own tables, vocabularies and invites; use resource roles for access to one resource.
  • Remove Plan from organization inputs and filters, and from API requests. Supply APIConfig.Entitlements with marketplace.EntitlementSource to return entitlements.

11. Entitlements fail closed

An approle.Service with no entitlement source entitles no organization to any application. Build it with marketplace.ApproleOptions(src), and declare applications free to everyone with marketplace.WithDefaultEntitlements. approle.EntitleAll is for development and tests. marketplace.WarnIfNoEntitlements logs a startup warning when nothing entitles anyone.

12. Relying parties read claims

The default membership source is ClaimsMemberships, which decodes the orgs claim. LocalMemberships is no longer picked up implicitly; pass it explicitly for development and tests. Use ClaimsOrRemoteMemberships when the claim may be truncated or absent.

13. Token lifetime and revocation

The default access token lifetime is 10 minutes (it was 15), a configured value above 15 minutes is refused at load, and per-client values are capped. Pass a relyingparty.RevocationCache to BearerMiddleware and NewBFF, and feed it as described in Access-change invalidation.

14. Authorization schema and packages

  • The marketplace SpiceDB fragment attaches to the base organization through a marketplace_org facet and no longer redefines principal or organization. Compose schemas with MergeSchema, or let authz/mode.New compose and write them.
  • Build the provider from configuration with authz/mode.New. The authz/simple and authz/spicedb forwarding packages (renamed in this release) remain deprecated and will be removed in the next major release; import authz/rbac and authz/rebac/spicedb.
  • Switching to rebac starts with an empty graph; write existing memberships, application roles and grants through the syncer before serving requests.

New Features

  • identity/managedprincipal: Create, AddManager, RemoveManager, ListManagers, ManagerRoles, IsManaged, IssueLoginToken, ListLoginTokens, RevokeLoginToken, ListSessions, RevokeSession, RevokeAll, IssueClaim, RedeemClaim, AttachVerifiedEmail, and NewHandler.
  • SystemAuth: POST /login-token, the managed claim, Server.ManagedPrincipalsAllowed, Server.LoginTokenPolicy, WithCredentialService, WithLoginTokenRateLimit.
  • identity/credential: login tokens and LoginTokenPolicy.
  • authz: OrgRolePermissions, PermOrg*, ManagerRolePermissions, PermManage*, NewPrincipalResource; rbac.WithManagerRoleGetter; providertest.RunOrgPermissionTests and RunManagerRelationTests; spicedb.Client.DeleteRelationships (now under authz/rebac/spicedb).
  • identity/requestprincipal: RequestPrincipal, authenticator chain and middleware, PrincipalUUID; aauthrs for AAuth agents; relyingparty.NewRequestAuthenticator.
  • marketplace: WithAuthorizer, WithPurchaseAuthorizer, OrgRolePurchaseAuthorizer, WithCallerResolver, PurchaserChecker; stripe.WithPurchaserChecker.
  • organization: MembershipRoleGetter, ErrMembershipNotFound, ListOrganizationsInput.MemberPrincipalID.
  • identity/pgschema (PostgreSQL lower(email) unique index), identity/clientassertion JWT ID replay protection, and store.Config.TrustProvisionedEmail for SCIM.
  • Signup on first social login (signup.SlugForLogin, CreatePersonalOrgInTx, EnsurePersonalOrgInTx); reserved slugs (organization.ReservedSlugs, WithReservedSlugs); Rename, ResolveSlug, WithSlugCoolingOff; ConvertToTeam; personal organization rules (ErrPersonalOrganization).
  • identity/approle: Assign, Change, Remove, RemoveForMember, EffectiveRoles, endpoints under /organizations/{slug}/members/{principal_id}/app-roles, and authz.AppRoleSyncer.
  • marketplace: EntitlementSource, ApproleOptions, WithDefaultEntitlements, WarnIfNoEntitlements; listings may name an application and subscriptions may be per listing.
  • systemauth: the orgs claim, GET /oauth/userinfo/memberships, GET /access-changes, NewBackchannel, and the backchannel_logout_uri client setting.
  • identity/accesschange, relyingparty.RevocationCache, NewAccessChangePoller, Replay, BackchannelHandler; relying-party membership sources ClaimsMemberships, RemoteMemberships, ClaimsOrRemoteMemberships, ProvisioningMemberships.
  • identity/resourcegrant, the resource-role ladder and authz.ResourceGranter; rbac.WithResourceGrants; resource-targeted invites.
  • authz.ModeConfig, authz/mode.New and systemauth.Config.Authz.
  • identity/managedprincipal: Release, manager invitations with accept and decline, Delete, and the managing operations over HTTP.

Fixes

  • The marketplace, organization, and invite APIs panicked at registration.
  • The simple provider denied every organization permission check.
  • SCIM listing panicked on every ascending sort.
  • identity/oauth.NewProvider panicked on construction, and token revocation had no effect.
  • private_key_jwt client authentication failed under foreign-key enforcement.
  • A managed principal's first relying-party sign-in failed; empty emails could be matched or adopted; invite emails were compared case-sensitively; organizations created without a plan failed.
  • Accepting an invite, and social login signup, register memberships, principals and organizations with the authorization graph.
  • Signup creates the principal and personal organization in one transaction.
  • The application-role seat limit cannot be exceeded by concurrent assignments, and assigning with no seat left answers 409.
  • Manager authority checks for AddManager, RemoveManager and Release run inside the row-locked transaction, a managed principal is never a valid manager, and access-change reasons are accurate.

The complete list is in the changelog.

Documentation

What's Next

Verified email for SystemAuth through omnimail (INIT-SYSTEMFORGE-006), which also drives the managed-principal attach-email flow.