@spfn/core 0.2.0-beta.67 → 0.2.0-beta.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +309 -116
  2. package/dist/authz/index.js +398 -3
  3. package/dist/authz/index.js.map +1 -1
  4. package/dist/codegen/index.d.ts +114 -8
  5. package/dist/codegen/index.js +162 -3
  6. package/dist/codegen/index.js.map +1 -1
  7. package/dist/config/index.js +1 -1
  8. package/dist/config/index.js.map +1 -1
  9. package/dist/contract/index.d.ts +329 -0
  10. package/dist/contract/index.js +558 -0
  11. package/dist/contract/index.js.map +1 -0
  12. package/dist/{define-middleware-DuXD8Hvu.d.ts → define-middleware-B9bFuXVU.d.ts} +1 -1
  13. package/dist/errors/index.js +398 -3
  14. package/dist/errors/index.js.map +1 -1
  15. package/dist/event/index.d.ts +3 -3
  16. package/dist/event/sse/client.d.ts +2 -2
  17. package/dist/event/sse/index.d.ts +4 -4
  18. package/dist/event/sse/index.js +9 -0
  19. package/dist/event/sse/index.js.map +1 -1
  20. package/dist/event/ws/client.d.ts +2 -2
  21. package/dist/event/ws/index.d.ts +3 -3
  22. package/dist/middleware/index.d.ts +108 -11
  23. package/dist/middleware/index.js +769 -632
  24. package/dist/middleware/index.js.map +1 -1
  25. package/dist/route/index.d.ts +8 -552
  26. package/dist/route/index.js +66 -3
  27. package/dist/route/index.js.map +1 -1
  28. package/dist/router-HDVa6gfc.d.ts +676 -0
  29. package/dist/server/index.d.ts +3 -3
  30. package/dist/server/index.js +9 -0
  31. package/dist/server/index.js.map +1 -1
  32. package/dist/{token-manager-jKD_EsSE.d.ts → token-manager-vZeqBbtA.d.ts} +7 -0
  33. package/dist/{types-DVjf37yO.d.ts → types-CF-37KAG.d.ts} +1 -1
  34. package/dist/{types-BFB72jbM.d.ts → types-D9uMxeQS.d.ts} +1 -1
  35. package/package.json +11 -9
package/README.md CHANGED
@@ -1,78 +1,82 @@
1
- # @spfn/core — Type-safe Next.js + Hono backend framework (route DSL → RPC proxy → typed client)
1
+ # @spfn/core
2
2
 
3
- `@spfn/core` is the backend runtime for SPFN: a tRPC-style **route DSL** (TypeBox-validated,
4
- end-to-end typed), a PostgreSQL Drizzle **data layer** (postgres.js by default, injectable
5
- providers such as PGlite), an HTTP **server** entry point, and a
6
- Next.js **RPC proxy + typed client** that wires a browser/RSC app to that backend with
7
- compile-time-only type inference.
3
+ > **The backend runtime for taking an AI-built app from prototype to production**
8
4
 
9
- There is **no root barrel** `@spfn/core` itself imports nothing. Everything is reached
10
- through subpath exports (`@spfn/core/route`, `@spfn/core/db`, `@spfn/core/nextjs`, …). Each
11
- module is canonically documented in its own `src/<mod>/README.md`; this file is the index +
12
- end-to-end flow. Follow the links for API detail.
5
+ `@spfn/core` is the package every SPFN app is built on. It decides the shape of a
6
+ feature an entity, a repository, a route, a router registration — so that neither you
7
+ nor your coding agent has to invent one per feature. Everything else in SPFN
8
+ (`@spfn/auth`, `@spfn/mcp`, `@spfn/storage`, …) plugs into it.
13
9
 
14
- ## Install
10
+ 📖 **[superfunction.xyz](https://superfunction.xyz)** — docs and the full-stack tutorial ·
11
+ [repository README](https://github.com/fxylabs/spfn) for the whole framework.
12
+
13
+ > **Status — Beta (`0.x`).** The public API is stabilizing but may still change between
14
+ > minor releases before `1.0`. Pin your version and install from the `@beta` tag.
15
+
16
+ ---
17
+
18
+ ## What is @spfn/core?
19
+
20
+ A TypeScript backend runtime with four parts, used together:
21
+
22
+ - **A route DSL** — `route.get(path).input({...}).handler(...)`, validated at runtime by
23
+ TypeBox and typed at compile time from the same schema.
24
+ - **A data layer** — PostgreSQL through Drizzle ORM, with `BaseRepository`, schema
25
+ helpers, and transactions that propagate automatically.
26
+ - **A server** — a Hono app you start as a long-lived process, or mount as serverless
27
+ functions on Vercel.
28
+ - **A Next.js bridge** — an RPC proxy and a typed client, so a route's input and output
29
+ types are the same object on both sides of the network.
30
+
31
+ What it is not: a frontend framework, an ORM, or a place to keep your business rules.
32
+ Next.js owns the frontend, Drizzle owns the SQL, and your services own the rules.
33
+
34
+ ---
35
+
36
+ ## How do I install it?
15
37
 
16
38
  ```bash
17
- pnpm add @spfn/core drizzle-orm@1.0.0-rc.4
18
- # peer (optional): next ^15 || ^16
19
- # optional deps: ioredis (cache), ws (websocket events); pg-boss ships as a direct dep for jobs
39
+ pnpm add @spfn/core@beta drizzle-orm@1.0.0-rc.4 postgres pg
40
+ # optional peer: next ^16.2.11 (only for the Next.js bridge)
20
41
  ```
21
42
 
22
- Node `>=18.18.0`. ESM-only.
43
+ Node `>=18.18.0`. ESM only.
23
44
 
24
- > **Apps that define entities must declare Drizzle and the Postgres driver directly:** add
25
- > `drizzle-orm@1.0.0-rc.4`, `postgres`, and `pg` to your app's `dependencies`.
26
- > `@spfn/core` consumes Drizzle as a peer, and Drizzle branches its type resolution on
27
- > optional driver peers. If the app doesn't pin the same ORM and drivers, pnpm can resolve
28
- > a second Drizzle instance, `BaseRepository` generics collapse to `unknown`, and RPC
29
- > responses lose their types.
30
- > `spfn create` adds these automatically; declare them by hand only when wiring SPFN into
31
- > an existing app.
32
- >
33
- > ```bash
34
- > pnpm add drizzle-orm@1.0.0-rc.4 postgres pg
35
- > ```
45
+ > **Declare Drizzle and the Postgres drivers in your own app, not just in SPFN.**
46
+ > `@spfn/core` takes `drizzle-orm` as a peer dependency, and Drizzle changes how it
47
+ > resolves types depending on which driver packages are present. If your app does not
48
+ > pin the same ORM and drivers, pnpm can install a second copy of Drizzle — and then
49
+ > `BaseRepository` generics collapse to `unknown` and your RPC responses lose their
50
+ > types. `spfn create` adds these for you; add them by hand only when wiring SPFN into
51
+ > an app that already exists.
36
52
 
37
- ## Modules
53
+ Optional dependencies, installed only if you use the feature: `ioredis` (cache) and
54
+ `ws` (WebSocket events). `pg-boss` ships as a direct dependency for background jobs.
38
55
 
39
- Each entry below is exactly one subpath from `package.json` `exports`. Import path → one-line
40
- purpose → canonical README.
56
+ ---
41
57
 
42
- | Import path | Purpose | Doc |
43
- |-------------|---------|-----|
44
- | `@spfn/core/route` | tRPC-style route DSL (`route.get(...).input(...).handler(...)`) + `defineRouter` / `registerRoutes` / `defineMiddleware`. The core. | [src/route/README.md](./src/route/README.md) |
45
- | `@spfn/core/route/types` | Shared route types (`HttpMethod`, route/router type primitives). | [src/route/README.md](./src/route/README.md) |
46
- | `@spfn/core/server` | HTTP server entry: `defineServerConfig()` → `startServer()`; middleware auto-wiring, infra init, graceful shutdown. | [src/server/README.md](./src/server/README.md) |
47
- | `@spfn/core/nextjs` | Client-safe: `createApi<AppRouter>()`, `ApiError`, client types. No `next/headers`. | [src/nextjs/README.md](./src/nextjs/README.md) |
48
- | `@spfn/core/nextjs/server` | Server-only: `createRpcProxy({ routeMap })`, `registerInterceptors`. Uses `next/headers`. | [src/nextjs/README.md](./src/nextjs/README.md) |
49
- | `@spfn/core/db` | Type-safe PostgreSQL (Drizzle): CRUD helpers, `BaseRepository`, schema helpers, transactions, PG error mapping. Single entry point. | [src/db/README.md](./src/db/README.md) |
50
- | `@spfn/core/db` → manager | Connection lifecycle, pool, primary/replica, health-check, reconnect (`initDatabase`, `getDatabase`). Re-exported from `@spfn/core/db`. | [src/db/manager/README.md](./src/db/manager/README.md) |
51
- | `@spfn/core/db` → schema | Drizzle column helpers (`id`, `uuid`, `timestamps`, `foreignKey`, `enumText`, `typedJsonb`, `softDelete`, …). Re-exported from `@spfn/core/db`. | [src/db/schema/README.md](./src/db/schema/README.md) |
52
- | `@spfn/core/db` → transaction | `Transactional` middleware + `runInTransaction`; AsyncLocalStorage propagation to every repo. Re-exported from `@spfn/core/db`. | [src/db/transaction/README.md](./src/db/transaction/README.md) |
53
- | `@spfn/core/middleware` | Built-in Hono middleware: `ErrorHandler`, `RequestLogger` (+ masking helper). | [src/middleware/README.md](./src/middleware/README.md) |
54
- | `@spfn/core/errors` | Serializable HTTP/DB error classes + `ErrorRegistry` for cross-boundary deserialization. | [src/errors/README.md](./src/errors/README.md) |
55
- | `@spfn/core/env` | Schema-based env validation/parsing (isomorphic). `@spfn/core/env/loader` is the **server-only** file loader (`node:fs`). | [src/env/README.md](./src/env/README.md) |
56
- | `@spfn/core/config` | `@spfn/core`'s own validated env config (`env`, `envSchema`, `registry`) built on `@spfn/core/env`. | [src/config/README.md](./src/config/README.md) |
57
- | `@spfn/core/logger` | Zero-dependency structured singleton `logger` + child loggers, level masking. | [src/logger/README.md](./src/logger/README.md) |
58
- | `@spfn/core/cache` | Singleton Valkey/Redis (ioredis) manager, graceful-degrading (`getCache`, `getCacheRead`). | [src/cache/README.md](./src/cache/README.md) |
59
- | `@spfn/core/job` | pg-boss background jobs: fluent `job()` builder, cron/run-once/event-driven, `defineJobRouter`. | [src/job/README.md](./src/job/README.md) |
60
- | `@spfn/core/event` | Decoupled pub/sub (`defineEvent`, `defineEventRouter`, `eventRouteMap`); SSE/WS variants below. | [src/event/README.md](./src/event/README.md) |
61
- | `@spfn/core/event/sse` | Server SSE handler + token manager (SERVER ONLY). | [src/event/README.md](./src/event/README.md) |
62
- | `@spfn/core/event/sse/client` | Browser SSE (`EventSource`) client. | [src/event/README.md](./src/event/README.md) |
63
- | `@spfn/core/event/ws` | Server WebSocket handler (SERVER ONLY; `ws` optional dep). | [src/event/README.md](./src/event/README.md) |
64
- | `@spfn/core/event/ws/client` | Browser WebSocket client. | [src/event/README.md](./src/event/README.md) |
65
- | `@spfn/core/codegen` | Pluggable codegen orchestrator + the built-in `@spfn/core:route-map` generator that produces the proxy's `routeMap`. | [src/codegen/README.md](./src/codegen/README.md) |
66
-
67
- `db/manager`, `db/schema`, `db/transaction` have **no package subpath** of their own — they are
68
- internal modules re-exported by `@spfn/core/db`. Import them from `@spfn/core/db`. (See Pitfalls
69
- for the `./client` subpath.)
70
-
71
- ## How it works
72
-
73
- End-to-end, a SPFN backend is **defined once** on the server and consumed type-safely from the
74
- Next.js app. Types flow purely through TypeScript inference; the only generated artifact is the
75
- proxy's `routeMap`.
58
+ ## What does one feature look like?
59
+
60
+ Four files, always the same four, always in the same places:
61
+
62
+ ```
63
+ src/server/
64
+ entities/order.ts # the data shape (Drizzle table)
65
+ repositories/order.ts # persistence (extends BaseRepository)
66
+ routes/orders.ts # the validated API contract
67
+ router.ts # registration
68
+ ```
69
+
70
+ The point is not that this arrangement is uniquely correct. The point is that it is
71
+ decided. An agent asked for "add orders" twice produces the same code twice, because
72
+ there is nothing left to choose.
73
+
74
+ ---
75
+
76
+ ## How does a route's type reach the browser?
77
+
78
+ Through TypeScript inference, not generated client code. One artifact is generated —
79
+ the route map the RPC proxy needs and nothing else.
76
80
 
77
81
  ```
78
82
  ① route DSL ② defineRouter ③ defineServerConfig → startServer
@@ -81,7 +85,7 @@ proxy's `routeMap`.
81
85
  .handler(c => …) export type AppRouter startServer() → Hono on :8790
82
86
  │ = typeof appRouter ▲
83
87
  │ TypeBox = runtime validation + compile-time types │ registerRoutes mounts routes
84
-
88
+
85
89
  ④ codegen (@spfn/core:route-map) ──► routeMap = { getUser: { method:'GET', path:'/users/:id' }, … }
86
90
 
87
91
 
@@ -94,30 +98,34 @@ proxy's `routeMap`.
94
98
  ```
95
99
 
96
100
  1. **Define a route** with `route.<method>(path).input({...}).handler(c => …)` from
97
- `@spfn/core/route`. TypeBox schemas in `.input()` give both runtime validation and the
98
- compile-time types the handler (`await c.data()`) and the client see. The handler's **return
99
- type is inferred** — no manual response typing.
100
- 2. **Compose routes** with `defineRouter({ … })` and export `type AppRouter = typeof appRouter`.
101
- That type is the single source of truth for the client; no client codegen reads it.
102
- 3. **Boot the server** with `defineServerConfig().routes(appRouter).build()` then `startServer()`
103
- (`@spfn/core/server`). It auto-wires `ErrorHandler`/`RequestLogger`, inits DB/cache, mounts
104
- routes via `registerRoutes`, and runs jobs/events. A request is matched by Hono → global
105
- middleware route middleware (`.use([...])`, e.g. `Transactional()`) → input validation/type
106
- conversion handler serialized response.
107
- 4. **Generate the route-map** with codegen (`@spfn/core:route-map`, e.g. `pnpm codegen`). It emits
108
- `routeName { method, path }` the only thing the proxy needs to resolve the *real* backend
109
- method/path.
110
- 5. **Mount the RPC proxy** in Next.js: `createRpcProxy({ routeMap })` from
111
- `@spfn/core/nextjs/server` as the `app/api/rpc/[routeName]/route.ts` catch-all. The client only
112
- ever sends `GET` (no body) or `POST` (body/formData) to `/api/rpc/{routeName}`; the proxy looks
113
- up `routeMap[routeName]`, substitutes `:params`, and forwards to the backend with the resolved
114
- method. Package route maps (`authRouteMap`, `eventRouteMap`) merge into the same `routeMap`.
115
- 6. **Call it** through `createApi<AppRouter>()` from `@spfn/core/nextjs`. The client is a `Proxy`
116
- over `AppRouter` — `api.getUser.call({ params })` is fully typed in and out, with zero runtime
117
- type cost. Errors come back as `ApiError`, or as the original typed error when its class is in
118
- the client's `errorRegistry` (the core registry is always merged in).
119
-
120
- ## Quick example
101
+ `@spfn/core/route`. The TypeBox schemas in `.input()` do double duty: they validate
102
+ the request at runtime and give the handler (`await c.data()`) and the client their
103
+ compile-time types. The handler's **return type is inferred** — you never write a
104
+ response type.
105
+ 2. **Compose routes** with `defineRouter({ })` and export
106
+ `type AppRouter = typeof appRouter`. That type is the single source of truth for the
107
+ client.
108
+ 3. **Boot the server** with `defineServerConfig().routes(appRouter).build()` and
109
+ `startServer()` from `@spfn/core/server`. It wires `ErrorHandler` and
110
+ `RequestLogger`, initializes the database and cache, mounts the routes, and starts
111
+ jobs and events. A request goes: Hono match global middleware route middleware
112
+ (`.use([...])`, e.g. `Transactional()`) input validation handler response.
113
+ 4. **Generate the route map** with `pnpm codegen`. It emits
114
+ `routeName { method, path }` — the only thing the proxy needs to find the real
115
+ backend endpoint.
116
+ 5. **Mount the RPC proxy** in Next.js as the `app/api/rpc/[routeName]/route.ts`
117
+ catch-all. The browser only ever sends `GET` (no body) or `POST` (body or formData)
118
+ to `/api/rpc/{routeName}`; the proxy looks up `routeMap[routeName]`, substitutes
119
+ `:params`, and forwards with the real method. Package route maps (`authRouteMap`,
120
+ `eventRouteMap`) merge into the same map.
121
+ 6. **Call it** through `createApi<AppRouter>()`. The client is a `Proxy` over the
122
+ `AppRouter` type typed in and out, with no runtime cost for the types. Errors
123
+ arrive as `ApiError`, or as the original error class when that class is registered in
124
+ the client's `errorRegistry`.
125
+
126
+ ---
127
+
128
+ ## Show me the whole thing in code
121
129
 
122
130
  ```typescript
123
131
  // server/router.ts — ① + ②
@@ -136,7 +144,7 @@ export const appRouter = defineRouter({
136
144
 
137
145
  createUser: route.post('/users')
138
146
  .input({ body: Type.Object({ name: Type.String() }) })
139
- .use([Transactional()]) // auto commit/rollback
147
+ .use([Transactional()]) // commit on return, rollback on throw
140
148
  .handler(async (c) =>
141
149
  {
142
150
  const { body } = await c.data();
@@ -171,40 +179,225 @@ import type { AppRouter } from '@/server/router';
171
179
 
172
180
  export const api = createApi<AppRouter>();
173
181
 
174
- // anywhere (RSC / client / server action):
182
+ // anywhere server component, client component, or server action:
175
183
  const user = await api.getUser.call({ params: { id: '123' } }); // typed { id, name }
176
184
  const made = await api.createUser.call({ body: { name: 'A' } });
177
185
  ```
178
186
 
187
+ ---
188
+
189
+ ## Which import path do I use for what?
190
+
191
+ There is **no root barrel**: `import … from '@spfn/core'` does not resolve. Every symbol
192
+ comes from a subpath, and the table below is the complete public surface — one row per
193
+ entry in `package.json` `exports`. Each module has its own README with the API detail.
194
+
195
+ | Import path | Purpose | Doc |
196
+ |-------------|---------|-----|
197
+ | `@spfn/core/route` | The route DSL (`route.get(...).input(...).handler(...)`) plus `defineRouter`, `registerRoutes`, `defineMiddleware`. The core of the core. | [src/route](./src/route/README.md) |
198
+ | `@spfn/core/route/types` | Shared route types (`HttpMethod`, router type primitives). | [src/route](./src/route/README.md) |
199
+ | `@spfn/core/server` | Server entry: `defineServerConfig()` → `startServer()`, plus `createServerlessApp()` for Vercel. Middleware wiring, infra init, graceful shutdown. | [src/server](./src/server/README.md) |
200
+ | `@spfn/core/nextjs` | Client-safe: `createApi<AppRouter>()`, `ApiError`, client types. Never touches `next/headers`. | [src/nextjs](./src/nextjs/README.md) |
201
+ | `@spfn/core/nextjs/server` | Server-only: `createRpcProxy({ routeMap })`, `registerInterceptors`. Uses `next/headers`. | [src/nextjs](./src/nextjs/README.md) |
202
+ | `@spfn/core/db` | PostgreSQL through Drizzle: CRUD helpers, `BaseRepository`, schema helpers, transactions, Postgres error mapping. One entry point for all of it. | [src/db](./src/db/README.md) |
203
+ | `@spfn/core/db` → manager | Connection lifecycle, pool, primary/replica, health check, reconnect (`initDatabase`, `getDatabase`). Re-exported from `@spfn/core/db`. | [src/db/manager](./src/db/manager/README.md) |
204
+ | `@spfn/core/db` → schema | Drizzle column helpers (`id`, `uuid`, `timestamps`, `foreignKey`, `enumText`, `typedJsonb`, `softDelete`, …). Re-exported from `@spfn/core/db`. | [src/db/schema](./src/db/schema/README.md) |
205
+ | `@spfn/core/db` → transaction | `Transactional()` middleware and `runInTransaction`; the transaction reaches every repository through AsyncLocalStorage. Re-exported from `@spfn/core/db`. | [src/db/transaction](./src/db/transaction/README.md) |
206
+ | `@spfn/core/middleware` | Built-in Hono middleware: `ErrorHandler`, `RequestLogger` and its masking helper. | [src/middleware](./src/middleware/README.md) |
207
+ | `@spfn/core/errors` | Serializable HTTP and database error classes, plus `ErrorRegistry` so an error survives the trip to the client as its own class. | [src/errors](./src/errors/README.md) |
208
+ | `@spfn/core/security` | `safeFetch` — a drop-in `fetch` hardened against SSRF, including DNS rebinding, by pinning the connection to a validated IP. | [src/security](./src/security/README.md) |
209
+ | `@spfn/core/authz` | Ownership guards. `requireOwner(resource, userId)` makes "load it, then check it belongs to the requester" one call, so a handler cannot forget it. | [src/authz/index.ts](./src/authz/index.ts) |
210
+ | `@spfn/core/env` | Schema-based environment validation, isomorphic. | [src/env](./src/env/README.md) |
211
+ | `@spfn/core/env/loader` | The **server-only** `.env` file loader (uses `node:fs`). | [src/env](./src/env/README.md) |
212
+ | `@spfn/core/config` | `@spfn/core`'s own validated env config (`env`, `envSchema`, `registry`), built on `@spfn/core/env`. | [src/config](./src/config/README.md) |
213
+ | `@spfn/core/logger` | Structured singleton `logger` with child loggers and level masking. No dependencies. | [src/logger](./src/logger/README.md) |
214
+ | `@spfn/core/cache` | Valkey/Redis singleton over ioredis (`getCache`, `getCacheRead`). Degrades to disabled rather than throwing. | [src/cache](./src/cache/README.md) |
215
+ | `@spfn/core/job` | Background jobs on pg-boss: a fluent `job()` builder, cron, run-once, event-driven, `defineJobRouter`. | [src/job](./src/job/README.md) |
216
+ | `@spfn/core/event` | Decoupled pub/sub (`defineEvent`, `defineEventRouter`, `eventRouteMap`). | [src/event](./src/event/README.md) |
217
+ | `@spfn/core/event/sse` | Server-side SSE handler and token manager. Server only. | [src/event](./src/event/README.md) |
218
+ | `@spfn/core/event/sse/client` | Browser SSE client (`EventSource`). | [src/event](./src/event/README.md) |
219
+ | `@spfn/core/event/ws` | Server-side WebSocket handler. Server only; needs the optional `ws` dependency. | [src/event](./src/event/README.md) |
220
+ | `@spfn/core/event/ws/client` | Browser WebSocket client. | [src/event](./src/event/README.md) |
221
+ | `@spfn/core/codegen` | The codegen orchestrator and the built-in generators: `@spfn/core:route-map` for the proxy's route map, `@spfn/core:contract` for the client contract. | [src/codegen](./src/codegen/README.md) |
222
+ | `@spfn/core/contract` | Route contracts for clients that ship separately: collect, snapshot, and the build gate that refuses a breaking change. | [src/contract](./src/contract/README.md) |
223
+
224
+ `db/manager`, `db/schema` and `db/transaction` are **not** package subpaths of their own.
225
+ They are internal modules re-exported by `@spfn/core/db` — import their symbols from
226
+ there.
227
+
228
+ ---
229
+
230
+ ## Do I have to run codegen?
231
+
232
+ Only for the RPC proxy, and only after you change routes.
233
+
234
+ | Change | What to run |
235
+ |---|---|
236
+ | Added, renamed or removed a route | `pnpm codegen`, then commit the regenerated route map |
237
+ | Changed a route's input or output types | Nothing — the client infers from `AppRouter` |
238
+ | Changed an entity | `pnpm db:generate` for the migration |
239
+
240
+ A route name that is missing from the merged route map produces a **404 from the proxy**,
241
+ not from your backend. That is almost always a codegen you did not re-run.
242
+
243
+ Generated files are output. Never hand-edit them.
244
+
245
+ ---
246
+
247
+ ## Can I deploy this to Vercel?
248
+
249
+ Yes, and it is a first-class target rather than a workaround. From your app:
250
+
251
+ ```bash
252
+ spfn add vercel
253
+ ```
254
+
255
+ That scaffolds `src/app/api/backend/[[...route]]/route.ts` (a `hono/vercel` adapter) and
256
+ `vercel.json`. The SPFN app mounts under `/api/backend`, so the Next.js frontend and the
257
+ backend share one Vercel origin — point `SPFN_API_URL` at
258
+ `https://<your-domain>/api/backend`. It runs on the Node runtime, not edge, because SPFN
259
+ needs `pg` and native `bcrypt`.
260
+
261
+ The adapter is a thin wrapper around `createServerlessApp` from `@spfn/core/server`:
262
+
263
+ ```typescript
264
+ import { handle } from 'hono/vercel';
265
+ import { createServerlessApp } from '@spfn/core/server';
266
+ import serverConfig from '@/server/server.config';
267
+
268
+ const handler = async (req: Request) => handle(await createServerlessApp(serverConfig))(req);
269
+ export const GET = handler;
270
+ export const POST = handler;
271
+ ```
272
+
273
+ It differs from `startServer()` in four ways, all forced by the platform:
274
+
275
+ | | Always-on (`startServer`) | Serverless (`createServerlessApp`) |
276
+ |---|---|---|
277
+ | Database init | welded to `serve()` | in the handler, once per warm container |
278
+ | Periodic DB health check | on | off — a timer only leaks across frozen invocations |
279
+ | Background job worker | runs in-process | **not started** — enqueuing works, nothing drains the queue |
280
+ | Seed and RBAC provisioning | per boot | a deploy-time step (`provisionInfrastructure`) |
281
+
282
+ The job worker is the one that bites. If your config declares jobs, the serverless path
283
+ logs a warning at startup: drain the queue from a scheduled endpoint (Vercel Cron calling
284
+ a route that processes a batch), or run jobs on an always-on target.
285
+
286
+ Background jobs, WebSocket events and the periodic health check all need the always-on
287
+ path: `spfn build && spfn start`, or the generated Docker files.
288
+
289
+ ---
290
+
291
+ ## How do I point Claude Code or another AI coding agent at this?
292
+
293
+ Put the contract in a file and let the agent read it, instead of describing the
294
+ architecture again in every prompt.
295
+
296
+ An SPFN repository ships an `AGENTS.md` stating what the repo is, the commands, the
297
+ vertical-slice pattern, and the rules that are not negotiable — never hand-edit generated
298
+ files, migrations come from the schema. Tool-specific files (`CLAUDE.md`,
299
+ `.cursorrules`) point at it in one line rather than duplicating it, so they cannot drift
300
+ apart. Projects created by `spfn create` get the same arrangement.
301
+
302
+ Each module README under `src/` is written for the same reader. When an agent is working
303
+ on database code, `src/db/README.md` is the page to give it.
304
+
305
+ ---
306
+
307
+ ## How is this different from NestJS or tRPC?
308
+
309
+ **NestJS** gives you a structured backend and leaves the structure to you: modules and
310
+ providers you design, validation and ORM of your choosing, DTOs you keep in sync with the
311
+ frontend by hand. It is the better fit when you need that freedom, or when your frontend
312
+ is not Next.js. `@spfn/core` fixes the shape instead — one vertical slice per feature,
313
+ TypeBox, Drizzle, and route types that reach the browser without a DTO layer. Its
314
+ ecosystem is small and young where NestJS's is large and mature.
315
+
316
+ **tRPC** solves a narrower problem, typed calls between client and server, and leaves
317
+ structure, persistence and auth to you. `@spfn/core` includes the typed-call layer as one
318
+ part of a whole backend.
319
+
320
+ ---
321
+
179
322
  ## Pitfalls
180
323
 
181
- - **No root barrel.** `import … from '@spfn/core'` does not resolve. Import from a subpath
182
- (`@spfn/core/route`, `@spfn/core/db`, …). The list above *is* the complete public surface.
183
- - **Use `@spfn/core/nextjs` for the client, not `@spfn/core/client`.** `package.json` still lists a
184
- `./client` export, but the build does not emit it (no `src/client`; the tsup `client` entry is
185
- disabled). `createApi` / `ApiError` and all client types ship from **`@spfn/core/nextjs`**. Treat
186
- `@spfn/core/client` as non-functional.
187
- - **Client vs. server boundary is load-bearing.** `@spfn/core/nextjs/server` pulls in
188
- `next/headers` + `next/server`; never import it from a Client Component. `@spfn/core/env/loader`,
189
- `@spfn/core/event/sse`, `@spfn/core/event/ws` are server-only (`node:fs` / Hono / `node:crypto`).
190
- Client code uses the `*/client` and isomorphic entry points only.
191
- - **`db/manager`, `db/schema`, `db/transaction` are not package subpaths.** Importing
192
- `@spfn/core/db/transaction` fails to resolve import those symbols from `@spfn/core/db`.
193
- - **The client needs no codegen; the proxy does.** `createApi<AppRouter>()` is metadata-free and
194
- driven by the `AppRouter` *type*. The generated `routeMap` is consumed by `createRpcProxy`. A
195
- `routeName` missing from the merged `routeMap` is a **proxy 404**, not a backend 404 — re-run
196
- codegen after adding routes.
197
- - **The proxy decides the real HTTP method.** The client only sends GET/POST to `/api/rpc/...`; a
198
- PUT/PATCH/DELETE route still works because the backend method comes from `routeMap[routeName]`.
199
- - **Cache/event/job degrade or depend on optional deps.** `@spfn/core/cache` runs disabled (getters
200
- return `undefined`) without `CACHE_*` config or `ioredis`; WebSocket events need the optional `ws`
201
- dep. Don't assume they throw.
324
+ - **There is no root barrel.** `import … from '@spfn/core'` does not resolve. Import from
325
+ a subpath. The module table above is the complete surface.
326
+ - **Use `@spfn/core/nextjs` for the client, not `@spfn/core/client`.** `package.json`
327
+ still lists a `./client` export, but the build does not emit it the entry is disabled
328
+ and there is no `src/client`. `createApi`, `ApiError` and every client type ship from
329
+ `@spfn/core/nextjs`. Treat `@spfn/core/client` as non-functional.
330
+ - **The client/server boundary is load-bearing.** `@spfn/core/nextjs/server` pulls in
331
+ `next/headers` and `next/server`; importing it from a Client Component breaks the
332
+ build. `@spfn/core/env/loader`, `@spfn/core/event/sse` and `@spfn/core/event/ws` are
333
+ server-only too. Client code uses the `*/client` and isomorphic entry points.
334
+ - **`db/manager`, `db/schema` and `db/transaction` are not subpaths.** Importing
335
+ `@spfn/core/db/transaction` fails to resolve. Import those symbols from `@spfn/core/db`.
336
+ - **The client needs no codegen; the proxy does.** A missing route name is a proxy 404.
337
+ - **Contracts are for clients TypeScript cannot reach.** `.contract()` and the build gate
338
+ exist for a mobile app or an external consumer, compiled and shipped separately. A web
339
+ client takes its types from `AppRouter` in the same build, so a removed response field
340
+ already breaks the compile. Do not put `.contract()` on a route only the web app calls.
341
+ - **The proxy decides the real HTTP method.** The browser only sends GET or POST to
342
+ `/api/rpc/...`; a PUT, PATCH or DELETE route still works because the method comes from
343
+ the route map.
344
+ - **Cache, events and jobs degrade quietly.** `@spfn/core/cache` runs disabled — its
345
+ getters return `undefined` — when there is no cache config or no `ioredis`, and
346
+ WebSocket events need the optional `ws` dependency. Do not write code expecting them to
347
+ throw.
348
+
349
+ ---
350
+
351
+ ## FAQ
352
+
353
+ **Can I use @spfn/core without Next.js?**
354
+ Yes. `next` is an optional peer dependency and the Next.js bridge is two modules
355
+ (`@spfn/core/nextjs`, `@spfn/core/nextjs/server`). The server runs on Hono by itself. You
356
+ give up the typed client and the RPC proxy, which are the Next.js-side pieces.
357
+
358
+ **Can I use Prisma instead of Drizzle?**
359
+ No. `drizzle-orm` is a required peer dependency (`>=1.0.0-rc.4 <2`) and the repository
360
+ layer is built on it.
361
+
362
+ **Do I need Redis?**
363
+ Only for features that use it. `ioredis` is an optional dependency, and without cache
364
+ configuration the cache module reports itself disabled instead of failing. The same holds
365
+ for `ws` and WebSocket events.
366
+
367
+ **Does @spfn/core require PostgreSQL specifically?**
368
+ Yes, 14 or later. The data layer is Drizzle on Postgres, with `postgres.js` as the
369
+ default driver; the provider is injectable, which is how PGlite works in tests.
370
+
371
+ **Why can't I import from `@spfn/core` directly?**
372
+ There is no `.` entry in `exports` — by design. Subpaths keep server-only code out of
373
+ client bundles, which a single barrel file cannot do.
374
+
375
+ **Which Node version?**
376
+ `>=18.18.0`. The package is ESM only.
377
+
378
+ **Where do my business rules go?**
379
+ In services and repositories, not in route handlers. A handler validates, calls, and
380
+ returns. `@spfn/core/authz` covers the one rule that is easy to forget: `requireOwner`
381
+ returns the resource only if it belongs to the requester, and answers "not found" either
382
+ way so the endpoint never reveals that someone else's record exists.
383
+
384
+ ---
202
385
 
203
386
  ## Related packages
204
387
 
205
388
  Other SPFN packages build on `@spfn/core`:
206
389
 
207
- - [`@spfn/auth`](../auth/README.md) — auth/session; exports `authRouteMap` and auto-registers proxy
208
- interceptors (merge its route map into `createRpcProxy`).
209
- - `@spfn/cms`, `@spfn/workflow`, `@spfn/notification`, `@spfn/monitor`, `@spfn/cli`
210
- see each package's README under `packages/`.
390
+ - [`@spfn/auth`](../auth/README.md) — sessions, social login and RBAC. Exports
391
+ `authRouteMap` and registers proxy interceptors automatically; merge its route map into
392
+ `createRpcProxy`.
393
+ - [`@spfn/mcp`](../mcp/README.md) exposes your operations as MCP tools, so an agent can
394
+ run them instead of you building an admin dashboard.
395
+ - `@spfn/i18n`, `@spfn/storage`, `@spfn/notification`, `@spfn/cms`, `@spfn/monitor`,
396
+ `@spfn/migrate`, `@spfn/workflow` — see each package's README under
397
+ [`packages/`](../).
398
+
399
+ ---
400
+
401
+ ## License
402
+
403
+ MIT © FXY Inc.