Phase J2: Identity, Sessions, and Consent

This phase created PackyTrace's first complete signed-in journey. A visitor can create an account, keep the history from their anonymous scans, sign in and out, grant or revoke health-profile consent, and use the web app with a real authenticated session.

The Identity Flow

User registers or signs in through the web app
        |
        v
Identity service asks Keycloak to create or authenticate the user
        |
        v
Keycloak issues signed access and refresh tokens
        |
        v
API gateway verifies the JWT
        |
        v
Gateway sends the verified account ID to backend services
        |
        v
Identity service manages PackyTrace account links and consent history

Task 2.1: Keycloak Realm

Like you're 5

We built an ID-card office. It knows which apps are allowed to request cards, which roles can be printed on them, and which trusted worker may create new people.

Normal Explanation

PackyTrace now has a version-controlled Keycloak realm configuration that is imported when Keycloak starts. The realm defines:

  • packytrace-web, the public browser application;
  • identity-service, a confidential backend client;
  • the consumer and brand_user roles;
  • an identity-service service account for future user creation;
  • a demo consumer for local testing.

Keycloak owns credentials, login sessions, and token issuance. It does not own PackyTrace consent history, Brands, or visitor-to-account links.

Technologies used: Keycloak is the identity provider; a realm is its isolated identity configuration; OpenID Connect and OAuth 2.0 define the authentication flows; Docker Compose imports the realm JSON during local startup.

Task 2.2: Gateway JWT Validation

Like you're 5

We placed a security guard at the front door. The guard checks every ID card using the ID office's official stamp. A real card opens private doors, a fake card is rejected, and visitors cannot write someone else's name on a sticky note and pretend to be them.

Normal Explanation

The Go API gateway now reads bearer JWTs from the Authorization header and verifies:

  • the token's digital signature;
  • its expiry time;
  • the exact Keycloak realm that issued it.

The gateway obtains Keycloak's public signing keys from its JWKS endpoint and refreshes them so Keycloak can rotate keys. A valid token's sub claim becomes the trusted X-Account-Id header forwarded to backend services.

Any client-supplied X-Account-Id is removed before this happens. Therefore, backend services can trust that the gateway, not the caller, asserted the account identity. Routes under /api/v1/me/* return 401 Unauthorized without a valid account token, while anonymous scanning remains public.

Technologies used: a JWT is a signed identity token; a bearer token is sent in the HTTP Authorization header; JWKS publishes public signature-verification keys; lestrrat-go/jwx performs JWT and JWKS handling in Go; middleware checks requests before routing them.

Task 2.3: Identity-Service Database Foundation

Like you're 5

We built private filing cabinets for PackyTrace's people-related information:

  • one for accounts;
  • one connecting anonymous visitors to accounts;
  • one permanent history book for consent;
  • one for Brands;
  • one connecting Brand workers to their Brand.

The consent history book only allows new lines. Nobody may erase or rewrite an old line.

Normal Explanation

The TypeScript identity service now connects to PostgreSQL through Kysely and applies its own migrations at startup. Those migrations create:

Table Purpose
accounts Stores the PackyTrace account keyed by the Keycloak user ID.
visitor_identities Links a pseudonymous visitor to an account.
consent_ledger Keeps the append-only history of granted and revoked consent.
brands Stores the tenant units used for brand isolation.
brand_users Connects each BrandUser to exactly one Brand.

The consent ledger has a PostgreSQL trigger that rejects updates and deletions. Its history can therefore be extended but not rewritten.

The service connects as the restricted identity_svc database role. PostgreSQL limits that role to the identity schema, preventing the identity service from reading another service's private schema. Four demo Brands are seeded with the same IDs used by the product catalog.

Technologies used: PostgreSQL stores the tables and enforces isolation; Kysely is a type-safe TypeScript database toolkit; migrations version the schema; a database trigger enforces append-only consent history; schemas and roles enforce service data ownership.

Task 2.4: Account Registration and Visitor Linking

Like you're 5

A visitor may have already scanned products before creating an account. When they register, the ID-card office creates their identity, PackyTrace creates their account, and their old anonymous visitor badge is attached to the new account. The other rooms receive a note saying the visitor and account now belong together.

Normal Explanation

POST /api/v1/accounts now creates a complete PackyTrace account:

  1. The identity service uses its trusted service account to ask the Keycloak Admin API to create the credential-holding user.
  2. It stores an accounts row using Keycloak's user ID as the account ID.
  3. If the request contains a Visitor ID, it links that existing pseudonymous identity to the account without replacing it.
  4. It publishes a VisitorLinkedToAccount event to Kafka.

This lets later services connect activity performed before registration to the new account. A duplicate email returns 409 Conflict, and missing required fields return 400 Bad Request. Kafka publishing is currently fire-and-forget, so temporary Kafka downtime does not make registration fail.

The implementation follows hexagonal architecture: the registration use case depends on ports, while Keycloak, PostgreSQL, Kafka, and HTTP are separate adapters.

Technologies used: the Keycloak Admin API creates authentication users; a Keycloak service account lets the identity service call that API; Fastify handles the HTTP endpoint; PostgreSQL stores the account and visitor link; Confluent's JavaScript Kafka client publishes the event; Ajv verifies that emitted JSON matches the event contract.

Task 2.5: Sign In and Sign Out

Like you're 5

At sign-in, the visitor gives their secret password to the ID-card office. If it is correct, the office gives them a short-lived entry card and a second card for renewing their visit. At sign-out, PackyTrace asks the office to cancel the visit.

Normal Explanation

The identity service now provides:

  • POST /api/v1/sessions to exchange an email and password for Keycloak-issued access and refresh tokens;
  • DELETE /api/v1/sessions/current to revoke the Keycloak session associated with a refresh token.

The access token is the signed JWT sent with protected API requests. The refresh token represents the longer-lived Keycloak session and is used here during sign-out.

Unknown users and incorrect passwords both return 401 Unauthorized with the same error, preventing callers from discovering which emails are registered. Sign-out is best-effort and idempotent: clients can safely discard their local tokens even if the server session was already gone.

For the current thin slice, sign-in uses the OAuth 2.0 Resource Owner Password Credentials flow, commonly called ROPC or Direct Access Grants. It is intentionally temporary and will later be replaced by browser-based Authorization Code with PKCE.

Technologies used: OAuth 2.0 defines the token flow; Keycloak authenticates the credentials and owns the session; JWT access tokens prove identity; refresh tokens represent renewable sessions; a TypeScript AuthGateway port isolates the application from the Keycloak adapter.

Like you're 5

Before PackyTrace may keep personal health preferences, it asks permission. Every “yes” and “no” is written as a new line in the permanent consent history book. Other rooms can ask the identity room whether the newest line currently says yes.

When a person changes their answer from yes to no, PackyTrace also places a note on the shared noticeboard so rooms holding health information know they must remove it.

Normal Explanation

Authenticated users can now manage health-profile consent:

  • PUT /api/v1/me/consents/health-profile appends a consent grant and publishes ConsentGranted;
  • DELETE /api/v1/me/consents/health-profile appends a revocation and publishes ConsentRevoked.

The domain calculates current consent by folding the append-only ledger: it reads the history in order and treats the newest applicable record as the current state. Revocation is idempotent, so repeatedly revoking already-revoked consent creates no extra record or event.

An internal service-only endpoint lets the personalization service check consent before creating a Health Profile. It distinguishes an unknown account from a known account that has never granted consent. Public consent endpoints trust only the account ID verified and injected by the API gateway.

Technologies used: an append-only ledger preserves consent history; a domain fold calculates current state from historical records; PostgreSQL stores the ledger; Kafka events announce grants and revocations; internal HTTP APIs allow trusted service-to-service checks.

Task 2.7: Real Web Authentication

Like you're 5

We connected the website's login form to the real ID-card office. The website remembers the visitor's cards after a page refresh, shows who is signed in, includes the entry card when asking for private information, and cancels the visit during sign-out.

Developers can still connect the website to a pretend ID-card office for practice.

Normal Explanation

The SvelteKit web app now has a real authentication path:

  • registration calls the account endpoint, then immediately signs in;
  • sign-in stores the Keycloak access token, refresh token, and basic account display information;
  • the shared API client automatically attaches the access token as an HTTP bearer token;
  • sign-out asks the identity service to revoke the refresh token, then clears the local session;
  • the profile and authentication pages show real account state and real API errors.

Like scanning, authentication uses ports and adapters. The UI depends on an AuthGateway interface. The default HTTP adapter uses the real gateway, while an in-memory adapter remains available for mock mode.

Tokens are currently stored in browser localStorage, so a page reload keeps the session. Refresh-token rotation and the hardened Authorization Code with PKCE browser flow are planned post-slice.

Technologies used: SvelteKit builds the interface; Svelte stores expose reactive signed-in state; a TypeScript AuthGateway port separates UI from infrastructure; fetch calls the HTTP endpoints; localStorage persists the session; bearer authentication attaches the access token to protected API requests.

Checkpoint J2 Status

Like you're 5

The account rooms are connected and individually tested. The final J2 job is to walk through the entire building once more and confirm everything works together.

Normal Explanation

Tasks 2.1 through 2.7 are complete. The remaining J2 checkpoint is the full make check && make smoke validation. Personalized health-profile storage and verdict calculation begin in Phase J3.