# API-owned authentication and legacy compatibility Status: API-owned authentication is the retained web's only login path, 30 October 2026. The [API runtime guide](../../../api/adaptors/authentication/README.md) records configuration and qualification. The retained NextAuth session owner and its signed-assertion bridge have been retired. The API owns login and sessions; Themelio validates those sessions through its compatibility adaptor. ## Ownership and retained behaviour | Boundary | Responsibility | | --- | --- | | Go API authentication edge | OIDC start/callback, verified AAI evidence, technical browser sessions, session validation, logout and scoped access decisions. | | Beto Party Governance | Stable Party identity, authentication eligibility/generation, AAI attribute projection and verified provider bindings through published commands/queries. | | Themelio legacy auth adaptor | Implement the existing server-side auth/session interface using API authority and adapt retained route shapes. It does not own new provider decisions or privileged credentials. | | Retained onboarding coordinator | Resume durable signup-time GitLab account/token and personal namespace setup, and request Waldur association through owner-defined closed ports. It records progress and hands qualified proof to Party Governance. | | Waldur integration process | Implement separate identity-association and external-invitation adaptors. It alone receives the Waldur staff credential and exposes a different scoped capability for each port. | | Retained AUP implementation | Present, read and write existing GitLab-backed acceptance until Phase 4. Expose a separately authenticated policy-only read to the API. | | Beto Project | Canonical Project/member authority and Project fulfilment through its provider-neutral port, including required provider-user prerequisites. | Keycloak remains the identity provider. The API owns its client protocol and delegates canonical identity decisions to Party Governance over typed RabbitMQ. OIDC, cookies, token storage and retained user response shapes stay outside the domain and application layers. Keep the working GitLab identifiers, namespaces, repositories, permissions and provider-specific user fields used by unmigrated features. The auth adaptor maps a verified Party/provider association to the existing user shape. It must never substitute PartyId for a numeric GitLab user ID or derive ownership from an email. Provider tokens remain in restricted server-side compatibility storage; canonical session state carries opaque references where needed. This is a refinement of the original coexistence wording. Preserving an existing writer does not assign ownership of new behaviour to the process that happens to host it. The previous wording allowed Waldur account and invitation decisions to land in Next.js and the general compatibility worker. The refined boundary keeps those callers as coordinators or HTTP adaptors and places each provider operation behind the Party-owned port it implements. ## One session authority across both endpoint families Use one API-owned canonical browser session. The preferred deployment exposes both route families under the same public origin so the browser presents its HttpOnly, Secure, appropriately scoped cookie to either family. The edge routes migrated paths to Go and retained paths to Next.js. Cookie scope, SameSite, trusted origins and CSRF protection must be qualified in the actual deployment. The legacy adaptor validates the canonical session server-to-server with the API before exposing a user or provider credential. Its cached display attributes and credential handle confer no independent authority. The API checks current Party eligibility and authVersion; owner commands still check their business authority. On unavailable authority, both route families return an explicit unavailable or pending outcome. Party rejection prevents normal access through either family. The retained web exposes only a scoped policy-acceptance read, resolving the verified Party/GitLab association server-side. This read bypasses browser auth middleware and never calls API session validation. Its service credentials authorise policy lookup only. Missing, stale or unavailable evidence cannot become an accepted result. Historical Party protocols and stored bytes remain unchanged; the deployed retained web issues no legacy session attestations. An accepted Party identity can receive a limited onboarding session before provider setup or AUP acceptance completes. Its route allowlist covers onboarding status, basic self identity, policy presentation/acceptance, explicit resume and logout. Normal business endpoints require current Party eligibility and current AUP; operations using GitLab additionally require their relevant provider readiness. Onboarding realtime subscriptions are equally restricted. Browser delivery uses authorised Centrifugo projections, initial reads and history-gap reconciliation. The legacy `auth()` shape alone is insufficient to enforce this boundary. Audit middleware, server actions, protected layouts, session/token helpers, realtime authorisation and logout call sites. Retained business handlers may stay intact where shared gates cover them. Explicit exceptions require small boundary edits, including an AUP screen that can run before normal business access is available. The current inbound `PRIVATE-TOKEN` pass-through needs a separate qualified policy for provider-token clients; it must not bypass canonical browser/Project authority. Retained state-changing GET routes also need an explicit CSRF-safe transition. ## First-time signup This flow assumes fresh v3 evidence with an eligible exact issuer/subject and a unique `voperson_id`. The [attribute inventory and recovery rules](../../../../contracts/authentication/README.md#identity-attribute-inventory-and-recovery) define every accepted claim and its source. Provider association is qualified during setup; conflicts require operator attention. Orange cards denote facts; only named published contracts are integration events. Blue denotes commands, yellow aggregates, purple policies, green read/status models and red exceptional outcomes. ```mermaid flowchart TD U["Person starts signup"] --> L["Command: start login
Go API"] L --> K["Keycloak authenticates
API validates ID token + UserInfo"] K --> R["Command: ResolveOrRegisterParty"] R --> P["Aggregate: PartyAuthentication
Beto Party Governance"] P --> E["Integration event: PartyRegistered
one PartyId, once"] P --> IA["Durable hand-off: identity attributes"] IA --> IP["Projection: latest AAI attributes
independent revision"] IP --> IC["Integration event:
PartyIdentityAttributesChanged
contact fields only"] IP --> SW["Durable hand-off: qualified Site identity
explicit issuer-to-Site enrolment"] SW --> SB["Command: record Site provider binding
separate PartyProviderBinding root"] SB --> BE SW -.-> SX["Site claim missing / conflict
Site pending or needs attention"] P --> A["Policy: identity accepted
current eligibility and generation"] P -.-> X["Rejected / recovery required
no normal access"] A --> S["Technical fact: limited API session issued"] S --> B["Command: ensure compatibility setup
durable, resumable hand-off"] B --> G["Provider association ports
GitLab retained actor + isolated Waldur identity adaptor
independent progress"] G -.-> Q["Status: provider pending / attention
resume with stable work identity"] G --> V["Command: record verified provider binding
independent Party Governance root"] V --> BE["Integration event:
PartyProviderBindingEstablished"] G -->|"Required GitLab setup ready"| F["Read model: current AUP
retained policy implementation"] F --> C["Command: accept AUP
retained acceptance writer"] C --> H["Fact: legacy acceptance recorded
no canonical PolicyAccepted event yet"] H --> N["Policy: current Party + AUP
and readiness required by the operation"] S --> N BE -->|"Verified association for this operation"| N N --> O["Read model: normal access available"] O --> API["Migrated endpoints
Go API → Beto"] O --> LEG["Retained endpoints
legacy auth adaptor → existing GitLab handlers"] E -.-> M["Source Control: durable event consumers
owner queries and activation pending"] BE -.-> M classDef command fill:#dcecff,stroke:#6b94bd,color:#15283d classDef aggregate fill:#fff0b8,stroke:#b69b43,color:#3d3213 classDef event fill:#ffd7ad,stroke:#c58a4b,color:#432a12 classDef policy fill:#eadffc,stroke:#9b7db5,color:#30223c classDef read fill:#dff1df,stroke:#79a779,color:#203620 classDef hotspot fill:#ffe0e0,stroke:#c65f5f,color:#512020 class L,R,B,V,C,SB command class P aggregate class E,S,BE,H,IC event class A,N policy class F,O,M,IP read class X,Q,SX hotspot ``` Party registration commits its receipt, outcome and outgoing intents atomically. The registration event and private attribute observation can be delivered independently; `GetCurrentParty` reports attributes as pending until the current projection is available. Accepted authentication evidence remains available to the trusted onboarding boundary without treating a stale display projection as identity proof. Attribute changes do not advance authVersion. Persist onboarding work before reporting it as scheduled. Repeated login and browser interruption resume existing work; they cannot depend solely on the one-time `PartyRegistered` event. Account setup has a stable Party/provider-scope identity. Per-session credentials have separate session/generation-bound work. Token issuance after an uncertain result must be reconciled before retrying. Track provider progress independently: a Waldur failure can leave its operations pending while ready GitLab-backed features remain usable. Successful provider work records qualified proof and a durable binding hand-off. The binding has its own revision and commit; provider completion cannot write PartyAuthentication or Project membership in the same transaction. Normal access must also wait for the verified association needed to resolve the operation's credentials and acceptance evidence. Provider failure leaves the Party intact. Signup creates no Project or membership. Existing Waldur membership may resolve after a verified binding arrives; otherwise the user needs the invitation or membership workflow. Metallo receives canonical facts and uses scoped owner queries or its own projections. Signup performs no direct Metallo provisioning. ## GitLab and Project fulfilment Project defines `ProjectProviderPort` for the representation and access it needs. Its GitLab adaptor can ensure required provider users as prerequisites, then reconcile groups and permissions. ProjectFulfilment records desired/confirmed revisions, durable work and redacted delivery status. During coexistence, prerequisite-user operations delegate to the retained account writer. Signup and Project fulfilment must not independently create or modify the same provider account. Transfer an operation only after its replacement passes qualification and its former writer is disabled. Preserve uncertain-effect recovery and stale-grant fencing for each target. Signup-time personal repositories, credentials and Waldur synchronisation stay explicit compatibility operations until their respective consumers move. New provider operations keep administrator credentials outside API/Beto, retained Next.js and the general compatibility worker. Existing retained operations keep their current credential until their own migration. The Waldur integration process exposes separate identity and invitation capabilities, with distinct caller credentials, through closed typed interfaces and qualified network isolation. An Account Provisioning context is outside this slice. Reconsider a separate context only when independent account business rules and consumers require it. ## Waldur association and invitation ports Party Governance owns the binding invariant and the authenticated invitation claim decision. Provider-specific mapping remains in driven adaptors: ```text PartyProviderIdentityPort ensureAssociation(verifiedIdentity, providerScope) verifyAssociation(verifiedIdentity, externalAccountId) ExternalInvitationPort listForRecipient(verifiedRecipientEvidence, boundExternalAccountId) accept(invitationId, verifiedRecipientEvidence, boundExternalAccountId) ``` Account existence is insufficient ownership proof. The Waldur identity adaptor checks the enrolled issuer, configured identity source and existing account provenance before calling Identity Bridge. A username or email match alone leaves the work in `attention_required`. Retried effects keep the same work identity and reconcile the exact external account before publishing binding proof. The preflight cannot make Identity Bridge atomic. A concurrent account creation after the provenance read can be mutated before the adaptor observes the lost race and returns `identity_conflict`. Cutover therefore requires a qualified provider-side condition or evidence that concurrent foreign creation is excluded. Waldur remains the invitation store during coexistence. The API validates the dedicated invitation operation, reads current verified contact evidence from Party Governance and requires the established Party-to-Waldur binding. Each accept command names one invitation. The adaptor records the provider outcome, and Project observes accepted membership through its own Waldur source port. ```mermaid flowchart LR UI["Retained invitation UI"] --> API["Go API session authority"] API --> PG["Party Governance
current contact + binding"] PG --> IP["Party provider identity port"] PG --> XP["External invitation port"] subgraph WI["Isolated Waldur integration process"] IA["Waldur identity adaptor"] XA["Waldur invitation adaptor"] WC["Shared Waldur transport"] IA --> WC XA --> WC end IP --> IA XP --> XA WC --> W["Waldur"] W --> PS["Project-owned Waldur source adaptor"] PS --> P["Project context"] ``` ## Logout, cutover and qualifications API session revocation immediately prevents both endpoint families from accepting that session. Record durable compatibility credential cleanup and clear browser cookies. Party disablement or a changed authVersion fences session authority before asynchronous cleanup. Late callbacks, retries and token issuance results must check the session generation/revocation state and cannot reopen it. Direct provider credentials require their own revocation; show delivery failures without treating them as permission to retain platform access. Deliver this transition before migrating the selected business endpoints: 1. Specify technical OIDC/session, policy-only and compatibility-work contracts. Preserve historical wire and stored-state compatibility during the cutover. 2. Implement API callback validation and limited sessions; replace the legacy session dependency with the independent AUP port. Persist onboarding hand-offs. 3. Implement the legacy auth adaptor and audit every shared gate/caller. Reuse retained provider operations behind resumable work with one writer per target. 4. Deploy API login/logout, retained authentication and the provider worker together. Browsers with NextAuth sessions sign in again. NextAuth session authority and mode selection are removed; installations require the API configuration before upgrading. 5. Migrate `/api/v1/me` and selected Project directory callers, then each further endpoint after parity. Keep provider layout and retained business flows working. 6. In Phase 4, move AUP evidence/writes and the gate together to Party Governance. Before switching, qualify exact issuer/audience, signature, nonce, state, PKCE, expiry, auth_time, callback replay and session fixation. Exercise concurrent first login, pending attributes, GitLab/Waldur outages, ownership conflict, crash after external success, AUP acceptance before normal access, expired policy and outage, cross-origin/CSRF checks, both endpoint families, two browser windows, disablement and logout during token creation. Verify retained repository creation, Git HTTPS/LFS, invitations, allocation views and deployment setup. Keep browser delivery, API/Beto actor scopes, PostgreSQL atomicity, broker retry and credential isolation in the relevant test lanes. Roll back only through a qualified coordinated release rollback which fences issued sessions and preserves Party identity and durable provider work; fallback on an individual auth failure is never a cutover mechanism.