@spfn/core 0.2.0-beta.5 → 0.2.0-beta.51

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 (64) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +181 -1281
  3. package/dist/{boss-BO8ty33K.d.ts → boss-Cxqc-Oiw.d.ts} +37 -7
  4. package/dist/cache/index.js +32 -29
  5. package/dist/cache/index.js.map +1 -1
  6. package/dist/codegen/index.d.ts +55 -8
  7. package/dist/codegen/index.js +179 -5
  8. package/dist/codegen/index.js.map +1 -1
  9. package/dist/config/index.d.ts +168 -6
  10. package/dist/config/index.js +29 -5
  11. package/dist/config/index.js.map +1 -1
  12. package/dist/db/index.d.ts +218 -4
  13. package/dist/db/index.js +351 -57
  14. package/dist/db/index.js.map +1 -1
  15. package/dist/env/index.d.ts +26 -2
  16. package/dist/env/index.js +11 -2
  17. package/dist/env/index.js.map +1 -1
  18. package/dist/env/loader.d.ts +26 -19
  19. package/dist/env/loader.js +32 -25
  20. package/dist/env/loader.js.map +1 -1
  21. package/dist/errors/index.js.map +1 -1
  22. package/dist/event/index.d.ts +33 -3
  23. package/dist/event/index.js +17 -1
  24. package/dist/event/index.js.map +1 -1
  25. package/dist/event/sse/client.d.ts +42 -3
  26. package/dist/event/sse/client.js +128 -45
  27. package/dist/event/sse/client.js.map +1 -1
  28. package/dist/event/sse/index.d.ts +12 -5
  29. package/dist/event/sse/index.js +188 -20
  30. package/dist/event/sse/index.js.map +1 -1
  31. package/dist/event/ws/client.d.ts +59 -0
  32. package/dist/event/ws/client.js +273 -0
  33. package/dist/event/ws/client.js.map +1 -0
  34. package/dist/event/ws/index.d.ts +94 -0
  35. package/dist/event/ws/index.js +213 -0
  36. package/dist/event/ws/index.js.map +1 -0
  37. package/dist/job/index.d.ts +23 -8
  38. package/dist/job/index.js +154 -44
  39. package/dist/job/index.js.map +1 -1
  40. package/dist/logger/index.d.ts +5 -0
  41. package/dist/logger/index.js +14 -0
  42. package/dist/logger/index.js.map +1 -1
  43. package/dist/middleware/index.d.ts +23 -1
  44. package/dist/middleware/index.js +58 -5
  45. package/dist/middleware/index.js.map +1 -1
  46. package/dist/nextjs/index.d.ts +2 -2
  47. package/dist/nextjs/index.js +77 -31
  48. package/dist/nextjs/index.js.map +1 -1
  49. package/dist/nextjs/server.d.ts +44 -23
  50. package/dist/nextjs/server.js +83 -65
  51. package/dist/nextjs/server.js.map +1 -1
  52. package/dist/route/index.d.ts +158 -4
  53. package/dist/route/index.js +238 -22
  54. package/dist/route/index.js.map +1 -1
  55. package/dist/server/index.d.ts +308 -17
  56. package/dist/server/index.js +1128 -261
  57. package/dist/server/index.js.map +1 -1
  58. package/dist/{router-Di7ENoah.d.ts → token-manager-CyG7la3p.d.ts} +116 -1
  59. package/dist/{types-D_N_U-Py.d.ts → types-7Mhoxnnt.d.ts} +21 -1
  60. package/dist/types-C1jMLGwK.d.ts +257 -0
  61. package/dist/types-Cfj--lfr.d.ts +151 -0
  62. package/docs/file-upload.md +717 -0
  63. package/package.json +18 -5
  64. package/dist/types-B-e_f2dQ.d.ts +0 -121
package/README.md CHANGED
@@ -1,1308 +1,208 @@
1
- # @spfn/core - Technical Architecture Documentation
1
+ # @spfn/core — Type-safe Next.js + Hono backend framework (route DSL → RPC proxy → typed client)
2
2
 
3
- Full-stack type-safe framework for building Next.js + Node.js applications with end-to-end type inference.
3
+ `@spfn/core` is the backend runtime for SPFN: a tRPC-style **route DSL** (TypeBox-validated,
4
+ end-to-end typed), a Drizzle/postgres.js **data layer**, an HTTP **server** entry point, and a
5
+ Next.js **RPC proxy + typed client** that wires a browser/RSC app to that backend with
6
+ compile-time-only type inference.
4
7
 
5
- [![npm version](https://badge.fury.io/js/@spfn%2Fcore.svg)](https://www.npmjs.com/package/@spfn/core)
6
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
- [![TypeScript](https://img.shields.io/badge/TypeScript-5.3-blue)](https://www.typescriptlang.org/)
8
+ There is **no root barrel** — `@spfn/core` itself imports nothing. Everything is reached
9
+ through subpath exports (`@spfn/core/route`, `@spfn/core/db`, `@spfn/core/nextjs`, …). Each
10
+ module is canonically documented in its own `src/<mod>/README.md`; this file is the index +
11
+ end-to-end flow. Follow the links for API detail.
8
12
 
9
- > **Beta Release**: SPFN is currently in beta. Core APIs are stable but may have minor changes before 1.0.
13
+ ## Install
10
14
 
11
- ---
12
-
13
- ## Table of Contents
14
-
15
- - [Overview & Philosophy](#overview--philosophy)
16
- - [System Architecture](#system-architecture)
17
- - [Module Architecture](#module-architecture)
18
- - [Type System](#type-system)
19
- - [Integration Points](#integration-points)
20
- - [Design Decisions](#design-decisions)
21
- - [Extension Points](#extension-points)
22
- - [Migration Guides](#migration-guides)
23
- - [Module Exports](#module-exports)
24
- - [Quick Reference](#quick-reference)
25
-
26
- ---
27
-
28
- ## Overview & Philosophy
29
-
30
- SPFN (Superfunction) is a full-stack TypeScript framework that provides **end-to-end type safety** from database to frontend with a **tRPC-inspired developer experience**.
31
-
32
- ### Core Principles
33
-
34
- 1. **Type Safety First**: Types flow from database schema server routes → client API
35
- 2. **Developer Experience**: tRPC-style API with method chaining (`.params().query().call()`)
36
- 3. **Explicit over Magic**: No file-based routing, explicit imports for tree-shaking
37
- 4. **Security by Default**: HttpOnly cookies, API Route Proxy, environment isolation
38
- 5. **Production Ready**: Transaction management, read/write separation, graceful shutdown
39
-
40
- ### Design Philosophy
41
-
42
- **Inspired by tRPC, Built for Production:**
43
-
44
- ```typescript
45
- // tRPC-style API calls with structured input
46
- const user = await api.getUser.call({
47
- params: { id: '123' },
48
- query: { include: 'posts' }
49
- });
50
- // ^? { id: string; name: string; email: string; posts: Post[] }
51
- ```
52
-
53
- **But with production features:**
54
- - Cookie handling via API Route Proxy
55
- - Transaction management with AsyncLocalStorage
56
- - Read/Write database separation
57
- - Graceful shutdown and health checks
58
- - Lifecycle hooks for extensibility
59
-
60
- ---
61
-
62
- ## System Architecture
63
-
64
- ### High-Level Overview
65
-
66
- ```
67
- +---------------------------------------------------------------+
68
- | Next.js Application |
69
- | +----------------------------------------------------------+ |
70
- | | Frontend (React) | |
71
- | | - Server Components: SSR, ISR, Static | |
72
- | | - Client Components: Interactive UI | |
73
- | +---------------------------+------------------------------+ |
74
- | | |
75
- | | import { api } from '@spfn/...' |
76
- | | api.getUser.call({ params }) |
77
- | v |
78
- | +----------------------------------------------------------+ |
79
- | | RPC Proxy (Edge/Node.js) | |
80
- | | app/api/rpc/[routeName]/route.ts | |
81
- | | | |
82
- | | 1. Resolve routeName method/path from router | |
83
- | | | |
84
- | | 2. Request Interceptors | |
85
- | | - Auth token injection | |
86
- | | - Cookie forwarding | |
87
- | | - Header manipulation | |
88
- | | | |
89
- | | 3. Forward to SPFN Server | |
90
- | | fetch(SPFN_API_URL + resolvedPath) | |
91
- | | | |
92
- | | 4. Response Interceptors | |
93
- | | - Set HttpOnly cookies | |
94
- | | - Transform response | |
95
- | | - Error handling | |
96
- | +---------------------------+------------------------------+ |
97
- +-------------------------------+--------------------------------+
98
- |
99
- | HTTP Request
100
- v
101
- +---------------------------------------------------------------+
102
- | SPFN API Server (Node.js) |
103
- | +----------------------------------------------------------+ |
104
- | | Hono Web Framework | |
105
- | | | |
106
- | | 12-Step Middleware Pipeline: | |
107
- | | 1. Logger | |
108
- | | 2. CORS | |
109
- | | 3. Global Middlewares | |
110
- | | 4. Route-specific Middlewares | |
111
- | | 5. Request Validation (TypeBox) | |
112
- | | 6. Route Handler | |
113
- | | 7-12. Response processing, error handling | |
114
- | +---------------------------+------------------------------+ |
115
- | | |
116
- | | define-route System |
117
- | v |
118
- | +----------------------------------------------------------+ |
119
- | | Route Handlers | |
120
- | | - Type-safe input validation | |
121
- | | - Transaction middleware | |
122
- | | - Business logic | |
123
- | +---------------------------+------------------------------+ |
124
- | | |
125
- | | Database Queries |
126
- | v |
127
- | +----------------------------------------------------------+ |
128
- | | Database Layer (Drizzle ORM) | |
129
- | | - Helper functions (findOne, create, etc.) | |
130
- | | - Transaction propagation (AsyncLocalStorage) | |
131
- | | - Read/Write separation | |
132
- | +---------------------------+------------------------------+ |
133
- +-------------------------------+--------------------------------+
134
- |
135
- | SQL Queries
136
- v
137
- +---------------------------------------------------------------+
138
- | PostgreSQL Database |
139
- | - Primary (Read/Write) |
140
- | - Replica (Read-only) [optional] |
141
- +---------------------------------------------------------------+
142
- ```
143
-
144
- ### Request Flow Example
145
-
146
- ```typescript
147
- // 1. Client Call (Next.js Server Component)
148
- // app/users/[id]/page.tsx
149
- import { createApi } from '@spfn/core/nextjs';
150
- import type { AppRouter } from '@/server/router';
151
-
152
- const api = createApi<AppRouter>();
153
- const user = await api.getUser.call({ params: { id: params.id } });
154
- // → GET /api/rpc/getUser?input={"params":{"id":"123"}}
155
-
156
- // 2. RPC Proxy
157
- // app/api/rpc/[routeName]/route.ts
158
- import { appRouter } from '@/server/router';
159
- import { createRpcProxy } from '@spfn/core/nextjs/server';
160
-
161
- export const { GET, POST } = createRpcProxy({ router: appRouter });
162
- // - Resolves routeName → method/path from router
163
- // - Forwards to http://localhost:8790/users/123
164
- // - Applies interceptors (auth, cookies)
165
-
166
- // 3. SPFN Server Route Handler
167
- // src/server/routes/users.ts
168
- export const getUser = route.get('/users/:id')
169
- .input({
170
- params: Type.Object({ id: Type.String() }),
171
- })
172
- .handler(async (c) => {
173
- const { params } = await c.data();
174
- const user = await userRepo.findById(params.id);
175
- return user;
176
- });
177
-
178
- // 4. Database Query
179
- // Drizzle ORM with helper function
180
- // SELECT * FROM users WHERE id = $1
181
-
182
- // 5. Response flows back through interceptors → proxy → client
183
- ```
184
-
185
- ---
186
-
187
- ## Module Architecture
188
-
189
- ### 1. Route System (`src/route/`)
190
-
191
- **Purpose**: Type-safe routing with automatic validation
192
-
193
- **Architecture**:
194
-
195
- ```
196
- route.get('/users/:id')
197
- .input({ params, query, body })
198
- .handler(async (c) => { ... })
199
- |
200
- |-- Type Inference: RouteDef<TInput, TResponse>
201
- |-- Validation: TypeBox schema
202
- |-- Middleware: Skip control per route
203
- |-- Response: Direct return / c.json() / helpers
204
- ```
205
-
206
- **Key Components**:
207
-
208
- - `defineRouter()`: Combines route definitions into typed router
209
- - `route.get/post/put/patch/delete()`: Route builder with method chaining
210
- - Input validation: Automatic TypeBox validation
211
- - Middleware control: Per-route middleware skip
212
-
213
- **Design Pattern**: Builder pattern with type inference
214
-
215
- **[→ Full Documentation](./src/route/README.md)**
216
-
217
- ---
218
-
219
- ### 2. Server System (`src/server/`)
220
-
221
- **Purpose**: HTTP server with lifecycle management
222
-
223
- **Architecture**:
224
-
225
- ```
226
- Configuration Sources (Priority Order):
227
- 1. Runtime config (startServer({ port: 3000 }))
228
- 2. server.config.ts (defineServerConfig().build())
229
- 3. Environment variables (PORT, DATABASE_URL)
230
- 4. Defaults
231
-
232
- |
233
- v
234
- Middleware Pipeline:
235
- 1. Request Logger
236
- 2. CORS
237
- 3. Global Middlewares
238
- 4. Named Middlewares
239
- 5. beforeRoutes hook
240
- 6. Route Registration
241
- 7. afterRoutes hook
242
- 8. Route-specific Middlewares
243
- 9. Request Validation
244
- 10. Route Handler Execution
245
- 11. Response Serialization
246
- 12. Error Handler
247
-
248
- |
249
- v
250
- Lifecycle Hooks:
251
- - beforeInfrastructure
252
- - afterInfrastructure
253
- - beforeRoutes
254
- - afterRoutes
255
- - afterStart
256
- - beforeShutdown
257
- ```
258
-
259
- **Key Components**:
260
-
261
- - `defineServerConfig()`: Fluent configuration builder
262
- - `createServer()`: Creates Hono app with routes
263
- - `startServer()`: Starts HTTP server with lifecycle
264
- - Lifecycle hooks: beforeInfrastructure, afterInfrastructure, beforeRoutes, afterRoutes, afterStart, beforeShutdown
265
- - Graceful shutdown: SIGTERM/SIGINT handling
266
-
267
- **Design Pattern**: Builder + Lifecycle hooks
268
-
269
- **[→ Full Documentation](./src/server/README.md)**
270
-
271
- ---
272
-
273
- ### 3. Database System (`src/db/`)
274
-
275
- **Purpose**: Type-safe database operations with transactions
276
-
277
- **Architecture**:
278
-
279
- ```
280
- Application Code
281
- |
282
- | findOne(users, { id: 1 })
283
- v
284
- Helper Functions (Facade)
285
- |
286
- | Check AsyncLocalStorage for transaction
287
- v
288
- Transaction Context?
289
- |-- Yes: Use transaction instance
290
- |-- No: Use default database connection
291
- |
292
- v
293
- Drizzle ORM Query Builder
294
- |
295
- v
296
- PostgreSQL (Primary or Replica)
297
- ```
298
-
299
- **Key Components**:
300
-
301
- - Helper functions: `findOne`, `findMany`, `create`, `update`, `delete`, `count`
302
- - Transaction middleware: `Transactional()` with AsyncLocalStorage propagation
303
- - Read/Write separation: Automatic routing based on operation
304
- - Schema helpers: `id()`, `timestamps()`, `foreignKey()`, `enumText()`
305
-
306
- **Design Pattern**: Facade + AsyncLocalStorage for transaction propagation
307
-
308
- **[→ Full Documentation](./src/db/README.md)**
309
-
310
- **Transaction Flow**:
311
-
312
- ```typescript
313
- // Route with Transactional middleware
314
- app.bind(createUserContract, [Transactional()], async (c) => {
315
- // All database operations use the same transaction
316
- const user = await create(users, { name: 'John' });
317
- const profile = await create(profiles, { userId: user.id });
318
-
319
- // Auto-commit on success
320
- // Auto-rollback on error
321
- return c.json(user);
322
- });
323
- ```
324
-
325
- ---
326
-
327
- ### 4. Client System (`src/nextjs/`)
328
-
329
- **Purpose**: Type-safe API client for Next.js with tRPC-style DX
330
-
331
- **Architecture**:
332
-
333
- ```
334
- Client Code
335
- |
336
- | api.getUser.call({ params: { id: '123' } })
337
- v
338
- ApiClient (Proxy)
339
- |
340
- | body 유무로 GET/POST 결정
341
- | GET /api/rpc/getUser?input={...}
342
- | POST /api/rpc/createUser body: {...}
343
- v
344
- fetch() to Next.js API Route
345
- |
346
- v
347
- RpcProxy (API Route Handler)
348
- |
349
- | 1. Extract routeName from URL
350
- | 2. Lookup method/path from appRouter
351
- | 3. Execute request interceptors
352
- | 4. Forward to SPFN_API_URL with resolved path
353
- | 5. Execute response interceptors
354
- | 6. Set cookies from ctx.setCookies
355
- v
356
- Return NextResponse
357
- ```
358
-
359
- **Key Components**:
360
-
361
- - `createApi()`: tRPC-style client with type inference (no metadata needed)
362
- - `createRpcProxy()`: RPC-style API Route handler with route resolution
363
- - `registerInterceptors()`: Registry for plugin interceptors
364
- - Path matching: Wildcard and param support (`/_auth/*`, `/users/:id`)
365
- - Cookie handling: `setCookies` array in response interceptors
366
-
367
- **Design Pattern**: Proxy for client, RPC resolution for proxy
368
-
369
- **[→ Full Documentation](./src/nextjs/README.md)**
370
-
371
- ---
372
-
373
- ### 5. Error System (`src/errors/`)
374
-
375
- **Purpose**: Structured error handling with HTTP status codes
376
-
377
- **Key Components**:
378
-
379
- - `ApiError`: Base error class
380
- - `ValidationError`: Input validation failures (400)
381
- - `NotFoundError`: Resource not found (404)
382
- - `ConflictError`: Duplicate resources (409)
383
- - `UnauthorizedError`: Authentication required (401)
384
-
385
- **[→ Full Documentation](./src/errors/README.md)**
386
-
387
- ---
388
-
389
- ### 6. Logger System (`src/logger/`)
390
-
391
- **Purpose**: Structured logging with transports and masking
392
-
393
- **Key Components**:
394
-
395
- - Adapter pattern: Pino (default) or custom
396
- - Sensitive data masking: Passwords, tokens, API keys
397
- - File rotation: Date and size-based with cleanup
398
- - Multiple transports: Console, File, Slack, Email
399
-
400
- **[→ Full Documentation](./src/logger/README.md)**
401
-
402
- ---
403
-
404
- ### 7. Cache System (`src/cache/`)
405
-
406
- **Purpose**: Valkey/Redis integration with master-replica support
407
-
408
- **Key Components**:
409
-
410
- - `initCache()`: Initialize connections
411
- - `getCache()`: Write operations (master)
412
- - `getCacheRead()`: Read operations (replica or master)
413
- - `isCacheDisabled()`: Check if cache is disabled
414
- - Graceful degradation when cache unavailable
415
-
416
- **[→ Full Documentation](./src/cache/README.md)**
417
-
418
- ---
419
-
420
- ### 8. Middleware System (`src/middleware/`)
421
-
422
- **Purpose**: Named middleware with skip control
423
-
424
- **Key Components**:
425
-
426
- - `defineMiddleware()`: Create named middleware
427
- - Skip control: Routes can skip specific middlewares
428
- - Built-in: Logger, CORS, Error handler
429
-
430
- **[→ Full Documentation](./src/middleware/README.md)**
431
-
432
- ---
433
-
434
- ### 9. Job System (`src/job/`)
435
-
436
- **Purpose**: Background job processing with pg-boss
437
-
438
- **Architecture**:
439
-
440
- ```
441
- job('send-email')
442
- .input(schema)
443
- .options({ retryLimit: 3 })
444
- .cron('0 9 * * *')
445
- .on(eventDef)
446
- .handler(async (input) => { ... })
447
- ```
448
-
449
- **Key Components**:
450
-
451
- - `job()`: Fluent job builder
452
- - `defineJobRouter()`: Group jobs for registration
453
- - Job types: Standard, Cron, RunOnce, Event-driven
454
- - `initBoss()`, `registerJobs()`: pg-boss lifecycle
455
-
456
- **Design Pattern**: Builder pattern (same as routes)
457
-
458
- ---
459
-
460
- ### 10. Event System (`src/event/`)
461
-
462
- **Purpose**: Decoupled pub/sub event system
463
-
464
- **Architecture**:
465
-
466
- ```
467
- defineEvent('user.created', schema)
468
- → emit(payload)
469
- → handlers (in-memory)
470
- → job queues (pg-boss)
471
- → cache pub/sub (multi-instance)
472
- ```
473
-
474
- **Key Components**:
475
-
476
- - `defineEvent()`: Define typed events
477
- - `event.emit()`: Emit event to all subscribers
478
- - `event.subscribe()`: In-memory subscription
479
- - `event.useCache()`: Multi-instance pub/sub via Redis
480
-
481
- **Integration**: `job().on(event)` for event-driven jobs
482
-
483
- ---
484
-
485
- ## Type System
486
-
487
- ### End-to-End Type Safety Flow
488
-
489
- ```typescript
490
- // 1. Database Schema (Source of Truth)
491
- // src/server/entities/users.ts
492
- export const users = pgTable('users', {
493
- id: id(),
494
- name: text('name').notNull(),
495
- email: text('email').notNull().unique(),
496
- ...timestamps(),
497
- });
498
-
499
- export type User = typeof users.$inferSelect;
500
- // ^? { id: string; name: string; email: string; createdAt: Date; updatedAt: Date }
501
-
502
- // 2. Server Route Definition
503
- // src/server/routes/users.ts
504
- import { route } from '@spfn/core/route';
15
+ ```bash
16
+ pnpm add @spfn/core
17
+ # peer (optional): next ^15 || ^16
18
+ # optional deps: ioredis (cache), ws (websocket events); pg-boss ships as a direct dep for jobs
19
+ ```
20
+
21
+ Node `>=18.18.0`. ESM-only.
22
+
23
+ > **Apps that define entities must declare the Postgres driver directly:** add
24
+ > `postgres` (and `pg`) to your app's `dependencies`. `@spfn/core` uses postgres.js
25
+ > internally, and Drizzle branches its type resolution on the `postgres`/`pg` peer. If
26
+ > the app doesn't pin the same driver, pnpm resolves `drizzle-orm` to a second instance,
27
+ > `BaseRepository` generics collapse to `unknown`, and RPC responses lose their types.
28
+ > `spfn create` adds these automatically; declare them by hand only when wiring SPFN into
29
+ > an existing app.
30
+ >
31
+ > ```bash
32
+ > pnpm add postgres pg
33
+ > ```
34
+
35
+ ## Modules
36
+
37
+ Each entry below is exactly one subpath from `package.json` `exports`. Import path → one-line
38
+ purposecanonical README.
39
+
40
+ | Import path | Purpose | Doc |
41
+ |-------------|---------|-----|
42
+ | `@spfn/core/route` | tRPC-style route DSL (`route.get(...).input(...).handler(...)`) + `defineRouter` / `registerRoutes` / `defineMiddleware`. The core. | [src/route/README.md](./src/route/README.md) |
43
+ | `@spfn/core/route/types` | Shared route types (`HttpMethod`, route/router type primitives). | [src/route/README.md](./src/route/README.md) |
44
+ | `@spfn/core/server` | HTTP server entry: `defineServerConfig()` → `startServer()`; middleware auto-wiring, infra init, graceful shutdown. | [src/server/README.md](./src/server/README.md) |
45
+ | `@spfn/core/nextjs` | Client-safe: `createApi<AppRouter>()`, `ApiError`, client types. No `next/headers`. | [src/nextjs/README.md](./src/nextjs/README.md) |
46
+ | `@spfn/core/nextjs/server` | Server-only: `createRpcProxy({ routeMap })`, `registerInterceptors`. Uses `next/headers`. | [src/nextjs/README.md](./src/nextjs/README.md) |
47
+ | `@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) |
48
+ | `@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) |
49
+ | `@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) |
50
+ | `@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) |
51
+ | `@spfn/core/middleware` | Built-in Hono middleware: `ErrorHandler`, `RequestLogger` (+ masking helper). | [src/middleware/README.md](./src/middleware/README.md) |
52
+ | `@spfn/core/errors` | Serializable HTTP/DB error classes + `ErrorRegistry` for cross-boundary deserialization. | [src/errors/README.md](./src/errors/README.md) |
53
+ | `@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) |
54
+ | `@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) |
55
+ | `@spfn/core/logger` | Zero-dependency structured singleton `logger` + child loggers, level masking. | [src/logger/README.md](./src/logger/README.md) |
56
+ | `@spfn/core/cache` | Singleton Valkey/Redis (ioredis) manager, graceful-degrading (`getCache`, `getCacheRead`). | [src/cache/README.md](./src/cache/README.md) |
57
+ | `@spfn/core/job` | pg-boss background jobs: fluent `job()` builder, cron/run-once/event-driven, `defineJobRouter`. | [src/job/README.md](./src/job/README.md) |
58
+ | `@spfn/core/event` | Decoupled pub/sub (`defineEvent`, `defineEventRouter`, `eventRouteMap`); SSE/WS variants below. | [src/event/README.md](./src/event/README.md) |
59
+ | `@spfn/core/event/sse` | Server SSE handler + token manager (SERVER ONLY). | [src/event/README.md](./src/event/README.md) |
60
+ | `@spfn/core/event/sse/client` | Browser SSE (`EventSource`) client. | [src/event/README.md](./src/event/README.md) |
61
+ | `@spfn/core/event/ws` | Server WebSocket handler (SERVER ONLY; `ws` optional dep). | [src/event/README.md](./src/event/README.md) |
62
+ | `@spfn/core/event/ws/client` | Browser WebSocket client. | [src/event/README.md](./src/event/README.md) |
63
+ | `@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) |
64
+
65
+ `db/manager`, `db/schema`, `db/transaction` have **no package subpath** of their own — they are
66
+ internal modules re-exported by `@spfn/core/db`. Import them from `@spfn/core/db`. (See Pitfalls
67
+ for the `./client` subpath.)
68
+
69
+ ## How it works
70
+
71
+ End-to-end, a SPFN backend is **defined once** on the server and consumed type-safely from the
72
+ Next.js app. Types flow purely through TypeScript inference; the only generated artifact is the
73
+ proxy's `routeMap`.
74
+
75
+ ```
76
+ route DSL ② defineRouter ③ defineServerConfig → startServer
77
+ route.get('/users/:id') defineRouter({ getUser, defineServerConfig()
78
+ .input({ params }) createUser }) .routes(appRouter).build()
79
+ .handler(c => …) export type AppRouter startServer() → Hono on :8790
80
+ │ = typeof appRouter ▲
81
+ │ TypeBox = runtime validation + compile-time types │ registerRoutes mounts routes
82
+ ▼ │
83
+ codegen (@spfn/core:route-map) ──► routeMap = { getUser: { method:'GET', path:'/users/:id' }, … }
84
+
85
+
86
+ Next.js RPC proxy ⑥ typed client
87
+ app/api/rpc/[routeName]/route.ts lib/api.ts
88
+ createRpcProxy({ routeMap }) createApi<AppRouter>() (no codegen for the client)
89
+ GET/POST /api/rpc/{routeName} api.getUser.call({ params:{ id } })
90
+ resolves real method+path from routeMap └─ fully typed input + output
91
+ forwards to backend, runs interceptors
92
+ ```
93
+
94
+ 1. **Define a route** with `route.<method>(path).input({...}).handler(c => )` from
95
+ `@spfn/core/route`. TypeBox schemas in `.input()` give both runtime validation and the
96
+ compile-time types the handler (`await c.data()`) and the client see. The handler's **return
97
+ type is inferred** — no manual response typing.
98
+ 2. **Compose routes** with `defineRouter({ … })` and export `type AppRouter = typeof appRouter`.
99
+ That type is the single source of truth for the client; no client codegen reads it.
100
+ 3. **Boot the server** with `defineServerConfig().routes(appRouter).build()` then `startServer()`
101
+ (`@spfn/core/server`). It auto-wires `ErrorHandler`/`RequestLogger`, inits DB/cache, mounts
102
+ routes via `registerRoutes`, and runs jobs/events. A request is matched by Hono → global
103
+ middleware route middleware (`.use([...])`, e.g. `Transactional()`) → input validation/type
104
+ conversion → handler → serialized response.
105
+ 4. **Generate the route-map** with codegen (`@spfn/core:route-map`, e.g. `pnpm codegen`). It emits
106
+ `routeName { method, path }` — the only thing the proxy needs to resolve the *real* backend
107
+ method/path.
108
+ 5. **Mount the RPC proxy** in Next.js: `createRpcProxy({ routeMap })` from
109
+ `@spfn/core/nextjs/server` as the `app/api/rpc/[routeName]/route.ts` catch-all. The client only
110
+ ever sends `GET` (no body) or `POST` (body/formData) to `/api/rpc/{routeName}`; the proxy looks
111
+ up `routeMap[routeName]`, substitutes `:params`, and forwards to the backend with the resolved
112
+ method. Package route maps (`authRouteMap`, `eventRouteMap`) merge into the same `routeMap`.
113
+ 6. **Call it** through `createApi<AppRouter>()` from `@spfn/core/nextjs`. The client is a `Proxy`
114
+ over `AppRouter` — `api.getUser.call({ params })` is fully typed in and out, with zero runtime
115
+ type cost. Errors come back as `ApiError`, or as the original typed error when its class is in
116
+ the client's `errorRegistry` (the core registry is always merged in).
117
+
118
+ ## Quick example
119
+
120
+ ```typescript
121
+ // server/router.ts — ① + ②
122
+ import { defineRouter, route } from '@spfn/core/route';
123
+ import { Transactional } from '@spfn/core/db';
505
124
  import { Type } from '@sinclair/typebox';
506
125
 
507
- export const getUser = route.get('/users/:id')
508
- .input({
509
- params: Type.Object({
510
- id: Type.String(),
511
- }),
512
- query: Type.Object({
513
- include: Type.Optional(Type.String()),
514
- }),
515
- })
516
- .handler(async (c) => {
517
- const { params, query } = await c.data();
518
- // ^? { params: { id: string }, query: { include?: string } }
519
-
520
- const user = await findOne(users, { id: params.id });
521
- // ^? User | null
522
-
523
- return user;
524
- // ^? Response type inferred from return value
525
- });
526
-
527
- // 3. Router Type Export
528
- // src/server/router.ts
529
126
  export const appRouter = defineRouter({
530
- getUser,
531
- // ... other routes
532
- });
533
-
534
- export type AppRouter = typeof appRouter;
535
- // ^? Router<{ getUser: RouteDef<..., ...>, ... }>
536
-
537
- // 4. Client API Call (Next.js)
538
- // app/users/[id]/page.tsx
539
- import { api } from '@spfn/core/nextjs/server';
540
-
541
- const user = await api.getUser
542
- .params({ id: '123' }) // Type-checked: must be { id: string }
543
- .query({ include: 'posts' }) // Type-checked: { include?: string }
544
- .call();
545
- // ^? User (inferred from server handler return type)
546
-
547
- // Full type inference chain:
548
- // User type → RouteDef → Router → Client API → return type
549
- ```
550
-
551
- ### Type Inference Utilities
552
-
553
- ```typescript
554
- // Extract input type from route
555
- type GetUserInput = InferRouteInput<typeof getUser>;
556
- // ^? { params: { id: string }; query: { include?: string } }
557
-
558
- // Extract output type from route
559
- type GetUserOutput = InferRouteOutput<typeof getUser>;
560
- // ^? User
561
-
562
- // Client type inference
563
- type ApiClient<TRouter> = {
564
- [K in keyof TRouter['routes']]: TRouter['routes'][K] extends RouteDef<infer TInput, infer TOutput>
565
- ? RouteClient<TInput, TOutput>
566
- : never;
567
- };
568
- ```
569
-
570
- ---
571
-
572
- ## Integration Points
573
-
574
- ### 1. Server ↔ Database: Transaction Context
575
-
576
- **Mechanism**: AsyncLocalStorage propagates transaction across async calls
577
-
578
- ```typescript
579
- // Route handler
580
- app.bind(createPostContract, [Transactional()], async (c) => {
581
- // AsyncLocalStorage stores transaction
582
- const post = await create(posts, { title: 'Hello' });
583
-
584
- // Same transaction is used automatically
585
- const tag = await create(tags, { postId: post.id, name: 'news' });
586
-
587
- // Auto-commit on success, auto-rollback on error
588
- return c.json(post);
589
- });
590
- ```
591
-
592
- **Why**: No need to pass transaction object through function calls
593
-
594
- ---
595
-
596
- ### 2. Server ↔ Client: Type Inference
597
-
598
- **Mechanism**: `typeof appRouter` captures all route types
599
-
600
- ```typescript
601
- // Server: Export router type
602
- export const appRouter = defineRouter({ getUser, createUser });
603
- export type AppRouter = typeof appRouter;
604
-
605
- // Client: Import type (not value!)
606
- import type { AppRouter } from '@/server/router';
607
- const api = createApi<AppRouter>(/* ... */);
608
-
609
- // Automatic type inference for all routes
610
- const user = await api.getUser.params({ id: '123' }).call();
611
- // ^? Full type safety without manual type definitions
612
- ```
613
-
614
- **Why**: Single source of truth (server) → client types automatically sync
615
-
616
- ---
617
-
618
- ### 3. Next.js ↔ SPFN: RPC Proxy
619
-
620
- **Mechanism**: Next.js API Route resolves routeName and forwards to SPFN server
621
-
622
- ```typescript
623
- // app/api/rpc/[routeName]/route.ts
624
- import { appRouter } from '@/server/router';
625
- import { createRpcProxy } from '@spfn/core/nextjs/server';
626
-
627
- export const { GET, POST } = createRpcProxy({ router: appRouter });
628
-
629
- // Automatically:
630
- // 1. Resolves routeName → method/path from router
631
- // 2. Forwards to http://localhost:8790/{resolved-path}
632
- // 3. Applies interceptors (auth, cookies)
633
- // 4. Returns NextResponse
634
- ```
635
-
636
- **Why**:
637
- - HttpOnly cookie support (browser → Next.js includes cookies automatically)
638
- - Security (SPFN_API_URL hidden from browser)
639
- - No CORS (same-origin requests)
640
- - No metadata codegen required
641
-
642
- ---
643
-
644
- ### 4. Packages: Registry System
645
-
646
- **Mechanism**: Packages register interceptors on import
647
-
648
- ```typescript
649
- // @spfn/auth package
650
- import { registerInterceptors } from '@spfn/core/nextjs/server';
651
-
652
- registerInterceptors('auth', [
653
- {
654
- pathPattern: '/_auth/*',
655
- response: async (ctx, next) => {
656
- // Set session cookie
657
- ctx.setCookies.push({ name: 'session', value: token });
658
- await next();
659
- },
660
- },
661
- ]);
662
-
663
- // App: Auto-discovery
664
- import '@spfn/auth/adapters/nextjs'; // Registers interceptors
665
- export { GET, POST } from '@spfn/core/nextjs/server'; // Uses registered interceptors
666
- ```
667
-
668
- **Why**: Zero-config integration for plugin packages
669
-
670
- ---
671
-
672
- ## Design Decisions
673
-
674
- ### 1. Why tRPC-Style API over REST Client?
675
-
676
- **Decision**: Method chaining with `.params().query().call()`
677
-
678
- **Reasons**:
679
- - Better DX: Fluent API guides usage
680
- - Type safety: Each method is type-checked
681
- - Flexibility: Optional parameters can be omitted
682
- - Discovery: IDE autocomplete shows available options
683
-
684
- **Example**:
685
- ```typescript
686
- // tRPC-style (SPFN)
687
- const user = await api.getUser
688
- .params({ id: '123' })
689
- .query({ include: 'posts' })
690
- .call();
691
-
692
- // vs Traditional REST client
693
- const user = await client.get('/users/123', {
694
- params: { include: 'posts' } // No type safety
695
- });
696
- ```
697
-
698
- ---
699
-
700
- ### 2. Why API Route Proxy Pattern?
701
-
702
- **Decision**: Next.js API Route forwards to SPFN server
703
-
704
- **Reasons**:
705
- - **HttpOnly Cookies**: Browser automatically sends cookies to same-origin
706
- - **Security**: SPFN API URL hidden from browser
707
- - **No CORS**: Same-origin requests (localhost:3000 → localhost:3000)
708
- - **Unified Path**: Server Components and Client Components use same API
709
-
710
- **Alternative Rejected**: Direct calls from browser to SPFN server
711
- - Would expose SPFN_API_URL to browser
712
- - CORS configuration required
713
- - Cannot use HttpOnly cookies from browser
714
-
715
- ---
716
-
717
- ### 3. Why define-route over File-Based Routing?
718
-
719
- **Decision**: Explicit route imports with `defineRouter()`
720
-
721
- **Reasons**:
722
- - **Explicit Imports**: Better tree-shaking, clear dependencies
723
- - **Type Safety**: `typeof appRouter` captures all routes
724
- - **Flexibility**: Routes can be defined anywhere
725
- - **No Magic**: No file system scanning at runtime
726
-
727
- **Alternative Rejected**: File-based routing (like Next.js)
728
- - Runtime file system scanning
729
- - Implicit route registration
730
- - Harder to trace route definitions
731
- - Less flexible for monorepo setups
732
-
733
- **Migration Path**: Deprecated contract-based and file-based routing systems
734
-
735
- ---
736
-
737
- ### 4. Why AsyncLocalStorage for Transactions?
738
-
739
- **Decision**: Transaction propagation without explicit passing
740
-
741
- **Reasons**:
742
- - **Clean API**: No need to pass `tx` object through all functions
743
- - **Automatic**: Works across async boundaries
744
- - **Safe**: Isolated per request
745
- - **Compatible**: Works with existing code
746
-
747
- **Example**:
748
- ```typescript
749
- // With AsyncLocalStorage (SPFN)
750
- async function createPostWithTags(data) {
751
- const post = await create(posts, data);
752
- const tags = await createMany(tags, data.tags.map(t => ({ postId: post.id, ...t })));
753
- // Same transaction automatically
754
- }
755
-
756
- // vs Explicit transaction passing
757
- async function createPostWithTags(tx, data) {
758
- const post = await tx.insert(posts).values(data);
759
- const tags = await tx.insert(tags).values(...);
760
- // Must pass tx everywhere
761
- }
762
- ```
763
-
764
- ---
765
-
766
- ### 5. Why Hono over Express?
767
-
768
- **Decision**: Hono as the underlying web framework
769
-
770
- **Reasons**:
771
- - **TypeScript First**: Better type inference
772
- - **Performance**: Faster than Express
773
- - **Modern**: Built for Edge/Serverless
774
- - **Lightweight**: Smaller bundle size
775
-
776
- ---
777
-
778
- ### 6. Why TypeBox over Zod?
779
-
780
- **Decision**: TypeBox for schema validation
781
-
782
- **Reasons**:
783
- - **JSON Schema**: Standard, interoperable
784
- - **Performance**: Faster validation (JIT compilation)
785
- - **OpenAPI**: Easy OpenAPI generation
786
- - **Smaller**: Smaller bundle size
787
-
788
- **Note**: Both are supported, TypeBox is the default
789
-
790
- ---
791
-
792
- ## Extension Points
793
-
794
- ### 1. Custom Middleware
795
-
796
- Add global or route-specific middleware:
797
-
798
- ```typescript
799
- // server.config.ts
800
- import { defineServerConfig } from '@spfn/core/server';
801
- import { defineMiddleware } from '@spfn/core/route';
802
-
803
- const rateLimitMiddleware = defineMiddleware('rateLimit', async (c, next) => {
804
- const ip = c.req.header('x-forwarded-for');
805
- if (await isRateLimited(ip)) {
806
- return c.json({ error: 'Rate limit exceeded' }, 429);
807
- }
808
- await next();
809
- });
810
-
811
- export default defineServerConfig()
812
- .middlewares([rateLimitMiddleware])
813
- .build();
814
-
815
- // Route can skip middleware
816
- export const publicRoute = route.get('/public')
817
- .middleware({ skip: ['rateLimit'] })
818
- .handler(async (c) => { ... });
819
- ```
820
-
821
- ---
822
-
823
- ### 2. Custom Interceptors
824
-
825
- Add request/response interceptors:
826
-
827
- ```typescript
828
- // app/api/rpc/[routeName]/route.ts
829
- import { appRouter } from '@/server/router';
830
- import { createRpcProxy } from '@spfn/core/nextjs/server';
127
+ getUser: route.get('/users/:id')
128
+ .input({ params: Type.Object({ id: Type.String() }) })
129
+ .handler(async (c) =>
130
+ {
131
+ const { params } = await c.data(); // params.id: string
132
+ return { id: params.id, name: 'John' }; // return type inferred
133
+ }),
831
134
 
832
- export const { GET, POST } = createRpcProxy({
833
- router: appRouter,
834
- interceptors: [
135
+ createUser: route.post('/users')
136
+ .input({ body: Type.Object({ name: Type.String() }) })
137
+ .use([Transactional()]) // auto commit/rollback
138
+ .handler(async (c) =>
835
139
  {
836
- pathPattern: '/admin/*',
837
- request: async (ctx, next) => {
838
- // Check admin role
839
- const isAdmin = await checkAdminRole(ctx.cookies);
840
- if (!isAdmin) {
841
- throw new Error('Unauthorized');
842
- }
843
- await next();
844
- },
845
- },
846
- ],
140
+ const { body } = await c.data();
141
+ return { id: '2', name: body.name };
142
+ }),
847
143
  });
848
- ```
849
-
850
- ---
851
-
852
- ### 3. Plugin System
853
-
854
- Create SPFN packages with auto-discovery:
855
-
856
- ```typescript
857
- // @yourcompany/spfn-analytics package
858
- import { registerInterceptors } from '@spfn/core/nextjs/server';
859
-
860
- registerInterceptors('analytics', [
861
- {
862
- pathPattern: '*',
863
- response: async (ctx, next) => {
864
- await analytics.track({
865
- path: ctx.path,
866
- status: ctx.response.status,
867
- duration: Date.now() - ctx.metadata.startTime,
868
- });
869
- await next();
870
- },
871
- },
872
- ]);
873
-
874
- // App: Auto-discovery
875
- import '@yourcompany/spfn-analytics/adapters/nextjs';
876
- export { GET, POST } from '@spfn/core/nextjs/server';
877
- ```
878
-
879
- ---
880
-
881
- ### 4. Custom Database Helpers
882
-
883
- Extend database layer with domain-specific functions:
884
-
885
- ```typescript
886
- // src/server/repositories/users.repository.ts
887
- import { findOne, create } from '@spfn/core/db';
888
- import { users } from '../entities/users';
889
-
890
- export async function findUserByEmail(email: string) {
891
- return findOne(users, { email });
892
- }
893
-
894
- export async function createUserWithProfile(data) {
895
- const user = await create(users, data.user);
896
- const profile = await create(profiles, {
897
- ...data.profile,
898
- userId: user.id,
899
- });
900
- return { user, profile };
901
- }
902
- ```
903
-
904
- ---
905
-
906
- ## Migration Guides
907
-
908
- ### From Contract-Based to define-route
909
-
910
- **Before** (Deprecated):
911
- ```typescript
912
- // contract.ts
913
- export const getUserContract = {
914
- method: 'GET' as const,
915
- path: '/users/:id',
916
- params: Type.Object({ id: Type.String() }),
917
- response: Type.Object({ id: Type.String(), name: Type.String() }),
918
- } as const satisfies RouteContract;
919
-
920
- // route.ts
921
- import { createApp } from '@spfn/core/route';
922
- const app = createApp();
923
- app.bind(getUserContract, async (c) => { ... });
924
- export default app;
925
- ```
926
144
 
927
- **After** (Current):
928
- ```typescript
929
- // routes/users.ts
930
- import { route } from '@spfn/core/route';
931
-
932
- export const getUser = route.get('/users/:id')
933
- .input({
934
- params: Type.Object({ id: Type.String() }),
935
- })
936
- .handler(async (c) => {
937
- const { params } = await c.data();
938
- return { id: params.id, name: 'John' };
939
- });
940
-
941
- // router.ts
942
- import { defineRouter } from '@spfn/core/route';
943
- export const appRouter = defineRouter({ getUser });
944
145
  export type AppRouter = typeof appRouter;
945
146
  ```
946
147
 
947
- **Benefits**:
948
- - Better type inference
949
- - Explicit imports (tree-shaking)
950
- - No separate contract files
951
-
952
- ---
953
-
954
- ### From File-Based to define-route
955
-
956
- **Before** (Deprecated):
957
148
  ```typescript
958
- // routes/users/[id].ts
959
- export default async function handler(c) { ... }
149
+ // server/index.ts — ③
150
+ import { defineServerConfig, startServer } from '@spfn/core/server';
151
+ import { appRouter } from './router';
960
152
 
961
- // Automatic file system scanning
153
+ export default defineServerConfig().port(8790).routes(appRouter).build();
154
+ await startServer(); // Hono on :8790
962
155
  ```
963
156
 
964
- **After** (Current):
965
157
  ```typescript
966
- // routes/users.ts
967
- export const getUser = route.get('/users/:id')...
968
-
969
- // router.ts
970
- export const appRouter = defineRouter({ getUser });
971
-
972
- // server.config.ts
973
- export default defineServerConfig()
974
- .routes(appRouter)
975
- .build();
976
- ```
977
-
978
- **Benefits**:
979
- - Explicit route registration
980
- - Better IDE support
981
- - No runtime file system scanning
982
-
983
- ---
984
-
985
- ### From ContractClient to tRPC-Style API
986
-
987
- **Before** (Old Client):
988
- ```typescript
989
- import { ContractClient } from '@spfn/core/client';
990
- import { getUserContract } from '@/contracts/users';
991
-
992
- const client = new ContractClient({ baseUrl: 'http://localhost:8790' });
993
- const user = await client.call(getUserContract, {
994
- params: { id: '123' },
995
- });
996
- ```
997
-
998
- **After** (Current):
999
- ```typescript
1000
- import { api } from '@spfn/core/nextjs/server';
1001
-
1002
- const user = await api.getUser
1003
- .params({ id: '123' })
1004
- .call();
1005
- ```
1006
-
1007
- **Benefits**:
1008
- - tRPC-style DX
1009
- - Method chaining
1010
- - Better type inference
1011
- - Automatic cookie handling
1012
-
1013
- ---
1014
-
1015
- ## Module Exports
1016
-
1017
- ### Main Server Exports
1018
-
1019
- ```typescript
1020
- import {
1021
- startServer,
1022
- createServer,
1023
- defineServerConfig,
1024
- } from '@spfn/core';
1025
- ```
1026
-
1027
- ### Route System
1028
-
1029
- ```typescript
1030
- import {
1031
- route,
1032
- defineRouter,
1033
- } from '@spfn/core/route';
1034
-
1035
- import type {
1036
- RouteDef,
1037
- Router,
1038
- RouteInput,
1039
- } from '@spfn/core/route';
1040
- ```
1041
-
1042
- ### Database System
1043
-
1044
- ```typescript
1045
- import {
1046
- getDatabase,
1047
- findOne,
1048
- findMany,
1049
- create,
1050
- createMany,
1051
- updateOne,
1052
- updateMany,
1053
- deleteOne,
1054
- deleteMany,
1055
- count,
1056
- } from '@spfn/core/db';
1057
-
1058
- import {
1059
- Transactional,
1060
- getTransaction,
1061
- runWithTransaction,
1062
- } from '@spfn/core/db';
1063
- ```
1064
-
1065
- ### Client System
1066
-
1067
- ```typescript
1068
- // Client-safe exports (works in Client Components)
1069
- import {
1070
- createApi,
1071
- ApiError,
1072
- } from '@spfn/core/nextjs';
1073
-
1074
- // Server-only exports (API Routes)
1075
- import {
1076
- createRpcProxy,
1077
- registerInterceptors,
1078
- } from '@spfn/core/nextjs/server';
1079
- ```
1080
-
1081
- ### Error System
1082
-
1083
- ```typescript
1084
- import {
1085
- ApiError,
1086
- ValidationError,
1087
- NotFoundError,
1088
- ConflictError,
1089
- UnauthorizedError,
1090
- } from '@spfn/core/errors';
1091
- ```
1092
-
1093
- ### Logger System
1094
-
1095
- ```typescript
1096
- import { logger } from '@spfn/core';
1097
- ```
1098
-
1099
- ### Cache System
1100
-
1101
- ```typescript
1102
- import {
1103
- initCache,
1104
- getCache,
1105
- getCacheRead,
1106
- isCacheDisabled,
1107
- closeCache,
1108
- } from '@spfn/core/cache';
1109
- ```
1110
-
1111
- ### Environment System
1112
-
1113
- ```typescript
1114
- import {
1115
- defineEnvSchema,
1116
- createEnvRegistry,
1117
- envString,
1118
- envNumber,
1119
- envBoolean,
1120
- envEnum,
1121
- } from '@spfn/core/env';
1122
- ```
1123
-
1124
- ### Configuration System
1125
-
1126
- ```typescript
1127
- import { env, envSchema } from '@spfn/core/config';
1128
- ```
1129
-
1130
- ### Job System
1131
-
1132
- ```typescript
1133
- import {
1134
- job,
1135
- defineJobRouter,
1136
- registerJobs,
1137
- initBoss,
1138
- getBoss,
1139
- stopBoss,
1140
- } from '@spfn/core/job';
1141
-
1142
- import type {
1143
- JobDef,
1144
- JobRouter,
1145
- JobOptions,
1146
- BossOptions,
1147
- InferJobInput,
1148
- } from '@spfn/core/job';
1149
- ```
1150
-
1151
- ### Event System
1152
-
1153
- ```typescript
1154
- import { defineEvent } from '@spfn/core/event';
1155
-
1156
- import type {
1157
- EventDef,
1158
- EventHandler,
1159
- InferEventPayload,
1160
- PubSubCache,
1161
- } from '@spfn/core/event';
1162
- ```
1163
-
1164
- ---
1165
-
1166
- ## Quick Reference
1167
-
1168
- ### Environment Variables
1169
-
1170
- ```bash
1171
- # Database (required)
1172
- DATABASE_URL=postgresql://user:pass@localhost:5432/db
1173
-
1174
- # Database Read Replica (optional)
1175
- DATABASE_READ_URL=postgresql://user:pass@replica:5432/db
1176
-
1177
- # Cache - Valkey/Redis (optional)
1178
- CACHE_URL=redis://localhost:6379
1179
- CACHE_WRITE_URL=redis://master:6379
1180
- CACHE_READ_URL=redis://replica:6379
1181
-
1182
- # Next.js App URL (for Server Components calling API Routes)
1183
- SPFN_APP_URL=http://localhost:3000
1184
-
1185
- # SPFN API Server URL (for API Route Proxy)
1186
- SPFN_API_URL=http://localhost:8790
1187
-
1188
- # Server
1189
- PORT=8790
1190
- HOST=localhost
1191
- NODE_ENV=development
1192
-
1193
- # Server Timeouts (optional, milliseconds)
1194
- SERVER_TIMEOUT=120000
1195
- SERVER_KEEPALIVE_TIMEOUT=65000
1196
- SERVER_HEADERS_TIMEOUT=60000
1197
- SHUTDOWN_TIMEOUT=30000
1198
-
1199
- # Logger (optional)
1200
- LOGGER_ADAPTER=pino
1201
- LOGGER_FILE_ENABLED=true
1202
- LOG_DIR=/var/log/myapp
1203
- ```
1204
-
1205
- ---
1206
-
1207
- ### Basic Setup
1208
-
1209
- **1. Install**
1210
- ```bash
1211
- npm install @spfn/core hono drizzle-orm postgres @sinclair/typebox
1212
- ```
1213
-
1214
- **2. Define Routes**
1215
- ```typescript
1216
- // src/server/routes/users.ts
1217
- export const getUser = route.get('/users/:id')
1218
- .input({ params: Type.Object({ id: Type.String() }) })
1219
- .handler(async (c) => {
1220
- const { params } = await c.data();
1221
- return { id: params.id, name: 'John' };
1222
- });
1223
- ```
1224
-
1225
- **3. Create Router**
1226
- ```typescript
1227
- // src/server/router.ts
1228
- export const appRouter = defineRouter({ getUser });
1229
- export type AppRouter = typeof appRouter;
1230
- ```
1231
-
1232
- **4. Configure Server**
1233
- ```typescript
1234
- // src/server/server.config.ts
1235
- export default defineServerConfig()
1236
- .port(8790)
1237
- .routes(appRouter)
1238
- .build();
1239
- ```
1240
-
1241
- **5. Create RPC Proxy (Next.js)**
1242
- ```typescript
1243
- // app/api/rpc/[routeName]/route.ts
1244
- import { appRouter } from '@/server/router';
158
+ // app/api/rpc/[routeName]/route.ts — ⑤ (server-only)
1245
159
  import { createRpcProxy } from '@spfn/core/nextjs/server';
160
+ import { routeMap } from '@/generated/route-map'; // ④ codegen output
1246
161
 
1247
- export const { GET, POST } = createRpcProxy({ router: appRouter });
162
+ export const { GET, POST } = createRpcProxy({ routeMap });
1248
163
  ```
1249
164
 
1250
- **6. Use Client**
1251
165
  ```typescript
1252
- // app/users/[id]/page.tsx
166
+ // lib/api.ts — ⑥ (client-safe; no codegen)
1253
167
  import { createApi } from '@spfn/core/nextjs';
1254
168
  import type { AppRouter } from '@/server/router';
1255
169
 
1256
- const api = createApi<AppRouter>();
1257
- const user = await api.getUser.call({ params: { id: '123' } });
1258
- ```
1259
-
1260
- ---
1261
-
1262
- ## Documentation
1263
-
1264
- ### Technical Architecture
1265
- - [Route System](./src/route/README.md) - define-route system, type inference
1266
- - [Server System](./src/server/README.md) - Configuration, middleware pipeline, lifecycle
1267
- - [Database System](./src/db/README.md) - Helper functions, transactions, schema helpers
1268
- - [Client System](./src/nextjs/README.md) - tRPC-style API, TypedProxy, interceptors
1269
-
1270
- ### Guides
1271
- - [Transaction Management](./src/db/docs/transactions.md)
1272
- - [Error Handling](./src/errors/README.md)
1273
- - [Logger](./src/logger/README.md)
1274
- - [Cache](./src/cache/README.md)
1275
- - [Middleware](./src/middleware/README.md)
1276
- - [Code Generation](./src/codegen/README.md)
1277
-
1278
- ---
1279
-
1280
- ## Requirements
1281
-
1282
- - Node.js >= 18
1283
- - Next.js 15+ with App Router (when using client integration)
1284
- - PostgreSQL
1285
- - Valkey/Redis (optional)
1286
- - TypeScript >= 5.0
1287
-
1288
- ---
1289
-
1290
- ## Testing
1291
-
1292
- ```bash
1293
- npm test # Run all tests
1294
- npm test -- route # Run route tests only
1295
- npm test -- --coverage # With coverage
1296
- ```
1297
-
1298
- **Test Coverage**: 650+ tests across all modules
1299
-
1300
- ---
1301
-
1302
- ## License
1303
-
1304
- MIT
1305
-
1306
- ---
1307
-
1308
- Part of the [Superfunction Framework](https://github.com/spfn/spfn)
170
+ export const api = createApi<AppRouter>();
171
+
172
+ // anywhere (RSC / client / server action):
173
+ const user = await api.getUser.call({ params: { id: '123' } }); // typed { id, name }
174
+ const made = await api.createUser.call({ body: { name: 'A' } });
175
+ ```
176
+
177
+ ## Pitfalls
178
+
179
+ - **No root barrel.** `import … from '@spfn/core'` does not resolve. Import from a subpath
180
+ (`@spfn/core/route`, `@spfn/core/db`, …). The list above *is* the complete public surface.
181
+ - **Use `@spfn/core/nextjs` for the client, not `@spfn/core/client`.** `package.json` still lists a
182
+ `./client` export, but the build does not emit it (no `src/client`; the tsup `client` entry is
183
+ disabled). `createApi` / `ApiError` and all client types ship from **`@spfn/core/nextjs`**. Treat
184
+ `@spfn/core/client` as non-functional.
185
+ - **Client vs. server boundary is load-bearing.** `@spfn/core/nextjs/server` pulls in
186
+ `next/headers` + `next/server`; never import it from a Client Component. `@spfn/core/env/loader`,
187
+ `@spfn/core/event/sse`, `@spfn/core/event/ws` are server-only (`node:fs` / Hono / `node:crypto`).
188
+ Client code uses the `*/client` and isomorphic entry points only.
189
+ - **`db/manager`, `db/schema`, `db/transaction` are not package subpaths.** Importing
190
+ `@spfn/core/db/transaction` fails to resolve — import those symbols from `@spfn/core/db`.
191
+ - **The client needs no codegen; the proxy does.** `createApi<AppRouter>()` is metadata-free and
192
+ driven by the `AppRouter` *type*. The generated `routeMap` is consumed by `createRpcProxy`. A
193
+ `routeName` missing from the merged `routeMap` is a **proxy 404**, not a backend 404 — re-run
194
+ codegen after adding routes.
195
+ - **The proxy decides the real HTTP method.** The client only sends GET/POST to `/api/rpc/...`; a
196
+ PUT/PATCH/DELETE route still works because the backend method comes from `routeMap[routeName]`.
197
+ - **Cache/event/job degrade or depend on optional deps.** `@spfn/core/cache` runs disabled (getters
198
+ return `undefined`) without `CACHE_*` config or `ioredis`; WebSocket events need the optional `ws`
199
+ dep. Don't assume they throw.
200
+
201
+ ## Related packages
202
+
203
+ Other SPFN packages build on `@spfn/core`:
204
+
205
+ - [`@spfn/auth`](../auth/README.md) — auth/session; exports `authRouteMap` and auto-registers proxy
206
+ interceptors (merge its route map into `createRpcProxy`).
207
+ - `@spfn/cms`, `@spfn/workflow`, `@spfn/notification`, `@spfn/monitor`, `@spfn/cli`
208
+ see each package's README under `packages/`.