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 schemas —
passport, 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'sPOST /api/v1/sessionsproxy, plus a service account whoserealm-managementroles let it provision users through the Keycloak Admin API onPOST /api/v1/accounts. - Roles.
consumer(in the realm default-roles composite, so every account created via the Admin API receives it) andbrand_user(post-slice; assigned explicitly, never derived from request data — ADR-001). - User profile. The declarative user profile makes
firstName/lastNameoptional (onlyemailis 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/democonsumer 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.
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.jsondefines metadata common to every event, such aseventId,eventType,occurredAtandschemaVersion.jsonschema/events/v1/defines the payload of each event.gen/go/andgen/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.