Platform Technologies

The shared infrastructure every service runs against. Locally it all starts with make up (see Local development).

PostgreSQL

What: Relational database, one instance (v17), holding six schemaspassport, personalization, fridge, identity, measurement, brand_analytics.

Why: One battle-tested infrastructure piece covers every storage shape the domain needs: relational catalog data, the fridge's append-only event table with its projections, the measurement pipeline's short-retention windows and the analytics read models. Data ownership is enforced by the database, not convention: each service connects with its own role, GRANT-restricted to its schema, so a cross-schema query fails with permission denied (ADR-009). The roles and schemas are provisioned by deployment/postgres/init/01-schemas.sql.

Apache Kafka

What: Distributed event log, run as a single-node KRaft (ZooKeeper-less) broker.

Why: Independently deployed services need a real broker for the domain event catalog — ProductScanned, VerdictComputed, the fridge stream, the consent/erasure events — and Kafka is what Assignment #03 names, so running it from day one makes Part III a deepening (topic design, partitioning, consumer groups, delivery semantics) instead of a migration (ADR-010). Kafka carries asynchronous facts only; immediate request/response interactions stay synchronous REST per API Design §3.

Keycloak

What: Self-hosted open-source identity provider speaking OpenID Connect.

Why: PackyTrace handles health-adjacent data, which must not ride on hand-rolled password auth. Keycloak brings password storage, reset, refresh-token rotation, token revocation and session handling as a hardened product (ADR-011). The gateway validates Keycloak-issued JWTs; identity-service keeps only the domain side — Visitor IDs, visitor→account linking, the consent ledger, Brand/BrandUser. Identity is a generic subdomain in the domain model: buy, don't build.

Realm: the packytrace realm is versioned as deployment/keycloak/import/packytrace-realm.json and loaded on boot via start-dev --import-realm (idempotent — an existing realm is skipped). It defines:

  • Clients. packytrace-web — public SPA client, Authorization Code + PKCE (the target browser flow, post-slice hardening). identity-service — confidential server client with Direct Access Grants (ROPC) backing the slice's POST /api/v1/sessions proxy, plus a service account whose realm-management roles let it provision users through the Keycloak Admin API on POST /api/v1/accounts.
  • Roles. consumer (in the realm default-roles composite, so every account created via the Admin API receives it) and brand_user (post-slice; assigned explicitly, never derived from request data — ADR-001).
  • User profile. The declarative user profile makes firstName/lastName optional (only email is required). PackyTrace accounts are email + an optional display name; the Keycloak default of requiring both names would otherwise trigger a profile-completion required action and block ROPC sign-in for Admin-API-created users.
  • Dev users. A demo@packytrace.dev / demo consumer for local ROPC testing.

Secrets in the import file (the identity-service client secret, the demo password) are dev-only and must never be reused in a deployed environment. The admin console is exposed on :8090.

Event contracts: JSON Schema + quicktype

What: Versioned JSON Schemas in contracts/jsonschema/ — the ADR-006 envelope plus one payload schema per domain event — with quicktype generating Go structs and TypeScript types from them.

Why: A polyglot fleet needs one contract source that is native to neither language. JSON Schema keeps the wire format human-readable in Kafka tooling, validates in CI without running a schema registry, and the generated types prevent drift on both sides. The rules that keep this from becoming lockstep coupling (envelope + payloads only, additive evolution, tolerant readers) are in contracts/README.md.

Producers. Go services import the generated structs from contracts/gen/go directly (the module is on the Go workspace). The first TypeScript producer (identity-service, publishing VisitorLinkedToAccount via the Confluent JS client to identity.facts.v1) builds the envelope in its Kafka adapter and asserts the emitted bytes against contracts/jsonschema with Ajv in a unit test — so the wire format is held to the contract source of truth without coupling the per-service TypeScript build to the generated package. A shared @packytrace/contracts npm package for TS services is a worthwhile later step once a second TS producer appears.

What is a contract?

A contract is an agreed description of the data two services exchange. It answers:

  • What is this message called?
  • Which fields must it contain?
  • What type and meaning does each field have?
  • Which version of the message is being sent?

For example, after passport-service resolves a scan, it publishes a ProductScanned event. Its contract requires a scanId, visitorId, gtin and brandId; consumerRef is optional because the visitor may be anonymous. A consumer can process that event without importing code from passport-service or knowing how the scan was resolved.

flowchart LR P[passport-service] -->|ProductScanned JSON| K[Kafka] K --> M[measurement-pipeline] C[JSON Schema contract] -. validates .-> P C -. validates .-> M

The contracts/ directory is therefore a small shared vocabulary, not a shared domain model. It contains only messages that cross service boundaries:

  • jsonschema/envelope.schema.json defines metadata common to every event, such as eventId, eventType, occurredAt and schemaVersion.
  • jsonschema/events/v1/ defines the payload of each event.
  • gen/go/ and gen/ts/ contain generated language types so producers and consumers do not manually recreate the schemas.

Changing a contract can affect every service that consumes it. Compatible additions are optional fields; incompatible changes require a new version. Internal entities, database rows and service-specific implementation details do not belong in contracts.

Event contracts describe asynchronous Kafka messages. HTTP request and response contracts serve the same purpose for REST APIs, but they are described separately with OpenAPI.

OpenAPI

What: Each service describes its HTTP surface in services/<name>/openapi.yaml (OpenAPI 3.1), derived from the API design.

Why: The API design page defines the system-wide contract; the per-service specs make it machine-readable where it is owned — each service documents exactly the public paths routed to it, its internal endpoints, and its schemas. The gateway's spec is intentionally just the routing table plus /health, because the gateway adds no endpoints of its own. Specs are linted with Redocly CLI.

Docker & Compose

What: A multi-stage Dockerfile per service (Go: build → ~15 MB Alpine; TS: build → node:24-alpine) and deployment/docker-compose.yml orchestrating all seven services plus Postgres, Kafka and Keycloak with healthchecks.

Why: Part II requires container-per-service deployment, and a solo developer needs the whole system reproducible with one command (make up). Every image builds from the repository root so services can import the shared generated contracts.

Planned: Prometheus, Grafana, Kubernetes

Part II adds Prometheus metrics (scan-latency histogram at the gateway, breaker-state counters) and health-check-driven orchestration; Part III adds Kubernetes manifests with liveness/readiness probes wired to the existing /health endpoints and SLO-driven autoscaling. They are listed here for completeness; their sections will be written when they land.