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
consumerandbrand_userroles; - 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:
- The identity service uses its trusted service account to ask the Keycloak Admin API to create the credential-holding user.
- It stores an
accountsrow using Keycloak's user ID as the account ID. - If the request contains a Visitor ID, it links that existing pseudonymous identity to the account without replacing it.
- It publishes a
VisitorLinkedToAccountevent 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/sessionsto exchange an email and password for Keycloak-issued access and refresh tokens;DELETE /api/v1/sessions/currentto 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.
Task 2.6: Health-Profile Consent¶
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-profileappends a consent grant and publishesConsentGranted;DELETE /api/v1/me/consents/health-profileappends a revocation and publishesConsentRevoked.
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.