Skip to content
2 changes: 2 additions & 0 deletions .drive/deferred.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,8 @@ ADR-0053 (the commands). What we deliberately didn't do:

# Values — deferred (2026-10-29)

- **`ServiceNode.inputs` and `InputRef` keep the retired port word** (the decision that a node depends on an interface or exposes one). Both are inside core and the extensions; rename them to `deps` and `DepRef` when a change already touches every reader, since `inputs` on a node is read by the Prisma Cloud extension and the local target.

- **`resolveParam` in `control/deploy.ts` uses "resolve" for a different act** (picking a param's binding or default and validating it) than Load's resolving of values. Rename it, for example to `bindParam`, when that file is next touched.

- **Rename `Values<P>` to `ParamValues<P>` at the next breaking release.** `Values<P>` is the hydrated data of a node's params, exported from `@prisma/composer`. Since `Value<T>` became the type `resolve()` returns, the two names read as related when they are not. Renaming a public type breaks callers, so it waits for a release that already breaks them. Origin: public-URLs slice C1.
Expand Down
4 changes: 2 additions & 2 deletions docs/design/01-principles/guiding-principles.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ genuinely missing.

## Thin core, fat targets

Keep the core small and stable — the Module/Input/Output model, the topology, the
Keep the core small and stable — the model of Modules, dependencies and exposed interfaces, the topology, the
lowering machinery. Target specifics (resource types, providers) live in extension
packs and are swappable without touching the core.

## Agnostic of deployment targets

The core knows nothing about any specific deployment target. The framework provides
the affordances — Module, Inputs/Outputs, Resource, the lowering SPI — and a target's
the affordances — Module, dependencies and exposed interfaces, Resource, the lowering SPI — and a target's
specifics (Prisma Cloud's resource types, or another cloud's) come *only* from an
extension pack. The framework never branches on target identity.

Expand Down
22 changes: 11 additions & 11 deletions docs/design/02-example-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ Each Module is a bounded context wrapping a Service (its code) plus any private
Resources. Storefront and Auth are single-Service Modules; Sales and Delivery each
own a private Resource alongside their Service.

| Module | Services | Private Resources | Inputs | Outputs |
| Module | Services | Private Resources | Depends on | Exposes |
| --- | --- | --- | --- | --- |
| **Storefront** | Next.js | — | `sales` (r/r), `auth` (r/r) | `site` (r/r · **ingress**) |
| **Sales & Billing** | Sales API | Invoices (object storage) | `data` (TCP · *sales* contract); `email` (**egress** → Configuration) | `sales` API (r/r); `order-placed` (stream) |
Expand All @@ -72,9 +72,9 @@ own a private Resource alongside their Service.

Plus one shared **Resource**, owned by the outermost Module (not by any of the four):

| Resource | Output |
| Resource | Exposes |
| --- | --- |
| **Postgres** (shared) | `data` **Data Output** offering the contract hashes `{sales, delivery, auth}` — the **aggregate contract** |
| **Postgres** (shared) | `data`, a data interface offering the contract hashes `{sales, delivery, auth}` — the **aggregate contract** |

## What the example demonstrates

Expand All @@ -86,7 +86,7 @@ Plus one shared **Resource**, owned by the outermost Module (not by any of the f
everything is a stream.
- **The shared Postgres has one owner — the outermost Module.** It isn't owned by
Sales, Delivery, or Auth; it's owned by the **outermost Module** (the module
topology), which wires it to each consumer's Data Input and owns its migration. Sales,
topology), which wires it to each consumer's data dependency and owns its migration. Sales,
Delivery, and Auth each declare only the contract slice they need; the owner's
schema satisfies the **aggregate** (the union), and consumer slices don't overlap.
Shared instance, single migration owner, every data dependency still a visible edge.
Expand All @@ -97,14 +97,14 @@ Plus one shared **Resource**, owned by the outermost Module (not by any of the f
doesn't provision — an **egress** backed by an API key. That's Configuration on
the Sales Service, so it's neither a node nor a topology edge; hence the dashed
arrow, drawn outside every Module.
- **Ingress vs internal.** Only Storefront's `site` Output is public ingress; the
sales/auth/delivery APIs are internal request/response Outputs consumed by other
Modules.
- **Ingress vs internal.** Only Storefront's exposed `site` interface is public ingress; the
sales/auth/delivery APIs are internal request/response interfaces that other Modules
depend on.
- **Encapsulation by convention.** No Module reads another's tables; cross-Module needs go
through an Output (an API, the *order-placed* stream), never the database. The
through an exposed interface (an API, the *order-placed* stream), never the database. The
recommended convention, not (yet) an enforced rule.
- **A Module behaves like a Service.** Each is stateless and reprovisionable
with typed I/O, so if Delivery grew enough to split internally, a nested
with typed dependencies and exposed interfaces, so if Delivery grew enough to split internally, a nested
Module would wire exactly like the Service it replaced.

## How it lowers (sketch)
Expand All @@ -115,15 +115,15 @@ and migrated by the outermost Module, its schema satisfying the aggregate contra
invoices bucket and the Redis → their
own Alchemy Resources (provider create/update/delete), provisioned per environment;
the `order-placed` stream → a Stream; each request/response edge → an endpoint +
injected typed client; each Data Input → a contract-scoped connection injected at
injected typed client; each data dependency → a contract-scoped connection injected at
runtime (no `DATABASE_URL` in user code); the email key → `effect/Config` bound to
the Sales Service's environment. The framework emits the whole graph as the
topology artifact — Configuration and egress ride along as environment binding,
not nodes.

## Open questions / deferred

- Whether Delivery needs a request/response Output at all, or is purely
- Whether Delivery needs to expose a request/response interface at all, or is purely
stream-driven — depends on the product.
- **Auth: Module or external?** As modelled, Auth is a **Module** that owns its
tables. If instead you used a hosted BetterAuth you don't provision, it wouldn't be an
Expand Down
63 changes: 32 additions & 31 deletions docs/design/03-domain-model/authoring-surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,20 @@ The service and resource vocabulary (`compute`, `postgres`, …) belongs to the
the wiring rules below, and the service-to-service connection kinds (service RPC, raw
HTTP), belong to the core.

## One port mechanic, uniform at every level
## One wiring mechanic, uniform at every level

Every node — Service, Module, or Resource — has typed Inputs and Outputs, and a
connection wires one node's Output to another's Input of the same named interface.
Every node — Service, Module, or Resource — depends on typed interfaces and exposes
typed interfaces, and a connection wires one node's dependency to another's exposed
interface of the same type.
This rule already governs the model (see the [domain map](domain-map.md)); the point
here is that it is also the *authoring surface*. A Service declares its ports as it
consumes and produces them:
here is that it is also the *authoring surface*. A Service declares its dependencies and
its exposed interfaces as it consumes and produces them:

```ts
// a service with an Input (db) and an Output (its API)
// a service with a dependency (db) and an exposed interface (its API)
export default compute(
{ db: rawPostgres() },
({ db }) => ({ api: Auth.serve(buildApp(db)) }) // returned ⇒ Output
({ db }) => ({ api: Auth.serve(buildApp(db)) }) // returned ⇒ exposed
)
```

Expand All @@ -46,10 +47,10 @@ A connection type is a value — `Auth = http(AuthInterface)`, shipped with
`@prisma/composer` — not a raw TypeScript interface, and it is direction-neutral.
(The typed `http(contract)` tier is not built yet; today the typed kind is service
RPC's `rpc(contract)`, and `rawHttp()` is the untyped HTTP kind.) Where you place
it decides polarity: name it as a dependency and it is an Input, hydrated to a typed
client; return it and it is an Output you must implement. This is not merely
stylistic — it mirrors data flow. Inputs must be declared up front because they are
hydrated before your code runs; Outputs are naturally the return, because they are
it decides polarity: name it as a dependency and it is one, hydrated to a typed
client; return it and it is an exposed interface you must implement. This is not merely
stylistic — it mirrors data flow. Dependencies must be declared up front because they are
hydrated before your code runs; exposed interfaces are naturally the return, because they are
what your code produces.

Because the connection type is a neutral value that either side may author, it
Expand All @@ -61,32 +62,32 @@ provider happened to publish.
export const Auth = http(AuthInterface) // neutral connection type

export default compute(
{ db: rawPostgres(), auth: Auth }, // Auth as dependency ⇒ Input ⇒ typed client
(deps) => ({ web: next(deps) }) // 'web' Output served by a Next.js adapter
{ db: rawPostgres(), auth: Auth }, // Auth as a dependency ⇒ typed client
(deps) => ({ web: next(deps) }) // 'web' exposed, served by a Next.js adapter
)
```

## Services and Modules are both wiring; they differ only in opacity

Neither a Service nor a Module runs itself; each is wired and then run by the
framework. The convention is identical for both: **Inputs arrive as arguments,
Outputs are the return.** A Service's body is opaque — it wires its ports to a
framework. The convention is identical for both: **dependencies arrive as arguments,
exposed interfaces are the return.** A Service's body is opaque — it wires its boundary to a
server adapter (Next.js, Hono, a bare handler) and the framework sees only the
boundary; it is a **black box**. A Module's body is transparent — the framework
sees the topology it owns — and it additionally gets `provision()`. Forwarding
needs no new primitive: a Module passes its Inputs *down* into the nodes it owns
and returns their Outputs *up* as its own. Provisioning and ownership are a
needs no new primitive: a Module passes its dependencies *down* into the nodes it owns
and returns their exposed interfaces *up* as its own. Provisioning and ownership are a
Module concern; a Service only ever *requires*.

```ts
// a module — same port mechanic for its boundary; body only wires
import store, { Auth } from "./storefront-service"
export default module("storefront",
{ auth: Auth, web: http(StoreInterface) }, // module-level ports
{ auth: Auth, web: http(StoreInterface) }, // the module's own boundary
({ auth, provision }) => {
const db = provision(rawPostgres()) // the Module owns resources
const svc = provision(store, { db, auth }) // forward module Input → service Input
return { web: svc.web } // forward service Output → module Output
const svc = provision(store, { db, auth }) // forward the module's dependency → the service's
return { web: svc.web } // forward the service's exposed interface → the module's
}
)
```
Expand All @@ -105,13 +106,13 @@ data object (see [core and targets](core-and-targets.md)). That object *is* the
manifest: there is no separate framework manifest file (the platform's
`compute.manifest.json` stays — it only names the boot entrypoint). The same value is
read twice: the control plane at deploy time routes each node's metadata to its
provider and wires the config; the runtime host hydrates the Inputs and injects them.
provider and wires the config; the runtime host hydrates the dependencies and injects them.
Because each Service is isolated in its own compute unit, the environment-variable
names those two readers agree on are derived by convention, not communicated through
a file. Environment variables remain the physical channel that carries config into
the VM, but they **terminate at hydration** — user code never reads them.

## The runtime is a dumb loop; a framework is an Output adapter
## The runtime is a dumb loop; a framework implements an exposed interface

At boot, the node's **`run`** loop executes: core enumerates the config shape
(semantic names + types — target-independent), the pack **deserializes** the
Expand All @@ -132,7 +133,7 @@ thereby enumerable without booting (`configOf`), injectable with fakes in tests
(`invoke`), and reportable (secrets redacted) in production. When the "handler" is a framework that owns
its own server — Next.js — Prisma Composer does not wrap the handler
signature; it wires the
framework in as the implementation of an HTTP Output, and framework code reaches its
framework in as the implementation of an exposed HTTP interface, and framework code reaches its
dependencies through a DI accessor (`use(…)`), never through the environment. This is
the concrete form of the [no-globals
principle](../01-principles/architectural-principles.md): Prisma Composer
Expand All @@ -143,16 +144,16 @@ propagates data to user code only through dependency injection.
Loading the graph — walking the constructors' data and validating it — constructs a
typed dataflow graph (a graph of streams, where request/response is the bounded case:
one message in, one out). At the end the graph is closed, so the framework validates
its integrity: every Input satisfied, interfaces compatible, nothing dangling. That is
its integrity: every dependency satisfied, interfaces compatible, nothing dangling. That is
**Load** — the graph is in memory to inspect, validate, or manipulate, and nothing
has executed. **Hydrate** is the second phase: attach adapters and push data through
the Inputs and out of the Outputs.
the dependencies and out of the exposed interfaces.

One graph serves both a test harness and a real deployment. Because Loading executes
nothing, the topology can be interrogated and emitted for tooling independently of a
deploy. Because integrity is validated at Load before any Hydrate, an error surfaces
before execution and a test can trust that nothing ran until the graph was whole. And
because a fake Output can be substituted at any Input — the same dependency inversion
because a fake can be substituted at any dependency — the same dependency inversion
— a Service or Module is testable with no real infrastructure, which is the
local-emulation story.

Expand All @@ -162,15 +163,15 @@ local-emulation story.
constructor's returned data already carries it; a second artifact would only restate
the code and risk drifting from it, and per-service isolation makes convention-based
env names sufficient.
- **Explicit `In()` / `Out()` wrappers** on every port. Position already determines
direction unambiguously (dependencies are hydrated before the body runs; outputs are
the return), and a neutral connection type is what enables dependency inversion.
- **Explicit `In()` / `Out()` wrappers** on every interface. Position already determines
direction unambiguously (dependencies are hydrated before the body runs; exposed
interfaces are the return), and a neutral connection type is what enables dependency inversion.
- **A generic core `service()` the user calls.** The concrete constructor is
target-provided (`compute`), because the node it produces must carry the target's
routing. Core defines the *kind* Service; the target stamps out nodes of it.
- **A framework as its own entrypoint kind**, distinct from a service the framework
defines. It fractures the model into "native" and "framework-hosted" cases; a
framework server is better understood as one implementation of an HTTP Output.
framework server is better understood as one implementation of an exposed HTTP interface.

## Open questions

Expand All @@ -189,7 +190,7 @@ local-emulation story.
## Related

- [`core-and-targets.md`](core-and-targets.md) — the thin-core/target-pack split this sits on.
- [`domain-map.md`](domain-map.md) — the Input/Output model this surface expresses.
- [`domain-map.md`](domain-map.md) — the dependency and exposed-interface model this surface expresses.
- [`glossary.md`](glossary.md) — the authoring vocabulary and the compile target.
- [`layering.md`](layering.md) — how the authoring plane lowers to Alchemy and Prisma Cloud.
- [`../01-principles/architectural-principles.md`](../01-principles/architectural-principles.md)
Expand Down
Loading
Loading