Architecture
Three layers, almost all of which patties already has. The boundary speaks standard Request/Response; the core is a Bun.serve route table plus the compose() middleware; the application is a set of module folders. The only new machinery is a build-time macro that discovers modules, merges their routes, and verifies the public-API boundary.
This page stays high-level
The module convention, the public-API boundary check, the data-ownership model, and the dependency-graph checker are documented in detail under Features. Here we cover only the layer model, build-time discovery, and the deploy artifact.
Layered architecture
No DI container layer, no adapter layer. The core is the things patties already ships; modules sit on top as plain folders.
flowchart TB
subgraph EDGE["Boundary — web standards"]
direction LR
REQ["Request"]
RES["Response"]
end
subgraph CORE["patties core (existing)"]
direction TB
SRV["Bun.serve · merged route table"]
MW["compose() middleware
+ thin PattiesContext"]
SRV --> MW
end
subgraph DOM["Application modules (folders)"]
direction LR
A["billing/
index.ts · routes.ts"]
B["orders/
index.ts · routes.ts"]
C["catalog/
index.ts · routes.ts"]
end
REQ --> SRV
MW --> RES
MW --> DOM
A -. public API .-> B
classDef core fill:#1d4ed8,color:#fff,stroke:#1e3a8a,stroke-width:2px;
class CORE core;
index.ts and a routes.ts. Cross-module edges (dashed) go through the public index.ts only — that is the one invariant the build enforces.Build-time discovery & boundary check
One macro, a sibling of patties' existing route/island/agent macros. It runs during Bun.build, finds modules, merges their route maps into a single literal, and checks that no module deep-imports another's internals. The runtime never repeats this work.
flowchart LR
G["Bun.Glob
app/*/index.ts
app/*/routes.ts"]
SCAN["read each module's
routes export + imports"]
CHK{"only public-API
imports?"}
ERR["Build error
deep import into
another module's internals
+ file:line"]
MERGE["merge route maps →
one { path: { METHOD } } literal"]
BUNDLE["Bun.build →
frozen server bundle"]
G --> SCAN --> CHK
CHK -->|no| ERR
CHK -->|yes| MERGE --> BUNDLE
classDef bad fill:#fee2e2,stroke:#ef4444,color:#7f1d1d;
class ERR bad;
*.macro.ts files: the macro's return value is inlined as a literal, so the production bundle contains the merged route table and does no runtime scan (honoring the repo's build-time-discovery rule).Build & deploy artifact
One artifact: a frozen merged-route bundle. The production server is Bun.serve({ routes }) — no filesystem scan, no container construction.
- Discovery
- Bun.Glob in a build macro — finds
app/*/routes.tsand merges. Runtime scans nothing. - Bundler
- Bun.build only. No Webpack/Rollup/esbuild/Vite/tsup.
- Dev loop
- bun --hot re-runs discovery on change — same as patties' route HMR today. Frozen in prod.
- HTTP / WS
- Bun.serve — native route table +
upgradefor WebSockets. - Request flow
- The existing
compose()middleware + thinPattiesContext. Nothing new. - Targets
bunandedge(WinterCGexport default { fetch }) — patties' existing adapters.
Key architectural rules
① Build-time discovery
Module + route discovery happens in a macro. The production bundle inlines the merged table and re-scans nothing.
② Public-API boundary
The one new invariant: cross-module imports go through index.ts. Deep imports are build errors.
③ Standards at the boundary
Request in, Response out. The ctx stays thin — no request abstraction.
④ Bun-native primitives
Bun.serve, Bun.Glob, Bun.build, bun --hot. Node-era replacements are not introduced.