Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ packages/e2e-tests/.tmp-output
# `makerkit deploy`/`destroy`'s generated stack file (deploy-cli.md) — regenerated every run.
.makerkit/

# `prisma-compose deploy`/`destroy`'s generated stack file — regenerated every
# run, full of machine-local absolute paths.
.prisma-compose/

# Local clone of prisma-next used as a scaffolding reference; not part of this repo.
/prisma-next/

Expand Down
88 changes: 88 additions & 0 deletions examples/store/DEMO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Standup walkthrough (~5 min)

Order of files to open, with the one point each makes.

## 1. The whole app — [module.ts](module.ts)

The entire application is ~6 lines of composition. Three components, three
edges, no infrastructure config anywhere. Point at the wiring:
`orders` gets `catalog.rpc`, `storefront` gets both.

## 2. A contract — [modules/catalog/src/contract.ts](modules/catalog/src/contract.ts)

The edge type. arktype schemas → a typed client on the consumer side and a
type-checked handler map on the producer side. Validated at the boundary at
runtime.

## 3. A module — [modules/catalog/src/module.ts](modules/catalog/src/module.ts)

catalog owns its own Postgres — a **Prisma Next-typed** one. Three pieces:

- [contract.prisma](modules/catalog/contract.prisma): the data schema. Emit
turns it into a typed contract; `migration plan` authors
[migrations/](modules/catalog/migrations), which the deploy applies before
the service ever starts — no `create table if not exists` in app code.
- [src/service.ts](modules/catalog/src/service.ts): the service declares
`deps: { db: pnPostgres(catalogData) }` and what it exposes; that's the
whole declaration.
- [src/server.ts](modules/catalog/src/server.ts): `load()` hands it the typed
client — `db.orm.public.Product.where({ id }).first()`. No SQL strings, no
row mappers, and the same contract types the resource end (the deploy
refuses to wire a service against a database with a different contract).

## 4. The interesting edge — [modules/orders/src/module.ts](modules/orders/src/module.ts)

orders owns a Postgres too, but declares `deps: { catalog }` as a **boundary
input** — the parent supplies any producer of `catalogContract`. In
[src/server.ts](modules/orders/src/server.ts), `placeOrder` calls
`catalog.getProduct()` — a typed async call, no URL, no fetch, no client
setup. It doesn't know or care where catalog runs.

## 5. A shared module — cron rotating the ★ special

[module.ts](module.ts) provisions `cron({ schedule, runner })` — the cron
module ships with the framework
(`@prisma/compose-prisma-cloud/cron`), not this app. Two points:

- [modules/promotions/src/service.ts](modules/promotions/src/service.ts):
the app supplies only the schedule (`rotateSpecial: '30s'`) and a runner
whose handler maps the job id to `catalog.rotateSpecial()`
([src/server.ts](modules/promotions/src/server.ts)). `serveSchedule` is
exhaustive over the schedule's ids at compile time.
- The cron module's boundary deps mirror the runner's own deps, so the root
wires `catalog: catalog.rpc` into it exactly like any other edge — a
reusable module composes the same way app modules do.

Live proof: watch the ★ move to the next product every 30s on refresh.

## 6. Plain app code — [modules/storefront/app/page.tsx](modules/storefront/app/page.tsx)

A normal Next.js server component. `service.load()` hands it two typed
clients, injected by the root's wiring. The Buy button is a server action
calling `orders.placeOrder`.

## 7. Show it live

Deployed: run the deployed storefront URL, buy a coffee, watch it appear in
recent orders (two RPC hops + two Postgres writes behind one click).

Fallback if the cloud misbehaves: `pnpm dev` here runs the same storefront
against in-memory fakes on loopback — same contracts, zero cloud.

## 8. Testing story (if time)

[page.test.tsx](modules/storefront/app/page.test.tsx): `mockService` swaps
`load()` for typed fakes — the handler maps are type-checked against the same
contracts the real modules serve. No Postgres, no server, no cloud.

## Likely questions

- **"What actually got deployed?"** Each compute service is a Prisma Compute
VM; each module's `postgres()` is a Prisma Postgres database. The deploy
derives the graph from the code above — same code could target another
extension pack.
- **"What's in an edge?"** At runtime: an env var (`CATALOG_URL`) the
framework injects, plus the contract's validation on both ends.
- **"Why modules and not just services?"** Boundaries. catalog's database can
never become someone else's dependency — it isn't reachable. Only exposed
ports are.
80 changes: 80 additions & 0 deletions examples/store/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# store — Compose Coffee

A readable example Prisma App: a Next.js storefront, a **catalog** module, an
**orders** module, and the shared **cron** module rotating the special of the
day.

```mermaid
flowchart TB
User((User))

subgraph Storefront["storefront"]
SF["Next.js · Compute Service"]
end
subgraph Catalog["Module: catalog"]
CA["catalog · Compute Service"]
CDB[("Postgres — private")]
CA --- CDB
end
subgraph Orders["Module: orders"]
OR["orders · Compute Service"]
ODB[("Postgres — private")]
OR --- ODB
end
subgraph Cron["Module: cron (shared)"]
SCH["scheduler · Compute Service"]
PR["promotions runner · Compute Service"]
SCH -->|"trigger(jobId)"| PR
end

User -->|HTTP| SF
SF -->|rpc · catalogContract| CA
SF -->|rpc · ordersContract| OR
OR -->|rpc · catalogContract| CA
PR -->|rpc · catalogContract| CA
```

Each module owns its own Postgres; the only edges between components are the
typed RPC contracts. The whole composition is [module.ts](module.ts).

## What each piece shows

- [modules/catalog](modules/catalog) — a self-contained Module: a contract
(`listProducts`/`getProduct`), a compute service, and a **Prisma
Next-typed Postgres** it owns. The data schema is
[contract.prisma](modules/catalog/contract.prisma); the deploy applies
[migrations/](modules/catalog/migrations) before the service starts, and
`load()` hands the server a typed client — queries like
`db.orm.public.Product.where({ id }).first()`, no SQL, no row mapping.
Consumers wire only the exposed `rpc` port.
- [modules/orders](modules/orders) — a Module with a **boundary input**: it
owns its (also Prisma Next-typed) Postgres but declares `deps: { catalog }`,
so whoever provisions it supplies a producer of `catalogContract`.
`placeOrder` calls catalog to price the order at placement time.
- [modules/storefront](modules/storefront) — a real Next.js app. The page
calls both typed clients; the Buy button is a server action that places an
order.
- [modules/promotions](modules/promotions) + the shared
`@prisma/compose-prisma-cloud/cron` module — **composition of a shared
module**. promotions defines the schedule
(`rotateSpecial: '30s'`) and the runner that maps the job id to
`catalog.rotateSpecial()`; the root provisions `cron({ schedule, runner })`
and wires `catalog.rpc` into its boundary like any other edge. The ★
special on the menu moves every 30 seconds.

## Run it locally (no cloud)

```sh
pnpm dev # from examples/store
```

Serves in-memory fakes of catalog and orders on loopback ports and runs
`next dev` against them — the same fakes the unit test injects via
`mockService` ([page.test.tsx](modules/storefront/app/page.test.tsx)).

## Deploy

```sh
pnpm deploy # needs .env at the repo root (see examples/storefront-auth)
pnpm destroy
```
33 changes: 33 additions & 0 deletions examples/store/module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { module } from '@prisma/compose';
import { cron } from '@prisma/compose-prisma-cloud/cron';
import catalogModule from '@store/catalog';
import ordersModule from '@store/orders';
import promotionsService, { schedule } from '@store/promotions';
import storefrontService from '@store/storefront';

/**
* The store app: four components, four edges.
*
* storefront ──rpc──▶ catalog (browse products)
* storefront ──rpc──▶ orders (place + list orders)
* orders ──rpc──▶ catalog (price an order at placement time)
* cron ──rpc──▶ catalog (rotate the special of the day, every 30s)
*
* catalog and orders each own their own Postgres internally — the root never
* sees it. All it wires are the exposed, typed rpc ports.
*
* `cron` is a SHARED module (@prisma/compose-prisma-cloud/cron), not app
* code: it takes our schedule and our promotions runner, and its boundary
* deps mirror the runner's own — so the root wires `catalog` into it exactly
* as it would for any other consumer of that contract.
*/
export default module('store', ({ provision }) => {
const catalog = provision(catalogModule);
const orders = provision(ordersModule, { deps: { catalog: catalog.rpc } });

provision(storefrontService, { deps: { catalog: catalog.rpc, orders: orders.rpc } });

provision(cron({ schedule, runner: promotionsService }), {
deps: { catalog: catalog.rpc },
});
});
Loading
Loading