Reaktor DocsSecurity and authClaude

Security and auth · Claude

Auth Architecture and Redesign

Principal, token, session, authorization, and adapter redesign plan for reaktor-auth.

Use whenUse when working on auth kernel boundaries and API shape.
SourceClaude
Route/docs/auth-analysis

01 Governing Principle

reaktor-auth is not a login library. It is a principal, token, session, and authorization kernel that happens to support Google, Apple, and Supabase-backed email auth.

The important boundary is Reaktor-issued access tokens, not Google/Apple/Supabase tokens. External providers authenticate the person. Reaktor authenticates and authorizes the call inside the Reaktor ecosystem.

The philosophical API direction: the interface should be simple at the surface, expressive underneath. The application developer should express who can do what on which resource under which app/tenant/context — not manually parse JWTs, fetch users, check string scopes, and throw 401/403.

val createEvent = post<CreateEvent, EventCreated>(
    "/events",
    auth = requires(EventAction.Create)
        .on(EventResource.fromBody { it.eventGroupId })
) { req ->
    val auth = req.auth
    events.create(auth, req.body)
}

The biggest redesign decision: stop treating auth as “login returns User + token” and make it “all calls enter with an AuthContext, and every protected capability declares an AuthRequirement.” Everything else follows from that.

02 Target Architecture

1 External credential provider (Google / Apple / Supabase)
2 Reaktor identity normalization
3 Global identity + app membership + tenant/context grants
4 Session + refresh token, or service credential
5 Short-lived Reaktor access token (JWT)
6 Middleware creates AuthContext
7 Authorizer checks Requirement against Resource
8 Service / Worker / Graph / Actor handler executes

Every target — Spring server, Cloudflare Worker, KMP app graph, actor mailbox, CLI tool — converges at steps 6–8. The transport-specific part (steps 1–5) only adapts credentials into the common kernel.

03 User-to-Service Auth

A human user signs in through an app-branded provider flow. Reaktor maps that external credential to a global identity and app membership, creates a session, and issues short-lived Reaktor access tokens that services verify.

1 App starts Google, Apple, or Supabase auth
2 Provider returns external credential (ID token / auth code / session)
3 App sends credential to Reaktor with appId, provider, platform, nonce
4 Reaktor verifies credential against app’s configured provider client
5 Reaktor upserts identity, provider account, principal, membership
6 Reaktor creates session + rotating refresh token + access token
7 Client calls services with Authorization: Bearer <reaktor-access-token>
8 Middleware verifies token, injects AuthContext
9 Handler checks AuthRequirement before domain work

Why not use the provider token as the API token?

  • Google’s aud claim must equal the app’s OAuth client ID — it is a proof of login to that OAuth client, not a general-purpose API credential for every Reaktor service.
  • Google warns not to use email as the stable identifier; use sub instead.
  • RFC 9700 recommends audience-restricting access tokens to specific resource servers and refresh-token rotation for public clients.

Reaktor Access Token Claims

{
  "iss": "https://auth.reaktor.build",
  "aud": "manna-api",
  "sub": "principal_usr_123",
  "jti": "tok_123",
  "sid": "ses_123",

  "principal_type": "user",
  "identity_id": "idn_123",
  "app_id": "app_manna",
  "tenant_id": "tenant_wedding_456",
  "context_id": "event_group_789",

  "scp": ["event.read", "event.write"],
  "perm_ver": 42,

  "amr": ["google"],
  "iat": 1710000000,
  "exp": 1710000900
}

04 Service-to-Service Auth

Service-to-service auth must not be modeled as “a fake user.” It is a first-class service principal.

A. Pure Service Principal

“manna-worker is calling manna-api.” Uses client_credentials grant with service account.

B. Tool / Developer Credential

“CLI/MCP/tool calls API using a PAT, then exchanges it for a short-lived JWT.”

C. User-Delegated Service Call

“manna-worker calls on behalf of user U after U triggered a long-running job.”

A. Client Credentials Flow

POST /auth/token
grant_type=client_credentials
client_id=svc_manna_worker
client_secret=...
audience=manna-api
scope=knowledge.ingest

Token issued with sub: "principal_svc_manna_worker", principal_type: "service". OAuth 2.0 RFC 6749 explicitly supports this grant type for non-user service access.

B. PAT Exchange Flow

PATs are exchange credentials, not direct bearer credentials. The tool keeps the rak_ token locally, exchanges it for a 5–15 minute JWT via POST /auth/token grant_type=pat, then workers verify the JWT locally without database hits.

C. On-Behalf-Of (Delegated Actor)

{
  "sub": "principal_usr_123",
  "act": {
    "sub": "principal_svc_manna_worker",
    "principal_type": "service"
  },
  "aud": "manna-api",
  "scp": ["job.result.write"],
  "job_id": "job_123"
}

Authorization distinguishes subject (whose resources are affected) from actor (which service is performing the action). This avoids the common bug where a background worker either has too much power as “system” or loses the user context entirely.

05 Global Identity, App-Branded Login

Key insight: You can support a single Reaktor identity across all apps without showing “Login to Reaktor.” Provider UX is app-specific. Identity normalization is Reaktor-global.

Each app has its own configured OAuth client / branding. Google’s consent screen shows the app name, logo, and homepage configured in Google Cloud Console. Apple’s prompt is tied to the app / service configuration. The user never sees “Reaktor.”

Database Model

The database should not say “identity belongs to app.” It should say:

  • Provider client belongs to app
  • Provider account belongs to global identity
  • Membership belongs to app / tenant / context
── Example ──

app
  app_manna       "Manna"
  app_bestbuds    "BestBuds"

auth_provider_client
  app_manna     GOOGLE  ANDROID  client_id=manna-android-client
  app_manna     GOOGLE  WEB      client_id=manna-web-client
  app_bestbuds  GOOGLE  ANDROID  client_id=bestbuds-android-client
  app_bestbuds  APPLE   IOS      bundle_id=dev.shibasis.bestbuds

identity
  idn_123

provider_account
  identity_id=idn_123  provider=GOOGLE  subject=<google_sub>

membership
  principal_usr_123  app_manna     ACTIVE
  principal_usr_123  app_bestbuds  ACTIVE

Login validation: appId + provider + platform → expected issuer + expected audience/clientId. Do not auto-link accounts only by email. Use provider subject as the primary key.

Supabase is treated as a credential provider, not as the Reaktor authorization system. Spring calls Supabase server-side using the service role key. The Reaktor client does not need to understand Supabase sessions.

Email / Password

POST /auth/login/email → Spring calls Supabase Auth → validates email/password → maps supabase_user_id to ProviderAccount → issues Reaktor tokens.

Magic Link

POST /auth/magic-link/start → Supabase sends email. POST /auth/magic-link/confirm → Spring verifies with Supabase → maps user → issues Reaktor tokens.

Supabase admin methods require the secret key and must be called only from trusted servers. The Supabase JWT does not become the Reaktor API token.

07 Core Domain Model

The main correction from the old model: split identity, principal, and membership into distinct concepts.

ConceptDefinitionKey Fields
IdentityGlobal human identity (one per person across all apps)id, status, primaryEmail, timestamps
ProviderAccountOne login method attached to an identityidentityId, provider, issuer, subject, email, providerTenant
PrincipalThe entity that can be authorized (user, service, agent, actor)id, kind, identityId?, status
MembershipApp/tenant/context-specific attachmentprincipalId, appId, tenantId?, contextId?, status, profile
AuthProviderClientApp-branded OAuth configurationappId, provider, platform, clientId, issuer, redirectUris
TenantOrganizational unit within an appid, appId, name, status
ResourceRefPointer to a specific protected resourcetype, id, appId, tenantId, contextId

ProviderAccount Unique Keys

ProviderissuersubjectproviderTenant
Googlehttps://accounts.google.comGoogle sub
Applehttps://appleid.apple.comApple user identifierApple Team ID
Supabasehttps://<project>.supabase.co/auth/v1Supabase user IDSupabase project ref

Principal Kinds

USER

Human user. Has an Identity. Created at first login.

SERVICE

Service account. No Identity. Has client credentials.

AGENT

AI agent or autonomous system. Future-ready.

ACTOR

Graph actor / message-processing entity. Future-ready.

08 Token Model

CredentialFormatLifetimePurpose
External provider credentialGoogle ID token / Apple identity token / Supabase sessionLogin onlyProves identity to external provider
Reaktor access tokenJWT (ES256/RS256, asymmetric JWKS)5–15 minUsed by APIs, workers, graph services
Refresh tokenOpaque random, hashed server-sideDays/weeksRotates to get new access tokens. User sessions only.
PAT / service credentialrak_ prefix, hashed server-sideLong-livedExchange-only — used to obtain short-lived access tokens

Asymmetric Signing

Prefer ES256 or RS256 with JWKS. Workers and services verify with public keys — no shared signing secret needed. HS256 should not be the default because every verifier would need the signing secret.

Refresh Token Rotation

refresh_token table:
  id
  session_id
  token_hash
  family_id          // for reuse detection
  previous_token_id
  created_at, used_at, rotated_at, expires_at, revoked_at

Every refresh rotates: old token → new access + new refresh. Old token becomes invalid. Reuse of old token revokes entire family (RFC 9700).

PAT Binding

PATs become app/audience-bound: appId, tenantId, contextId, allowedAudiences, scopes. The current model has userId, name, tokenHash, scopes, nullable contextId — but no required appId or audience binding.

09 AuthContext & AuthRequirement

AuthContext is the single most important primitive. It is the answer to “who is calling, for what app/tenant/context, with what authority?” Available everywhere: request.auth, workerContext.auth, graph.auth, actorMessage.auth.
data class AuthContext(
    val principal: PrincipalRef,
    val identityId: String?,
    val appId: String,
    val tenantId: String?,
    val contextId: String?,
    val sessionId: String?,
    val tokenId: String?,
    val credentialId: String?,
    val issuer: String,
    val audience: String,
    val scopes: Set<Scope>,
    val roles: Set<RoleRef>,
    val permissions: Set<PermissionRef>,
    val method: AuthMethod,
    val actor: PrincipalRef? = null,
    val delegation: Delegation? = null,
    val claims: JsonObject = JsonObject(emptyMap())
)

Context Shapes by Auth Type

Scenarioprincipal.kindidentityIdsessionIdactor
User → serviceUSERpresentpresentnull
Service → serviceSERVICEnullnullnull
Service on behalf of userUSERpresentnullSERVICE ref

AuthRequirement DSL

The current SecuredPort model checks required string scopes. That becomes the beginner path, but the kernel supports composable, resource-aware requirements:

// Simple scope check
auth = requires("event.write")

// Resource-aware
auth = requires(Event.Write)
    .on(EventResource.fromPath("eventId"))

// Multi-actor
auth = anyOf(
    requires(Event.Write).forUsers(),
    requires("event.import").forServices()
)

// Tenant-bound
auth = requires(Event.Write)
    .inTenant { path["eventGroupId"] }

// Delegated actor
auth = requires(Job.WriteResult)
    .allowDelegatedActor("manna-worker")

10 Authorization Algorithm

1 Is there an AuthContext? — No → 401
2 Does token match required issuer/audience? — No → 401
3 Is principal/session/credential active? — No → 401
4 Is app/tenant/context compatible with route/resource? — No → 403
5 Does requirement allow this principal kind? — No → 403
6 Does principal have required scopes/permissions/roles? — No → 403
7 If resource-bound, does grant apply to this resource? — No → 403
8 If delegated, is actor allowed to act for subject? — No → 403
9 Allow
sealed interface AuthDecision {
    data class Allow(val context: AuthContext) : AuthDecision
    data class Deny(
        val reason: AuthDenyReason,
        val statusCode: Int,
        val safeMessage: String
    ) : AuthDecision
}

This is already implemented in LocalAuthorizer in the kernel — it evaluates constraints, anyOf/allOf composition, scopes, permissions, roles, resources, and delegation in the correct order.

11 Current Implementation Audit

What Already Exists

AreaStatusDetails
Kernel typesDoneAuthContext, AuthRequirement, AuthDecision, Principal, Identity, ProviderAccount, Membership, AuthProviderClient, Tenant, ResourceRef, all enums
Kernel interfacesDoneAuthenticator, Authorizer, TokenVerifier, TokenIssuer, SessionManager, PermissionResolver, TenantResolver
LocalAuthorizerDoneFull authorization algorithm with constraint validation, anyOf/allOf, scopes, permissions, roles, resources, delegation
DSL functionsDonerequires(), permits(), anyOf(), allOf(), resource(), fluent methods on AuthRequirement
Secured graph portsDoneSecuredProviderPort, SecuredConsumerPort, providesSecured(), consumesSecured() — now using AuthRequirement
AuthNodeDoneGraph node with AuthSession state, provider port, DB rehydration, login/logout
Platform providersPartialGoogle (Android, iOS, Web), Apple (iOS, Web), Android Apple TODO
Auth service APIDonelogin, token, mintPat, verifyPat, exchangePat + TokenRequest/TokenResponse with grant types
PAT systemDoneSecure random generation, hash-only storage, revocation, exchange to JWT
RBAC DTOsDoneApp, User, Role, Permission, Session, Context, RolePermission, UserRole, PersonalAccessToken
Spring serverPartialAuthServer routes, LoginInteractor, TokenInteractor, JwtVerifier, JwtMinter, Exposed tables/repos
UI componentsDoneGoogleLoginButton, AppleLoginButton, icons

Critical Gaps

GapImpactCurrent State
DefaultSecurityConfig permits allNo auth boundary on Springauthorize(anyExchange, permitAll) + CSRF disabled. Actual protection depends on handler-local checks.
Fake refresh tokensNo session lifecycleLoginInteractor returns UUID.randomUUID().toString() as refresh token with no persistence, rotation, or revocation.
No real sessionsCannot revoke accessNo server-side session store. No /refresh or /logout endpoints.
No app-branded provider clientsCannot multi-appLogin verifies against hardcoded audience, not per-app AuthProviderClient.
No identity/principal splitFlat user modelOld User table conflates identity, principal, and membership into one row.
PATs not audience-boundOver-privileged tokensPAT has userId, scopes, contextId but no required appId or allowedAudiences.
No JWKS endpointWorkers can’t verify locallyNo /.well-known/jwks.json. Workers would need the signing secret or a DB hit.
No worker middlewareCloudflare unprotectedNo WorkerJwtVerifier, no JWKS cache, no Hono middleware.
No Supabase providerEmail/magic link blockedNo server-side Supabase integration.
No service accountsNo S2S authNo client_credentials grant. No ServiceAccountService.
Apple iOS issuesCrashes on 2nd loginForce-unwraps email (it.email!!), logs serialized user, doesn’t persist first-login name/email.

12 Planned API Tree

reaktor-auth ├── kernel (commonMain, pure KMP) │ ├── model │ │ ├── Identity, ProviderAccount, Principal, Membership │ │ ├── Tenant, Context, ResourceRef, Action │ │ ├── Permission, Role, Grant │ │ ├── AuthContext, AuthRequirement, AuthDecision, AuthError │ │ └── PrincipalRef, Scope, RoleRef, PermissionRef │ ├── authn │ │ ├── Authenticator, CredentialVerifier │ │ ├── ExternalIdentityVerifier, ReaktorTokenVerifier │ │ ├── SessionManager, RefreshTokenManager │ │ └── TokenIssuer, TokenExchange │ ├── authz │ │ ├── Authorizer, LocalAuthorizer │ │ ├── PermissionResolver, GrantResolver │ │ ├── TenantResolver, ResourceResolver │ │ └── PolicyEvaluator │ ├── credentials │ │ ├── UserSessionCredential, ServiceCredential │ │ ├── PersonalAccessTokenCredential │ │ └── GoogleCredential, AppleCredential, SupabaseCredential │ └── dsl │ └── requires(), permits(), resource(), anyOf(), allOf() │ ├── service (commonMain service API) │ ├── AuthService — externalLogin, emailPasswordLogin, magicLink, refresh, logout, me │ ├── TokenService — token (client_credentials / pat / on_behalf_of), introspect, revoke, jwks │ ├── SessionService — listSessions, revokeSession │ ├── PatService — createPat, listPats, revokePat, exchangePat │ ├── ServiceAccountService — create, list, credentials, rotate, revoke │ ├── AppAuthService — apps, providerClients, tenants, memberships, roles, grants │ └── AuditService — listAuditEvents │ ├── client (commonMain + platform) │ ├── ReaktorAuthClient — loginWithGoogle/Apple/Email, magicLink, refresh, logout │ ├── AuthSessionStore, SecureTokenStore, AccessTokenCache │ ├── AuthHttpPlugin — auto-attach bearer, auto-refresh │ └── AuthGraphPlugin — wire AuthContext into graph │ ├── providers (platform-specific) │ ├── google/ — Android, iOS, Web, GoogleTokenVerifier (JVM) │ ├── apple/ — iOS, Web, AndroidAppleWebLogin, AppleTokenVerifier (JVM) │ └── supabase/ — SupabaseEmailPasswordProvider, SupabaseMagicLinkProvider (JVM) │ ├── spring (jvmMain) │ ├── ReaktorSecurityWebFilter │ ├── ReaktorReactiveJwtDecoder │ ├── ReaktorAuthorizationManager │ ├── AuthContextServerWebExchangeAdapter │ └── RouteAuthDsl │ ├── cloudflare (jsMain — new) │ ├── ReaktorWorkerAuthMiddleware │ ├── WorkerJwtVerifier (crypto.subtle) │ ├── WorkerJwksCache (KV-backed) │ └── HonoAuthContextAdapter │ ├── graph (commonMain) │ ├── AuthNode, AuthContextPort │ ├── requiresAuth(), providesSecured(), consumesSecured() │ └── ActorAuthEnvelope │ └── cli (new) └── reaktor auth login / create-app / configure-provider / mint-pat / inspect-token

New items are marked in green. The kernel types and interfaces are already implemented; the gaps are primarily in Spring middleware, worker middleware, Supabase provider, service accounts, and real session lifecycle.

13 JVM / Spring Target

Security Configuration

Replace the current permitAll default with proper route-aware auth. Use both Spring Resource Server (validates Reaktor JWT) and a Reaktor WebFlux filter (maps to AuthContext, calls Authorizer).

// Public routes
/auth/login/*
/auth/magic-link/*
/.well-known/jwks.json

// Protected (everything else)
authorize(anyExchange, authenticated)

Handler-Level Auth (Reaktor-Native)

val createEvent = PostHandler<CreateEvent, EventCreated>(
    route = "/events",
    auth = requires(Event.Write)
        .on(EventResource.fromBody { it.eventGroupId })
) { req ->
    eventService.create(req.auth, req.body)
}

// Spring router wraps automatically:
//   extract bearer → authenticate → attach AuthContext
//   → evaluate handler.auth → call handler

Spring Service Routes

PathMethodPurpose
/auth/login/googlePOSTGoogle login
/auth/login/applePOSTApple login
/auth/login/emailPOSTSupabase email/password
/auth/magic-link/startPOSTSend magic link
/auth/magic-link/confirmPOSTConfirm magic link
/auth/session/refreshPOSTRotate refresh token
/auth/session/logoutPOSTRevoke current session
/auth/session/logout-allPOSTRevoke all sessions
/auth/session/meGETCurrent user/session info
/auth/session/sessionsGETList active sessions
/auth/tokenPOSTToken exchange (client_credentials / pat / on_behalf_of)
/auth/token/introspectPOSTToken introspection
/auth/token/revokePOSTToken revocation
/.well-known/jwks.jsonGETPublic signing keys

Required Stores

IdentityStore, ProviderAccountStore, PrincipalStore, MembershipStore, SessionStore, RefreshTokenStore, ServiceAccountStore, PatStore, GrantStore, SigningKeyStore, AuditStore. The existing repository/RBAC work can be preserved; the current User table should become principal + membership or a compatibility view.

14 Cloudflare Workers Target

Primary requirement: no DB hit per request, no shared signing secret, fast local verification.

1 Worker request arrives
2 Middleware extracts Authorization: Bearer
3 WorkerJwtVerifier fetches/caches JWKS (KV-backed)
4 Verify signature using crypto.subtle
5 Validate iss/aud/exp/nbf/app_id → build AuthContext
6 Route requirement check → handler executes
val auth = reaktorWorkerAuth {
    issuer = "https://auth.reaktor.build"
    audience = "manna-mcp"
    jwksUrl = "https://auth.reaktor.build/.well-known/jwks.json"
    cache = WorkerJwksCache(kv = env.AUTH_KV)
}

app.use("*", auth.middleware())

app.post("/mcp", requires("mcp.read").forUsersOrServices()) { c ->
    val auth = c.auth
    mcp.handle(auth, c.req)
}

Hono’s JWT middleware follows the same shape. Cloudflare Workers expose Web Crypto via crypto.subtle, which is the right primitive for JWT signature verification.

15 Android / iOS / Web Clients

App-Facing API

Feature.Auth.configure {
    appId = "app_manna"
    authBaseUrl = "https://auth.reaktor.build"
    providers {
        google()
        apple()
        supabaseEmail()
        supabaseMagicLink()
    }
}

Feature.Auth.login(Google)
Feature.Auth.login(EmailPassword(email, password))
Feature.Auth.sendMagicLink(email)
Feature.Auth.confirmMagicLink(tokenHash)

Feature.Auth.currentSession.collect { session -> /* update UI */ }

Client Token Behavior

  1. Store refresh token securely (Keychain / EncryptedSharedPreferences / HttpOnly cookie)
  2. Keep access token in memory when possible
  3. Attach access token to Reaktor service calls via AuthHttpPlugin
  4. Refresh before expiry
  5. On refresh failure, clear session and emit logged-out state

Storage Split

StoreContentsMobileWeb
AuthSessionStoreUser/session metadataObjectStoreMemory / localStorage
SecureTokenStoreRefresh token / credentialKeychain / EncryptedPrefsHttpOnly Secure cookie
AccessTokenCacheCurrent access tokenMemoryMemory

Apple iOS Fixes Required

  • Do not force-unwrap email (it.email!!) — Apple only sends name/email on first authorization
  • Persist name/email received only on first login as profile hints
  • Assign successful AppleUser into the MutableStateFlow
  • Remove token logging

16 Graph & Actors

This is where Reaktor can be better than Clerk/Auth0/Firebase. Auth travels with causality through the graph.

Graph Auth

class AuthNode(graph: Graph) : BasicNode(graph) {
    val authState: StateFlow<AuthContext?>
    val authPort by provides<AuthContextProvider>(...)
}

// Secured port with requirement
val eventRepository by consumesSecured<EventRepository>(
    requires(Event.Read)
        .on(EventResource.fromGraphContext())
)

Actor Messages

Actor messages carry auth explicitly so authorization context is never lost across async boundaries:

data class AuthEnvelope<T>(
    val auth: AuthContext,
    val message: T,
    val causationId: String?,
    val correlationId: String?
)

actor<EventCommand>(
    auth = anyOf(
        requires(Event.Write).forUsers(),
        requires("event.import").forServices()
    )
)

If a user action triggers a service job which sends an actor message, the actor sees both the subject and the service actor.

17 Tools, CLI & MCP

Tools use PATs or service accounts, then exchange for scoped JWTs.

$ reaktor auth login
$ reaktor auth mint-pat --app manna --audience manna-mcp --scope mcp.read
$ reaktor auth token --audience manna-mcp
1 CLI stores PAT locally
2 CLI exchanges PAT for audience-bound JWT
3 CLI calls worker/API with JWT
4 Worker/API verifies locally via JWKS

Service Account Examples

  • manna-ingestion-worker — knowledge base ingest pipeline
  • manna-notification-worker — push notifications
  • bestbuds-chat-worker — message processing
  • reaktor-deploy-agent — CI/CD automation

18 Migration Roadmap

Step 1: Kernel types — already done

AuthContext, AuthRequirement, Authenticator, Authorizer, TokenVerifier, TokenIssuer, SessionManager, LocalAuthorizer, DSL functions. Keep current LoginResponse but enrich with TokenSet.

Step 2: Real sessions & refresh tokens

Replace UUID.randomUUID() refresh token with persisted, hashed, rotating refresh tokens. Add POST /auth/session/refresh, POST /auth/session/logout, GET /auth/session/me.

Step 3: Spring middleware

Replace permitAll with route-aware auth. Add ReaktorSecurityWebFilter, ReaktorReactiveJwtDecoder. Public routes: /auth/login/*, /auth/magic-link/*, /.well-known/jwks.json.

Step 4: App-branded provider clients

Add auth_provider_client table. Update login verification: appId + provider + platform → expected clientId/audience.

Step 5: Identity / principal / membership split

Keep compatibility mapping: old User → new Principal + Membership. Migrate data gradually.

Step 6: Service accounts & PAT audience binding

Add ServiceAccountService, TokenService grant_type=client_credentials, PAT allowedAudiences + appId.

Step 7: Worker middleware

JWKS endpoint on Spring. Worker JWKS cache (KV). WorkerJwtVerifier with crypto.subtle. Hono/Reaktor middleware.

Step 8: Supabase provider

POST /auth/login/email, POST /auth/magic-link/start, POST /auth/magic-link/confirm. Spring calls Supabase server-side. Reaktor issues its own tokens after Supabase verification.


Mental Model

Identity

Who is this human globally?

Principal

What authenticated entity is acting? (user / service / agent / actor)

Membership

Where does this principal belong? (app / tenant / context)

Credential

How did they prove identity? (Google / Apple / Supabase / secret / PAT)

Session

Is this login still alive?

Token

What short-lived proof is presented to this service?

Requirement

What does this route/port/actor need?

AuthContext

The answer to “who is calling, for what app/tenant/context, with what authority?”

Every target converges on the same kernel. Apps, servers, workers, tools, graph nodes, and actors all see AuthContext and declare AuthRequirement. The transport varies. The authorization model does not.