Amends 0117: a generated client and a
port are contracts-entry surfaces, and a consumer imports neither from another service’s source.
Amends 0142: the choice between an in-process
call and an RPC call is made by deployment topology inside the driver, and the consumer’s code
does not change.
Amends 0158: the committed
app-manifest.json is the single contract source. Clients and event maps are generated from it,
and openapi.json is a projection of it.Amends 0163: a consumer’s read model
of another service’s data is a
@Projection fed by that service’s integration events.Amends 0078 (decision 3): integration event shapes are
read from the producer’s committed manifest, not from its
./contracts entry.Amends 0141: an event becomes an integration
event when its
defineEvents entry is marked public: true. The name format does not change.Context
0010 says services are independent and never import each other’s source. 0142 moved the transport behind drivers. What was missing is the reach a consumer writes. Nest-style string patterns compile with typos, and two contract sources (an OpenAPI file for calls, a./contracts entry for
events) drift apart. The owner settled the surface on 2026-10-21: one generated client per service, three modes on one action key, and
one contract source.
Decision
-
No direct access. A service never imports another service’s code, tables or
Topic, and never queries another service’s database. -
One contract source. The producer’s committed
app-manifest.json(0158) holds its actions, queries and events with JSON Schema, refusal codes and hook-extended inputs.openapi.jsonis a projection of it for external consumers../contractsis the TypeScript authoring source that gen reads to write the manifest. A consumer never reads a producer’s./contracts. -
One generated client per service, injected.
stackra genbuilds the client from the manifest. The client is a class, so it is its own token (0263) and a consumer receives it through its constructor:The type is imported from the generated#stackra/clientsmodule (0261). -
Three modes, one action key, one pipeline. Each mode targets the producer’s action key and runs through the producer’s pipeline,
so its permissions, tenancy and audit apply.
- Call:
await commerce.order.price(input). The consumer needs the answer now. The call is synchronous. - Command:
await notifications.message.send.queue(input). The consumer does not need the answer. The command is queued on the producer’s queue driver, runs at least once, is deduplicated by a derived idempotency key, and returns a receipt. - Event:
@OnEvent(Commerce.order.placed). Something happened. Consumers subscribe to the typed descriptor the generated event map provides, and the descriptor carries the versioned wire name of 0141.
- Call:
-
Integration events are declared by the producer. A
defineEventsentry markedpublic: trueis an integration event. It goes through the outbox (0078), and its shape is exported in the manifest. - Topology picks the transport. When producer and consumer run in one process, the generated client calls the action pipeline directly. When they run as separate deployments, it uses RPC through the driver of 0142. Commands use the producer’s queue driver. The consumer’s code is the same in both cases.
-
Read models for queries. A consumer that needs another service’s data in its own queries keeps an event-fed read model
(
@Projection, 0163). It never queries the producer’s tables. - Shared logic is not a client. Logic that two services need goes to a figentra-os package (0259, home B), not into a client.
-
Stackra packages use ports. A Stackra package that needs service data defines a port in its
./contractsentry (0117). The service implements the port, and gen wires it (0263). The package never imports the service. -
Tests override the client. A consumer’s test replaces the client with a fake through
createTestingModule(...).override(...)(0263).
Consequences
- A consumer’s code reads like local dependency injection, and a reviewer sees the dependency in the constructor.
- A wrong key, event or payload is a type error at the call site, because the client and the event map come from one manifest.
- Commands remove the hand-written retry loop that fire-and-forget work otherwise needs. A command that fails is retried by the producer’s queue.
- Read models add storage and an explicit lag; the consumer owns that staleness.
- Each service needs a committed manifest before consumers can generate against it.
Enforcement
planned — a boundaries rule bans imports of another service’s package or path, which extends the rule of 0010. Planned:stackra gen fails when a consumer names a service with no
committed app-manifest.json; stackra gen --check fails when the committed openapi.json is not the projection of the manifest; and the
conformance test of 0144 checks that the event map matches the manifest.