weifuwu 0.16.3 → 0.16.5

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.
package/README.md CHANGED
@@ -7,35 +7,33 @@ description: Web-standard HTTP framework for Node.js — (req, ctx) => Response
7
7
 
8
8
  **Web-standard HTTP framework for Node.js.** `(req, ctx) => Response` — no framework-specific objects, just the Web API your browser already speaks.
9
9
 
10
- ### Design
11
-
12
- weifuwu doesn't invent its own request/response abstraction. `Request` and `Response` are the same objects you use in `fetch()` — what you learn in the browser applies directly on the server. `ctx` is the only framework object, and it only carries what the router parsed for you (`params`, `query`).
13
-
14
- Everything follows the same `(req, ctx) => Response` contract. The Router handles HTTP routing and WebSocket. All other features — auth, validation, database, GraphQL, AI — are standalone modules you import and mount with `app.use()`.
15
-
16
- ## Features
17
-
18
- - **Web Standard** — `Request` / `Response` / `ReadableStream`, zero abstractions
19
- - **Zero build** — native TypeScript in Node.js v24+, zero deps (core)
20
- - **Trie router** — static > param > wildcard, sub-router mounting, WebSocket
21
- - **Middleware**global/path-scoped/route-level onion model with short-circuit
22
- - **Modules** — auth, validation, upload, compression, rate-limit, cookies, static files, CORS, logging
23
- - **React SSR** — `tsx()` — pages, layouts, loaders, route handlers, Tailwind CSS, HMR
24
- - **PostgreSQL** — schema builder with type-safe DDL, CRUD (`read`/`readMany`, `insertMany`, `update`/`updateMany`, `delete`/`deleteMany`), WHERE helpers (`eq`, `gte`, `contains`, `and`, `or`), transactions, vector search
25
- - **Auth** — password + JWT + OAuth2 Server (authorization code / PKCE / client_credentials)
26
- - **Real-time** — WebSocket, messaging channels with agent routing
27
- - **AI** — streaming endpoint, DAG workflow tool, AI agents with RAG and tool-use — re-exports `streamText`, `tool`, `openai` and more from AI SDK
28
- - **Data** — Redis client, job queue with cron scheduling
29
- - **Multi-tenant BaaS** — dynamic tables, auto REST + GraphQL, row-level isolation
30
- - **Deploy** — self-hosted PaaS: multi-app proxy, zero-downtime updates, auto SSL
31
- - **Security** — `helmet()` security headers, request ID tracing, rate limiting, CORS, auth
32
- - **SEO** — `robots.txt`, `sitemap.xml`, `X-Robots-Tag` middleware, `seoTags()` for meta / OG / Twitter Card
33
- - **i18n** — locale detection, JSON translations, `ctx.t()`
34
- - **Email** — SMTP or custom transport
35
- - **Health check** — configurable `/health` endpoint
36
- - **Environment** — `loadEnv()` — `.env` file loader into `process.env`
37
- - **iii** — optional module bringing Worker/Function/Trigger service paradigm, `registerWorker()` WebSocket SDK, and built-in `stream::*` functions
38
- - **Test utilities** — `createTestServer()` — one-line test server setup
10
+ - [Quick start](#quick-start)
11
+ - [serve() — HTTP server](#serve--http-server)
12
+ - [Router](#router)
13
+ - [Middleware](#middleware)
14
+ - [React SSR (tsx)](#react-ssr-tsx)
15
+ - [PostgreSQL](#postgresql)
16
+ - [Auth](#auth)
17
+ - [Security](#security)
18
+ - [WebSocket & Real-time](#websocket--real-time)
19
+ - [AI](#ai)
20
+ - [Data Layer](#data-layer)
21
+ - [iiiWorker / Function / Trigger](#iii--worker--function--trigger)
22
+ - [Multi-tenant BaaS](#multi-tenant-baas)
23
+ - [Messager](#messager)
24
+ - [LogDB](#logdb)
25
+ - [SEO](#seo)
26
+ - [Opencode](#opencode)
27
+ - [Deploy](#deploy)
28
+ - [Health check](#health-check)
29
+ - [Internationalization](#internationalization)
30
+ - [Email](#email)
31
+ - [Server-Sent Events](#server-sent-events)
32
+ - [Utility functions](#utility-functions)
33
+ - [Testing](#testing)
34
+ - [License](#license)
35
+
36
+ ---
39
37
 
40
38
  ## Quick start
41
39
 
@@ -46,207 +44,69 @@ import { serve } from 'weifuwu'
46
44
  serve((req, ctx) => new Response('Hello, World!'), { port: 3000 })
47
45
  ```
48
46
 
49
- ### Full app
50
-
51
- ```ts
52
- import { serve, Router, postgres, user, aiStream, graphql, openai } from 'weifuwu'
53
-
54
- const app = new Router()
55
- const pg = postgres()
56
-
57
- // Auth
58
- const auth = user({ pg, jwtSecret: process.env.JWT_SECRET! })
59
- await auth.migrate()
60
- app.use('/auth', auth.router())
61
-
62
- // AI streaming
63
- const chat = await aiStream(async (req) => ({
64
- model: openai('gpt-4o'),
65
- messages: (await req.json()).messages,
66
- }))
67
- app.use('/chat', chat.router())
68
-
69
- // GraphQL
70
- const gql = graphql(() => ({
71
- schema: `type Query { hello: String }`,
72
- resolvers: { Query: { hello: () => 'world' } },
73
- }))
74
- app.use('/graphql', gql.router())
75
-
76
- // Static files
77
- app.get('/static/*', serveStatic('./public'))
47
+ ### weifuwu init
78
48
 
79
- serve(app.handler(), { port: 3000 })
80
- ```
49
+ Generate a full project with React SSR, WebSocket, Tailwind CSS, and graceful shutdown:
81
50
 
82
- ```
83
- node app.ts
51
+ ```bash
52
+ npx weifuwu init my-app
53
+ cd my-app && npm install && npm run dev
84
54
  ```
85
55
 
86
- ## Infrastructure
87
-
88
- | Module | Import | What it gives you |
89
- |--------|--------|-------------------|
90
- | PostgreSQL | `postgres(options?)` | Connection pool + schema builder + CRUD (`read`/`readMany`, `insertMany`, `update`/`updateMany`, `delete`/`deleteMany`) + where helpers (`eq`, `gte`, `contains`, `and`, `or`) + transactions |
91
- | Redis | `redis(options?)` | ioredis client injected as `ctx.redis` |
92
- | Queue | `queue(options?)` | Redis-backed job queue with cron scheduling |
93
- | Deploy | `deploy(config)` | Self-hosted PaaS: multi-app proxy, zero-downtime updates, auto SSL |
94
-
95
- ## Mountable modules
96
-
97
- All use the same pattern — `const m = module(options)` → `app.use('/path', m.router())`:
98
-
99
- | Module | Purpose | Also provides |
100
- |--------|---------|---------------|
101
- | `user(options)` | Auth (password + JWT + OAuth2) | `migrate()`, `middleware()`, `register()`, `login()`, `verify()`, `close()` |
102
- | `tenant(options)` | Multi-tenant BaaS | `migrate()`, `middleware()`, `graphql()`, `close()` |
103
- | `agent(options)` | AI agents | `migrate()`, `run()`, `addKnowledge()`, `close()` |
104
- | `opencode(options)` | Programming assistant | `migrate()`, `wsHandler()`, `close()` |
105
- | `messager(options)` | Real-time messaging | `migrate()`, `wsHandler()`, `send()`, `close()` |
106
- | `aiStream(handler)` | AI streaming endpoint | — |
107
- | `graphql(handler)` | GraphQL endpoint | — |
108
- | `logdb(options)` | Structured event logging | `log()`, `migrate()`, `clean()`, `close()` |
109
- | `health(options?)` | Health check | — |
110
- | `seo(options?)` | `robots.txt`, `sitemap.xml`, indexing control | `seoMiddleware()`, `seoTags()` |
111
- | `iii(options?)` | Worker/Function/Trigger service paradigm | `migrate()`, `trigger()`, `addWorker()`, `listWorkers()`, `listFunctions()`, `listTriggers()`, `shutdown()` |
112
- | `registerWorker(url)` | Pure WebSocket SDK (browser/Node) | `registerFunction()`, `registerTrigger()`, `trigger()`, `shutdown()` |
113
-
114
- ## Middleware (all `(req, ctx, next) => Response`)
115
-
116
- | Middleware | Description |
117
- |-----------|-------------|
118
- | `auth(options)` | Bearer token / custom header / verify / proxy |
119
- | `cors(options?)` | CORS with preflight, origin whitelist, credentials |
120
- | `logger(options?)` | Request logging with duration |
121
- | `rateLimit(options?)` | In-memory rate limiting with headers |
122
- | `compress(options?)` | Brotli / Gzip / Deflate compression |
123
- | `validate(schemas)` | Zod validation (body, query, params) |
124
- | `upload(options?)` | Multipart file upload |
125
- | `i18n(options)` | Internationalization — `ctx.t()`, locale detection |
126
- | `seoMiddleware(options?)` | `X-Robots-Tag` header — string or path-based function |
127
- | `helmet(options?)` | Security headers — CSP, HSTS, X-Frame-Options, etc. |
128
- | `requestId(options?)` | `X-Request-ID` header + `ctx.requestId` |
129
-
130
- ## Utility functions
131
-
132
- | Function | Description |
133
- |----------|-------------|
134
- | `serveStatic(root, options?)` | Static file serving |
135
- | `loadEnv(path?)` | Load `.env` file into `process.env` — no override, comments, quotes |
136
- | `getCookies(req)` / `setCookie(res, ...)` / `deleteCookie(res, ...)` | Cookie helpers |
137
- | `mailer(options)` | Email sender (SMTP or custom) |
138
- | `createTestServer(handler)` | Start test server → `{ server, url }` |
139
- | `seoTags(config)` | Generate `<title>`, `<meta>`, Open Graph, Twitter Card, canonical tags |
140
- | `createSSEStream(iterable, opts?)` | SSE response from `AsyncIterable` |
141
- | `formatSSE(event, data)` | Format SSE event string |
142
- | `formatSSEData(data)` | Format SSE data string |
143
- | `runWorkflow(options)` | DAG execution engine as AI SDK `Tool` |
144
- | `pgTable(name, columns)` | Type-safe table schema builder |
145
- | `pg.table(name, columns)` | Pre-bound table (no `sql` param needed) |
146
- | `serial()`, `uuid()`, `text()`, ... | Column type builders |
147
- | `eq()`, `gte()`, `contains()`, `and()` ... | WHERE clause helpers — same API as Drizzle |
148
- | `PgModule` | Base class for DB-backed modules |
149
- | `streamText()` / `generateText()` / `streamObject()` / `generateObject()` | AI SDK — text/structured generation |
150
- | `tool()` | AI SDK — tool definition |
151
- | `embed()` / `embedMany()` | AI SDK — text embeddings |
152
- | `smoothStream()` | AI SDK — smooth streaming middleware |
153
- | `openai` / `createOpenAI()` | OpenAI provider for AI SDK |
154
- | `createHub(options?)` | WebSocket channel hub — `join()`, `leave()`, `broadcast()` with optional Redis pub/sub |
56
+ Creates `app.ts` with `tsx()`, `router.ws()`, `/api/ping` API route, and `ui/pages/layout.tsx` + `page.tsx`.
155
57
 
156
58
  ---
157
59
 
158
- # iiiWorker / Function / Trigger
159
-
160
- Optional module that organizes service logic as **Worker + Function + Trigger**, plus a pure WebSocket SDK for connecting remote workers. Built-in `stream::*` functions for hierarchical real-time data.
60
+ ## serve()HTTP server
161
61
 
162
62
  ```ts
163
- import { serve, Router, iii, createWorker, registerWorker } from 'weifuwu'
164
-
165
- // Engine
166
- const engine = iii({ pg, redis })
167
- const app = new Router()
168
- app.use('/iii', engine.router())
169
- serve(app.handler(), { port: 3000, websocket: app.websocketHandler() })
170
-
171
- // Local worker
172
- const w = createWorker('orders')
173
- w.registerFunction('orders::create', async (payload) => {
174
- return db.query('INSERT INTO orders ...', [payload.items])
175
- })
176
- w.registerTrigger({
177
- type: 'http', function_id: 'orders::create',
178
- config: { method: 'POST', path: '/orders' },
179
- })
180
- engine.addWorker(w)
181
-
182
- // Invoke via Engine
183
- await engine.trigger({ function_id: 'orders::create', payload: { items: ['apple'] } })
63
+ import { serve } from 'weifuwu'
64
+ import type { Server } from 'weifuwu'
184
65
 
185
- // Remote worker (browser or another process)
186
- const rw = registerWorker('ws://host:3000/iii/worker')
187
- rw.registerFunction('ui::notify', (p) => new Notification(p.title))
66
+ const server = serve(handler, { port: 3000 })
67
+ await server.ready
68
+ console.log(`Listening on http://localhost:${server.port}`)
188
69
  ```
189
70
 
190
- ## Built-in functions
191
-
192
- | Function | Description |
193
- |----------|-------------|
194
- | `stream::set(stream_name, group_id, item_id, data)` | Write + persist + notify subscribers |
195
- | `stream::get(stream_name, group_id, item_id)` | Read single item |
196
- | `stream::delete(stream_name, group_id, item_id)` | Delete + notify |
197
- | `stream::list(stream_name, group_id)` | List all items in a group |
198
- | `stream::list_groups(stream_name)` | List all groups in a stream |
199
- | `stream::list_all()` | List all streams with metadata |
200
- | `stream::send(stream_name, group_id, type, data, id?)` | Push event without persisting |
201
- | `stream::update(stream_name, group_id, item_id, ops)` | Atomic operations (set/merge/increment/decrement/append/remove) |
202
-
203
- ## Storage backends
71
+ ### Options
204
72
 
205
- | Config | Persistence | Cross-process broadcast |
206
- |--------|-------------|------------------------|
207
- | `iii({})` | In-memory Map | |
208
- | `iii({ pg })` | PG table `_iii_stream` | |
209
- | `iii({ redis })` | Redis Hash | Redis pub/sub |
210
- | `iii({ pg, redis })` | PG table | Redis pub/sub |
73
+ | Option | Type | Default | Description |
74
+ |--------|------|---------|-------------|
75
+ | `port` | `number` | `0` (random) | Listen port |
76
+ | `hostname` | `string` | `'0.0.0.0'` | Listen address |
77
+ | `signal` | `AbortSignal` | | Shutdown on abort |
78
+ | `websocket` | `WsUpgradeHandler` | | WebSocket upgrade handler |
79
+ | `maxBodySize` | `number` | — | Max request body bytes |
80
+ | `shutdown` | `boolean` | `true` | Auto-register SIGTERM/SIGINT |
211
81
 
212
- ## Trigger actions
82
+ Graceful shutdown is **enabled by default** — `serve()` registers `SIGTERM` and `SIGINT` handlers that call `server.close()`. Set `shutdown: false` to disable.
213
83
 
214
- | Action | Behavior |
215
- |--------|----------|
216
- | `'sync'` (default) | Wait for result |
217
- | `'void'` | Fire-and-forget, no result |
84
+ ### Server
218
85
 
219
86
  ```ts
220
- // Sync
221
- const result = await engine.trigger({ function_id: 'math::add', payload: { a: 2, b: 3 } })
222
- // → { c: 5 }
223
-
224
- // Void
225
- await engine.trigger({ function_id: 'notifications::send', payload: {...}, action: 'void' })
87
+ interface Server {
88
+ stop: () => void
89
+ readonly port: number
90
+ readonly hostname: string
91
+ ready: Promise<void>
92
+ }
226
93
  ```
227
94
 
228
- ## REST API (mounted at `/iii`)
95
+ ### createTestServer
229
96
 
230
- | Path | Description |
231
- |------|-------------|
232
- | `GET /iii/workers` | List connected workers |
233
- | `GET /iii/functions` | List registered functions |
234
- | `GET /iii/triggers` | List registered triggers |
235
- | `POST /iii/trigger/:fnId` | Invoke a function |
236
- | `WS /iii/worker` | Remote worker connection |
97
+ ```ts
98
+ const { server, url } = await createTestServer(handler)
99
+ // url = 'http://localhost:PORT'
100
+ ```
237
101
 
238
102
  ---
239
103
 
240
- # Router
104
+ ## Router
241
105
 
242
106
  ```ts
243
- import { serve, Router } from 'weifuwu'
107
+ import { Router } from 'weifuwu'
244
108
 
245
109
  const app = new Router()
246
- .use((req, ctx, next) => {
247
- console.log(`${req.method} ${new URL(req.url).pathname}`)
248
- return next(req, ctx)
249
- })
250
110
  .get('/hello/:name', (req, ctx) =>
251
111
  Response.json({ message: `Hello, ${ctx.params.name}!` }),
252
112
  )
@@ -254,214 +114,201 @@ const app = new Router()
254
114
  const body = await req.json()
255
115
  return Response.json(body, { status: 201 })
256
116
  })
257
-
258
- serve(app.handler(), { port: 3000 })
259
117
  ```
260
118
 
261
- ## WebSocket
119
+ ### Route patterns
262
120
 
263
- ```json
264
- { "type": "message", "channel_id": 1, "content": "Hi" }
265
- { "type": "typing", "channel_id": 1, "is_typing": true }
266
- { "type": "read", "channel_id": 1, "last_message_id": 42 }
267
- ```
121
+ | Pattern | Example | Match |
122
+ |---------|---------|-------|
123
+ | Static | `/about` | Exact path |
124
+ | Param | `/users/:id` | `/users/42` → `ctx.params.id = '42'` |
125
+ | Wildcard | `/static/*` | `/static/js/app.js` |
126
+
127
+ Query params are auto-parsed into `ctx.query`.
268
128
 
269
- ## Error handling
129
+ ### Sub-router mounting
270
130
 
271
131
  ```ts
272
- const app = new Router()
273
- .onError((err, req, ctx) =>
274
- Response.json({ error: err.message }, { status: 500 }),
275
- )
276
- .get('/crash', () => { throw new Error('boom') })
132
+ const admin = new Router()
133
+ admin.use(auth({ token: 'secret' }))
134
+ admin.get('/dashboard', handler)
135
+
136
+ app.use('/admin', admin)
137
+ // Mounts admin routes at /admin/dashboard, etc.
277
138
  ```
278
139
 
279
- ## Graceful shutdown
140
+ ### WebSocket
280
141
 
281
142
  ```ts
282
- import { serve } from 'weifuwu'
283
- import type { Server } from 'weifuwu'
284
-
285
- const ac = new AbortController()
286
- let server: Server
287
-
288
- process.on('SIGTERM', () => {
289
- ac.abort()
290
- server.stop()
143
+ app.ws('/echo', {
144
+ open(ws, ctx) { ws.send('connected') },
145
+ message(ws, ctx, data) { ws.send(`echo: ${data}`) },
146
+ close(ws, ctx) { /* cleanup */ },
147
+ error(ws, ctx, err) { /* log */ },
291
148
  })
292
149
 
293
- server = serve((req, ctx) => new Response('Hello'), {
150
+ serve(app.handler(), {
294
151
  port: 3000,
295
- signal: ac.signal,
152
+ websocket: app.websocketHandler(),
296
153
  })
297
- await server.ready
298
154
  ```
299
155
 
300
- ### Using with WebSocket
156
+ ### Error handling
301
157
 
302
158
  ```ts
303
- const app = new Router().ws('/chat', { })
304
- const server = serve(app.handler(), {
305
- port: 3000,
306
- signal: ac.signal,
307
- websocket: app.websocketHandler(),
308
- })
159
+ app.onError((err, req, ctx) =>
160
+ Response.json({ error: err.message }, { status: 500 }),
161
+ )
309
162
  ```
310
163
 
311
- ### Cross-process WebSocket with `createHub`
312
-
313
- Use `createHub()` to group WebSocket connections into named channels and broadcast to them — works within a single process, and with optional Redis pub/sub across multiple Node.js processes behind a load balancer.
164
+ ---
314
165
 
315
- ```ts
316
- import { createHub, serve, Router } from 'weifuwu'
317
- import { redis } from 'weifuwu'
166
+ ## Middleware
318
167
 
319
- const hub = createHub({ redis }) // omit redis for in-process only
168
+ All middleware follows `(req, ctx, next) => Response | Promise<Response>`.
320
169
 
321
- const app = new Router().ws('/chat/:room', {
322
- open(ws, ctx) {
323
- hub.join(`room:${ctx.params.room}`, ws)
324
- },
325
- message(ws, ctx, data) {
326
- hub.broadcast(`room:${ctx.params.room}`, {
327
- user: ctx.user?.id,
328
- text: data.toString(),
329
- })
330
- },
331
- close(ws) {
332
- hub.leave(ws)
333
- },
334
- })
335
-
336
- serve(app.handler(), {
337
- port: 3000,
338
- websocket: app.websocketHandler(),
339
- })
170
+ ```ts
171
+ app.use(middleware) // global
172
+ app.use('/admin', middleware) // path-scoped
173
+ app.get('/admin', middleware, handler) // route-level
340
174
  ```
341
175
 
342
- With Redis configured, `hub.broadcast()` publishes to a Redis channel — all processes subscribed via `createHub({ redis })` receive and forward to their local WebSocket connections.
343
-
344
- ---
345
-
346
- # Middleware
176
+ | Middleware | Description |
177
+ |-----------|-------------|
178
+ | `auth(options)` | Bearer token / custom header / verify / proxy |
179
+ | `cors(options?)` | CORS with preflight, origin whitelist, credentials |
180
+ | `csrf(options?)` | Double-submit cookie CSRF protection |
181
+ | `logger(options?)` | Request logging with duration |
182
+ | `rateLimit(options?)` | In-memory rate limiting with headers |
183
+ | `compress(options?)` | Brotli / Gzip / Deflate compression |
184
+ | `validate(schemas)` | Zod validation (body, query, params) |
185
+ | `upload(options?)` | Multipart file upload |
186
+ | `i18n(options)` | Internationalization — `ctx.t()`, locale detection |
187
+ | `seoMiddleware(options?)` | `X-Robots-Tag` header — string or path-based function |
188
+ | `helmet(options?)` | Security headers — CSP, HSTS, X-Frame-Options, etc. |
189
+ | `requestId(options?)` | `X-Request-ID` header + `ctx.requestId` |
347
190
 
348
- ## Auth
191
+ ### auth
349
192
 
350
193
  ```ts
351
194
  import { auth } from 'weifuwu'
352
195
 
353
- // Static bearer token
354
- app.use(auth({ token: 'sk-123' }))
355
-
356
- // Custom verify (JWT, DB, etc.) — return object to set ctx.user
357
- app.use(auth({
358
- verify: async (token) => {
359
- const user = await db.findUserByToken(token)
360
- return user ? { sub: user.id, role: user.role } : null
361
- },
362
- }))
363
-
364
- // Proxy validation to external auth service
196
+ app.use(auth({ token: 'sk-123' })) // static token
197
+ app.use(auth({ header: 'X-API-Key', token: 'my-key' })) // custom header
198
+ app.use(auth({ verify: async (token) => ({ sub: 'abc' }) })) // custom verify
365
199
  app.get('/protected', auth({ proxy: 'http://auth:3000/validate' }), handler)
366
-
367
- // Custom header
368
- app.use(auth({ header: 'X-API-Key', token: 'my-key' }))
369
200
  ```
370
201
 
371
- ## CORS
202
+ ### cors
372
203
 
373
204
  ```ts
374
205
  import { cors } from 'weifuwu'
375
206
 
376
- app.use(cors()) // allow all
377
- app.use(cors({ origin: ['https://example.com'] })) // whitelist
378
- app.use(cors({ origin: (o) => o.endsWith('.trusted.com') ? o : false }))
207
+ app.use(cors()) // allow all
208
+ app.use(cors({ origin: ['https://example.com'] })) // whitelist
209
+ app.use(cors({ origin: (o) => o.endsWith('.trusted.com') && o }))
379
210
  app.use(cors({ credentials: true, maxAge: 3600 }))
380
211
  ```
381
212
 
382
- ## Logger
213
+ ### csrf
214
+
215
+ Double-submit cookie pattern. Sets `_csrf` cookie on GET, validates `X-CSRF-Token` header (or `_csrf` body field) on POST/PUT/DELETE/PATCH.
216
+
217
+ ```ts
218
+ import { csrf } from 'weifuwu'
219
+
220
+ app.use(csrf())
221
+
222
+ // ctx.csrfToken available in handlers
223
+ app.get('/form', (req, ctx) => {
224
+ return new Response(`<input name="_csrf" value="${ctx.csrfToken}" hidden />`, {
225
+ headers: { 'content-type': 'text/html' },
226
+ })
227
+ })
228
+ ```
229
+
230
+ | Option | Default | Description |
231
+ |--------|---------|-------------|
232
+ | `cookie` | `'_csrf'` | Cookie name |
233
+ | `header` | `'x-csrf-token'` | Header name |
234
+ | `key` | `'_csrf'` | Body field name (fallback) |
235
+ | `excludeMethods` | `['GET', 'HEAD', 'OPTIONS']` | Skip validation |
236
+
237
+ For fetch-based forms, `useAction()` reads the `_csrf` cookie automatically.
238
+
239
+ ### logger
383
240
 
384
241
  ```ts
385
242
  import { logger } from 'weifuwu'
386
243
 
387
- app.use(logger()) // GET /hello 200 5ms
388
- app.use(logger({ format: 'combined' })) // with query params
244
+ app.use(logger()) // GET /hello 200 5ms
245
+ app.use(logger({ format: 'combined' })) // with query params
389
246
  ```
390
247
 
391
- ## Rate limit
248
+ ### rateLimit
392
249
 
393
250
  ```ts
394
251
  import { rateLimit } from 'weifuwu'
395
252
 
396
- app.use(rateLimit({ max: 100, window: 60_000 })) // 100 req/min
397
- app.get('/api', rateLimit({ max: 10 }), handler) // per-route
398
-
399
- // Custom key (by API key, user ID, etc.)
400
- app.use(rateLimit({
401
- max: 1000,
402
- key: (req) => req.headers.get('x-api-key') ?? 'anonymous',
403
- }))
253
+ app.use(rateLimit({ max: 100, window: 60_000 })) // 100 req/min
254
+ app.get('/api', rateLimit({ max: 10 }), handler) // per-route
255
+ app.use(rateLimit({ key: (req) => req.headers.get('x-api-key') ?? 'anonymous' }))
404
256
  ```
405
257
 
406
- ## Compression
258
+ ### compress
407
259
 
408
260
  ```ts
409
261
  import { compress } from 'weifuwu'
410
262
 
411
- app.use(compress()) // brotli > gzip > deflate
412
- app.use(compress({ threshold: 2048 })) // only compress > 2KB
263
+ app.use(compress()) // brotli > gzip > deflate
264
+ app.use(compress({ threshold: 2048 })) // only > 2KB
413
265
  ```
414
266
 
415
- ## Validation
267
+ ### validate
416
268
 
417
269
  ```ts
418
270
  import { z } from 'zod'
419
271
  import { validate } from 'weifuwu'
420
272
 
421
- const CreateUser = z.object({
422
- name: z.string().min(1),
423
- email: z.string().email(),
273
+ const CreateUser = z.object({ name: z.string().min(1), email: z.string().email() })
274
+ router.post('/users', validate({ body: CreateUser }), (req, ctx) => {
275
+ // ctx.parsed.body — typed & validated
424
276
  })
425
-
426
- router.post('/users',
427
- validate({ body: CreateUser }),
428
- (req, ctx) => {
429
- // ctx.parsed.body — typed & validated
430
- },
431
- )
432
277
  ```
433
278
 
434
- ## File upload
279
+ ### upload
435
280
 
436
281
  ```ts
437
282
  import { upload } from 'weifuwu'
438
283
 
439
- router.post('/upload',
440
- upload({ dir: './uploads', maxFileSize: 10_485_760 }),
441
- (req, ctx) => {
442
- // ctx.parsed.files.avatar → { name, type, size, path }
443
- // ctx.parsed.fields.title → 'hello'
444
- },
445
- )
284
+ router.post('/upload', upload({ dir: './uploads', maxFileSize: 10_485_760 }), (req, ctx) => {
285
+ // ctx.parsed.files.avatar → { name, type, size, path }
286
+ // ctx.parsed.fields.title → 'hello'
287
+ })
446
288
  ```
447
289
 
448
- ## Cookie
290
+ ### cookie
449
291
 
450
292
  ```ts
451
293
  import { getCookies, setCookie, deleteCookie } from 'weifuwu'
452
294
 
453
- // Read
454
295
  const cookies = getCookies(req) // { session: 'abc' }
455
-
456
- // Set (immutable — returns new Response)
457
296
  let res = new Response('ok')
458
297
  res = setCookie(res, 'session', 'token', { httpOnly: true, secure: true, maxAge: 3600 })
459
-
460
- // Delete
461
298
  res = deleteCookie(res, 'session')
462
299
  ```
463
300
 
464
- ## Static files
301
+ | Option | Type | Description |
302
+ |--------|------|-------------|
303
+ | `domain` | `string` | Cookie domain |
304
+ | `path` | `string` | Cookie path |
305
+ | `maxAge` | `number` | Seconds |
306
+ | `expires` | `Date` | Expiration |
307
+ | `httpOnly` | `boolean` | Not accessible to JS |
308
+ | `secure` | `boolean` | HTTPS only |
309
+ | `sameSite` | `'strict' \| 'lax' \| 'none'` | SameSite policy |
310
+
311
+ ### serveStatic
465
312
 
466
313
  ```ts
467
314
  import { serveStatic } from 'weifuwu'
@@ -469,936 +316,571 @@ import { serveStatic } from 'weifuwu'
469
316
  router.get('/static/*', serveStatic('./public'))
470
317
  ```
471
318
 
472
- Features: MIME type detection (20+ types), ETag + If-None-Match (304), directory index (index.html), path traversal protection, Cache-Control.
473
-
474
- ---
475
-
476
- # PostgreSQL
319
+ 20+ MIME types, ETag + 304, directory index, path traversal protection, Cache-Control.
477
320
 
478
- Built-in PostgreSQL client — connection management, type-safe DDL, transactions, and module lifecycle.
321
+ ### helmet
479
322
 
480
323
  ```ts
481
- import { serve, Router, postgres } from 'weifuwu'
482
-
483
- const app = new Router()
484
- const pg = postgres() // reads DATABASE_URL
485
- app.use(pg) // injects ctx.sql into handlers
486
- ```
487
-
488
- ## Type-safe DDL with schema builder
489
-
490
- Define tables declaratively with type inference — no raw SQL for common operations, no Zod needed:
324
+ import { helmet } from 'weifuwu'
491
325
 
492
- ```ts
493
- import { pgTable, serial, uuid, text, integer, boolean, timestamptz, jsonb, sql, timestamps } from 'weifuwu'
326
+ app.use(helmet()) // CSP, HSTS, X-Frame-Options, X-Content-Type-Options, etc.
494
327
 
495
- const users = pgTable('_users', {
496
- id: serial('id').primaryKey(),
497
- name: text('name').notNull(),
498
- email: text('email').unique().notNull(),
499
- age: integer('age'),
500
- active: boolean('active').default(true),
501
- ...timestamps(), // adds created_at + updated_at with defaults
502
- metadata: jsonb<{ role: string }>('metadata'),
503
- })
328
+ app.use(helmet({
329
+ contentSecurityPolicy: "default-src 'self'",
330
+ xFrameOptions: 'DENY',
331
+ strictTransportSecurity: 'max-age=63072000; includeSubDomains; preload',
332
+ }))
504
333
  ```
505
334
 
506
- Supports 11 column types:
507
- | Builder | DDL | TS Type |
508
- |---------|-----|---------|
509
- | `serial()` | `SERIAL` | `number` |
510
- | `uuid()` | `UUID` | `string` |
511
- | `text()` | `TEXT` | `string` |
512
- | `integer()` | `INTEGER` | `number` |
513
- | `boolean()` | `BOOLEAN` | `boolean` |
514
- | `timestamptz()` | `TIMESTAMPTZ` | `string` |
515
- | `jsonb<T>()` | `JSONB` | `T` |
516
- | `textArray()` | `TEXT[]` | `string[]` |
517
- | `vector(name, dims)` | `vector(N)` | `number[]` |
518
- | `timestamps()` | two TIMESTAMPTZ columns | `{ created_at, updated_at }` |
519
-
520
- Column constraints chainable: `.primaryKey()`, `.notNull()`, `.nullable()`, `.default(value | sql\`...\`)`, `.unique()`, `.references(table, column?, onDelete?)`.
521
-
522
- ## DDL execution
335
+ ### requestId
523
336
 
524
337
  ```ts
525
- await users.create() // CREATE TABLE IF NOT EXISTS
526
- await users.create({ // WITH PARTITION BY RANGE
527
- partitionBy: partitionBy('range', 'created_at'),
528
- })
529
- await users.createIndex('email') // CREATE INDEX
530
- await users.createUniqueIndex('slug') // CREATE UNIQUE INDEX
531
- await users.createIndex('created_at', { desc: true })
532
- await users.createIndex(['a', 'b']) // multi-column
533
- await users.createIndex('embedding', { // pgvector HNSW
534
- type: 'hnsw', operator: 'vector_cosine_ops',
535
- })
536
- await users.drop({ cascade: true })
338
+ import { requestId } from 'weifuwu'
339
+
340
+ app.use(requestId())
341
+ // Sets X-Request-ID header on responses, available as ctx.requestId
537
342
  ```
538
343
 
539
- ## Type-safe CRUD with BoundTable
344
+ 13 security headers set by default with `helmet()`: `X-Content-Type-Options`, `X-Frame-Options`, `X-XSS-Protection`, `Strict-Transport-Security`, `Content-Security-Policy`, `Referrer-Policy`, `Permissions-Policy`, `Cross-Origin-Openner-Policy`, `Cross-Origin-Resource-Policy`, `Cross-Origin-Embedder-Policy`, `X-DNS-Prefetch-Control`, `X-Download-Options`, `X-Permitted-Cross-Domain-Policies`.
540
345
 
541
- Two usage paths — use `pg.table()` when you have a `pg` handle, or `pgTable()` with explicit `sql`:
346
+ ---
542
347
 
543
- The `BoundTable` follows a clean CRUD naming — singular for one, plural for many:
348
+ ## React SSR (tsx)
544
349
 
545
350
  ```ts
546
- // pg.table() — auto-binds sql, no need to pass it
547
- const users = pg.table('_users', {
548
- id: serial('id').primaryKey(),
549
- name: text('name').notNull(),
550
- email: text('email').unique(),
551
- active: boolean('active').default(true),
552
- ...timestamps(),
553
- })
554
-
555
- // Create — single
556
- const user = await users.insert({ name: 'Alice', email: 'alice@test.com' })
557
- // → { id: 1, name: 'Alice', ... }
558
-
559
- // Create — many
560
- const batch = await users.insertMany([
561
- { name: 'Alice' },
562
- { name: 'Bob' },
563
- { name: 'Charlie' },
564
- ])
565
- // → [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, { id: 3, name: 'Charlie' }]
566
-
567
- // Read — by id
568
- const found = await users.read(1)
569
-
570
- // Read — with selected columns
571
- const partial = await users.read(1, { select: ['id', 'name'] })
572
-
573
- // Read — many with optional filtering + pagination
574
- const { count, data } = await users.readMany({ role: 'admin' })
575
- // count is total matching rows, data is the page
576
- const { data: sorted } = await users.readMany({ active: true }, { orderBy: { name: 'asc' } })
577
- const { data: page } = await users.readMany(undefined, { limit: 10, offset: 0 })
578
- const { data: filtered } = await users.readMany(
579
- { role: 'admin' },
580
- { orderBy: { name: 'desc' }, limit: 5 },
581
- )
582
-
583
- // Read — complex conditions with where helpers
584
- import { eq, gte, lt, contains, and } from 'weifuwu'
585
- const { count, data } = await users.readMany(
586
- and(
587
- eq('role', 'admin'),
588
- gte('created_at', '2026-01-01'),
589
- contains('metadata', { region: 'us' }),
590
- ),
591
- { orderBy: { name: 'asc' } },
592
- )
593
- // Array shorthand — implicit AND
594
- const { data } = await users.readMany(
595
- [eq('role', 'admin'), gte('created_at', '2026-01-01')],
596
- { limit: 10 },
597
- )
598
-
599
- // Update — single row by id (auto-sets updated_at if column exists)
600
- const updated = await users.update(1, { name: 'Bob' })
601
- // → { id: 1, name: 'Bob', email: 'alice@test.com', ... }
602
-
603
- // Update — many with Partial where
604
- const count = await users.updateMany({ role: 'guest' }, { role: 'user' })
605
-
606
- // Update — many with SQL where
607
- await users.updateMany(gte('age', 65), { role: 'retired' })
608
-
609
- // Delete — single row by id, returns deleted row
610
- const deleted = await users.delete(1)
611
- // → { id: 1, name: 'Bob', ... } or undefined
612
-
613
- // Delete — many
614
- const deleted = await users.deleteMany({ active: false })
351
+ import { serve, Router } from 'weifuwu'
352
+ import { serve, Router, tsx } from 'weifuwu'
615
353
 
616
- // Read select specific columns
617
- const { data } = await users.readMany(
618
- { role: 'admin' },
619
- { select: ['id', 'name', 'email'], limit: 10 },
620
- )
354
+ const app = new Router()
355
+ app.use('/', await tsx({ dir: './ui/' }))
356
+ serve(app.handler(), { port: 3000, websocket: app.websocketHandler() })
621
357
  ```
622
358
 
623
- ### Upsert
359
+ ### Directory structure
624
360
 
625
- ```ts
626
- // Insert or update on conflict
627
- const user = await users.upsert(
628
- { email: 'alice@test.com', name: 'Alice' },
629
- 'email', // conflict target — column(s) with unique constraint
630
- )
631
- // ON CONFLICT (email) DO UPDATE SET "name" = EXCLUDED."name" RETURNING *
632
361
  ```
633
-
634
- Supports composite conflict targets:
635
-
636
- ```ts
637
- await members.upsert(
638
- { channel_id: 1, member_id: 42, role: 'admin' },
639
- ['channel_id', 'member_id'],
640
- )
362
+ ui/
363
+ ├── pages/
364
+ │ ├── page.tsx → GET / (React component)
365
+ │ ├── layout.tsx → root layout (HTML shell, receives req/ctx)
366
+ │ ├── not-found.tsx → 404 page
367
+ │ ├── about/page.tsx → GET /about
368
+ │ ├── blog/[slug]/
369
+ │ │ ├── page.tsx → GET /blog/:slug
370
+ │ │ ├── load.ts → data fetching (server-only)
371
+ │ │ └── route.ts → POST /blog/:slug (API, named exports)
372
+ │ ├── blog/layout.tsx → /blog/* layout (UI structure, hydrated)
373
+ │ └── api/search/
374
+ │ └── route.ts → GET /api/search
375
+ └── components/
376
+ └── button.tsx
641
377
  ```
642
378
 
643
- ### Count
379
+ ### page.tsx — page component
644
380
 
645
- ```ts
646
- const total = await users.count() // all rows
647
- const admins = await users.count({ role: 'admin' }) // with Partial filter
648
- const recent = await users.count(gte('created_at', from)) // with SQL condition
381
+ ```tsx
382
+ export default function Page({ params, query }: {
383
+ params: { slug: string }
384
+ query: Record<string, string>
385
+ }) {
386
+ return <article><h1>{params.slug}</h1></article>
387
+ }
649
388
  ```
650
389
 
651
- ### Soft delete
652
-
653
- If a table has a `deleted_at` column, `delete()` and `deleteMany()` set the timestamp instead of removing the row:
390
+ ### load.ts — data fetching (server-only)
654
391
 
655
392
  ```ts
656
- const users = pg.table('_users', {
657
- id: serial('id').primaryKey(),
658
- name: text('name'),
659
- deleted_at: timestamptz('deleted_at'), // enables soft delete
660
- })
393
+ export default async function load({ params, query }: {
394
+ params: Record<string, string>
395
+ query: Record<string, string>
396
+ }) {
397
+ const data = await db.query(params.slug)
398
+ return { data } // merged into page component props
399
+ }
400
+ ```
661
401
 
662
- await users.delete(1) // SET deleted_at = NOW() WHERE id = 1
663
- await users.deleteMany({ role: 'guest' })
402
+ ### layout.tsx
664
403
 
665
- // readMany auto-filters soft-deleted rows
666
- const { data } = await users.readMany() // WHERE deleted_at IS NULL
404
+ **Root layout** (`pages/layout.tsx`) receives `{ children, req, ctx }`:
667
405
 
668
- // Include soft-deleted rows
669
- const { data } = await users.readMany(undefined, { withDeleted: true })
406
+ > The `<div id="__weifuwu_root">` hydration target is **auto-injected** by the framework — do not add it manually. Just render `{children}` where you want page content.
670
407
 
671
- // Hard delete (bypass soft delete)
672
- await users.hardDelete(1)
673
- await users.hardDeleteMany({ role: 'guest' })
408
+ ```tsx
409
+ export default function RootLayout({ children, req, ctx }: {
410
+ children: React.ReactNode
411
+ req: Request
412
+ ctx: Context
413
+ }) {
414
+ return (
415
+ <html>
416
+ <head><title>App</title></head>
417
+ <body><main>{children}</main></body>
418
+ </html>
419
+ )
420
+ }
674
421
  ```
675
422
 
676
- ### Timestamps
677
-
678
- The `timestamps()` macro adds `created_at` and `updated_at` columns with `NOT NULL DEFAULT NOW()`.
679
-
680
- `update()` automatically appends `"updated_at" = NOW()` to the SET clause when the column exists — no need to pass it manually.
681
-
682
- ### Where helpers
423
+ **Nested layouts** (`pages/blog/layout.tsx`) — receives only `{ children }`.
683
424
 
684
- Importable functions for composing complex WHERE clauses. Works with `readMany`, `updateMany`, `deleteMany`, and `count` pass as the first argument (single `SQL` or `SQL[]` for implicit AND):
425
+ ### route.tsAPI (co-located with page)
685
426
 
686
427
  ```ts
687
- import { eq, ne, gt, gte, lt, lte, isNull, isNotNull, like, contains, in_, and, or, not } from 'weifuwu'
688
-
689
- // Single condition
690
- const { data } = await users.readMany(gte('created_at', '2026-01-01'))
691
-
692
- // Array = implicit AND
693
- const { data } = await users.readMany([
694
- eq('role', 'admin'),
695
- gte('created_at', '2026-01-01'),
696
- contains('metadata', { region: 'us' }),
697
- ])
698
-
699
- // Explicit AND/OR composition
700
- const { data } = await users.readMany(
701
- or(
702
- and(eq('role', 'admin'), eq('status', 'active')),
703
- eq('role', 'superadmin'),
704
- ),
705
- { orderBy: { name: 'asc' }, limit: 10 },
706
- )
707
-
708
- // Also works with updateMany and deleteMany
709
- await users.updateMany(gte('age', 65), { role: 'retired' })
710
- await users.deleteMany(eq('status', 'archived'))
428
+ export const POST: Handler = async (req, ctx) => {
429
+ const body = await req.json()
430
+ return Response.json({ ...body, slug: ctx.params.slug })
431
+ }
711
432
  ```
712
433
 
713
- | Helper | SQL | Example |
714
- |--------|-----|---------|
715
- | `eq(col, val)` | `= $1` | `eq('level', 'error')` |
716
- | `ne(col, val)` | `!= $1` | `ne('status', 'archived')` |
717
- | `gt(col, val)` | `> $1` | `gt('age', 18)` |
718
- | `gte(col, val)` | `>= $1` | `gte('created_at', '2026-01-01')` |
719
- | `lt(col, val)` | `< $1` | `lt('id', beforeId)` |
720
- | `lte(col, val)` | `<= $1` | `lte('score', 100)` |
721
- | `isNull(col)` | `IS NULL` | `isNull('deleted_at')` |
722
- | `isNotNull(col)` | `IS NOT NULL` | `isNotNull('email')` |
723
- | `like(col, pattern)` | `LIKE $1` | `like('name', 'Alice%')` |
724
- | `contains(col, obj)` | `@> $1::jsonb` | `contains('metadata', { service: 'auth' })` |
725
- | `in_(col, arr)` | `= ANY($1)` | `in_('id', [1, 2, 3])` |
726
- | `and(...conds)` | `(... AND ...)` | `and(eq('a', 1), eq('b', 2))` |
727
- | `or(...conds)` | `(... OR ...)` | `or(eq('a', 1), eq('b', 2))` |
728
- | `not(cond)` | `NOT (...)` | `not(eq('status', 'archived'))` |
434
+ ### Public environment variables
729
435
 
730
- ### Complex queries use raw SQL
436
+ Prefix with `WEIFUWU_PUBLIC_` for automatic inlining into the client hydration bundle:
731
437
 
732
- ```ts
733
- app.get('/users/stats', async (req, ctx) => {
734
- const rows = await ctx.sql`
735
- SELECT u.*, count(p.id) as posts
736
- FROM ${users} u LEFT JOIN posts p ON p.user_id = u.id
737
- GROUP BY u.id
738
- `
739
- return Response.json(rows)
740
- })
438
+ ```bash
439
+ WEIFUWU_PUBLIC_API_URL=https://api.example.com
741
440
  ```
742
441
 
743
- ### Transactions
744
-
745
- ```ts
746
- const result = await pg.transaction(async (tx) => {
747
- const [user] = await tx`INSERT INTO "_users" (...) VALUES (...) RETURNING *`
748
- const [wallet] = await tx`INSERT INTO "_wallets" ("user_id") VALUES (${user.id}) RETURNING *`
749
- return { user, wallet }
750
- })
442
+ ```tsx
443
+ // page.tsx — works on both server and client
444
+ const apiUrl = process.env.WEIFUWU_PUBLIC_API_URL
751
445
  ```
752
446
 
753
- Use BoundTable methods inside transactions with `withSql()`:
754
-
755
- ```ts
756
- const users = pg.table('_users', { ... })
757
- const wallets = pg.table('_wallets', { ... })
758
-
759
- const result = await pg.transaction(async (tx) => {
760
- const txUsers = users.withSql(tx)
761
- const txWallets = wallets.withSql(tx)
762
-
763
- const user = await txUsers.insert({ name: 'Alice' })
764
- await txWallets.insert({ user_id: user.id })
765
- return user
766
- })
767
- ```
447
+ The hydration bundle also injects `self.process = { env: {} }` as a safety net so any `process.env.*` reference in bundled dependencies won't throw.
768
448
 
769
- ### Connection lifecycle
449
+ ### Client-side hooks
770
450
 
771
- ```ts
772
- const pg = postgres() // reads DATABASE_URL
773
- const pg = postgres('postgres://...') // explicit connection
774
- const pg = postgres({
775
- connection: 'postgres://...',
776
- max: 10, // pool size
777
- ssl: { rejectUnauthorized: false }, // SSL options
778
- idle_timeout: 30, // idle timeout (s)
779
- connect_timeout: 10, // connection timeout (s)
780
- closeTimeout: 5, // close grace period (s)
781
- signal: ac.signal, // abort → sql.end()
782
- })
783
- await pg.close()
784
- ```
451
+ #### useWebsocket — auto-reconnecting WebSocket
785
452
 
786
- ### Module base class
453
+ ```tsx
454
+ import { useWebsocket } from 'weifuwu/react'
787
455
 
788
- Every database module extends `PgModule`:
456
+ function Chat() {
457
+ const { send, lastMessage, readyState, close, reconnect } = useWebsocket('/ws/chat', {
458
+ onMessage: (data) => console.log('received', data),
459
+ reconnect: { maxRetries: 10, delay: 3000 },
460
+ })
789
461
 
790
- ```ts
791
- import { PgModule } from 'weifuwu'
792
-
793
- class MyModule extends PgModule {
794
- constructor(pg: PostgresClient) {
795
- super(pg) // sets this.sql = pg.sql
796
- }
797
- async migrate() { /* override */ }
798
-
799
- // Built-in helpers
800
- // this.table(name, builders) — create a BoundTable
801
- // this.transaction(fn) — run in a transaction
802
- // close() — calls pg.close() automatically
462
+ return <div>
463
+ <p>Status: {readyState === 1 ? 'Connected' : readyState === 0 ? 'Connecting...' : 'Disconnected'}</p>
464
+ <button onClick={() => send('Hello')}>Send</button>
465
+ {lastMessage && <p>Last: {lastMessage}</p>}
466
+ </div>
803
467
  }
804
468
  ```
805
469
 
806
- ---
807
-
808
- # Auth & User
470
+ `url` accepts `string`, `URL`, or `() => string | URL | null` (function form avoids reconnecting on every render). `close()` disables auto-reconnect; `reconnect()` resets retry count.
809
471
 
810
- ```ts
811
- import { serve, Router, postgres, user } from 'weifuwu'
812
-
813
- const app = new Router()
814
- const pg = postgres()
815
- await pg.migrate()
472
+ #### useAction — async form submission
816
473
 
817
- const auth = user({ pg, jwtSecret: process.env.JWT_SECRET! })
474
+ ```tsx
475
+ import { useAction } from 'weifuwu/react'
818
476
 
819
- // POST /auth/register { email, password, name }
820
- // POST /auth/login { email, password }
821
- // GET /auth/oauth/authorize?client_id=...&redirect_uri=...&response_type=code
822
- // POST /auth/oauth/consent
823
- // POST /auth/oauth/token (grant_type=authorization_code|client_credentials)
824
- app.use('/auth', auth.router())
477
+ function FeedbackForm() {
478
+ const { submit, data, error, pending, reset } = useAction('/api/feedback', { method: 'POST' })
825
479
 
826
- // Protected routes verifies JWT, sets ctx.user
827
- app.get('/me', auth.middleware(), async (req, ctx) => {
828
- return Response.json(ctx.user)
829
- // { id, email, name, role }
830
- })
480
+ return <form onSubmit={() => submit({ name, email })}>
481
+ <button disabled={pending}>{pending ? 'Saving...' : 'Submit'}</button>
482
+ {error && <p className="text-red-500">{error.message}</p>}
483
+ {data && <p>Saved: {data.id}</p>}
484
+ </form>
485
+ }
831
486
  ```
832
487
 
833
- Password hashing uses `crypto.scryptSync` + `timingSafeEqual` (Node.js built-in, zero deps). JWT tokens use the `jsonwebtoken` package. The users table (`_users` by default) is auto-created on first `migrate()`.
488
+ Auto-serializes JSON, auto-reads `_csrf` cookie and sends as `X-CSRF-Token`. Returns `{ submit, data, error, pending, reset }`. `submit(body?)` returns `Promise<T>`.
834
489
 
835
- ## OAuth2 Server
490
+ ### Client-side navigation
836
491
 
837
- Enable OAuth2 Server to let third-party apps (SPA, mobile, microservices) authenticate users through your app.
838
-
839
- ```ts
840
- const auth = user({
841
- pg,
842
- jwtSecret: process.env.JWT_SECRET!,
843
- oauth2: { server: true },
844
- })
845
-
846
- await auth.migrate() // creates _users + _oauth2_clients + _oauth2_codes + _oauth2_tokens
847
-
848
- // Register a client app (programmatic — CLI, admin UI, seed script)
849
- const client = await auth.registerClient({
850
- name: 'My SPA',
851
- redirectUris: ['https://myapp.com/callback'],
852
- })
853
- // → { clientId, clientSecret, name, redirectUris }
492
+ ```tsx
493
+ import { Link, useNavigate } from 'weifuwu/react'
854
494
 
855
- // Use auth middleware to protect routes — OAuth2 JWT tokens work seamlessly
856
- app.get('/api/data', auth.middleware(), handler)
495
+ function Nav() {
496
+ const navigate = useNavigate()
497
+ return (
498
+ <nav>
499
+ <Link href="/about">About</Link>
500
+ <button onClick={() => navigate('/contact')}>Contact</button>
501
+ </nav>
502
+ )
503
+ }
857
504
  ```
858
505
 
859
- ### Supported Grant Types
860
-
861
- | Grant | Use Case | PKCE |
862
- |-------|----------|------|
863
- | `authorization_code` (with client_secret) | Server-side apps | Optional |
864
- | `authorization_code` (with `code_challenge`/`code_verifier`) | SPA / Mobile apps | Required |
865
- | `client_credentials` | Machine-to-machine | — |
506
+ `navigate(href)` fetches the target via SSR, extracts `__weifuwu_root` content and `__WEIFUWU_PROPS`, replaces in-place, then imports the new hydration bundle. `load.ts` runs on the server for every navigation. Initial load is full SSR; subsequent navigations are client-side.
866
507
 
867
- ### Flow (Authorization Code + PKCE)
868
-
869
- ```
870
- 1. Third-party app redirects user:
871
- GET /oauth/authorize?client_id=xxx&redirect_uri=https://app.com/cb
872
- &response_type=code&code_challenge=S256&state=yyy
508
+ ### Development mode
873
509
 
874
- 2. User not logged in 302 to /login?redirect=... auto returns to consent page after login
510
+ Auto-detected when `NODE_ENV !== 'production'`. File watching (`chokidar`), single-file recompilation, WebSocket live reload (`/__weifuwu/livereload`), Tailwind CSS v4 auto-compilation.
875
511
 
876
- 3. User confirms consent → POST /oauth/consent { approve: true, client_id, ... }
877
- 302 redirect_uri?code=xxx&state=yyy
512
+ ### Tailwind CSS
878
513
 
879
- 4. Third-party app POST /oauth/token
880
- { grant_type: authorization_code, code, client_id, client_secret,
881
- redirect_uri, code_verifier }
882
- → { access_token, token_type: "Bearer", expires_in, refresh_token }
514
+ If `ui/app.css` exists with `@import "tailwindcss"`, it's compiled automatically. If not found, one is created. PostCSS + `@tailwindcss/postcss`, zero config.
883
515
 
884
- 5. access_token is a standard JWT — auth.middleware() and auth.verify() work with it directly
885
- ```
516
+ ### shadcn/ui
886
517
 
887
- ### Client Management
518
+ Works out of the box:
888
519
 
889
- ```ts
890
- const client = await auth.registerClient({ name, redirectUris })
891
- const found = await auth.getClient(client.clientId)
892
- await auth.revokeClient(client.clientId)
520
+ ```bash
521
+ npx shadcn@latest init
522
+ # Style: your preference
523
+ # Base color: your preference
524
+ # CSS file path: ui/app.css
525
+ # Import alias: @/ → ./ui/
893
526
  ```
894
527
 
895
- ### Using OAuth2 Tokens with the Built-in Auth Middleware
528
+ ---
896
529
 
897
- The `access_token` issued by the OAuth2 Server shares the same `jwtSecret` and compatible payload (`sub`, `email`, `role`) as password-login JWTs, so `auth()` can verify OAuth2 tokens without any modifications:
530
+ ## PostgreSQL
898
531
 
899
532
  ```ts
900
- import { auth } from 'weifuwu'
533
+ import { serve, Router, postgres, pgTable, serial, text, boolean, timestamps, sql } from 'weifuwu'
901
534
 
902
- // Same auth() middleware validates both password-login JWTs and OAuth2 JWTs
903
- app.get('/api', auth({ verify: (token) => auth.verify(token) }), handler)
535
+ const pg = postgres() // reads DATABASE_URL
536
+ const app = new Router()
537
+ app.use(pg) // injects ctx.sql
904
538
  ```
905
539
 
906
- For `client_credentials` tokens (machine-to-machine), `verify()` returns `null` since no user is associated.
907
-
908
- ### Social Login (GitHub) — Cookbook
909
-
910
- `user()` does not bundle social login (to avoid third-party dependencies), but adding a GitHub login with the low-level API takes ~30 lines:
540
+ ### Type-safe DDL
911
541
 
912
542
  ```ts
913
- import { user } from 'weifuwu'
914
- import jwt from 'jsonwebtoken'
915
-
916
- const auth = user({ pg, jwtSecret })
917
-
918
- // 1. Redirect to GitHub authorization
919
- app.get('/auth/github', () => {
920
- const url = new URL('https://github.com/login/oauth/authorize')
921
- url.searchParams.set('client_id', process.env.GH_CLIENT_ID!)
922
- url.searchParams.set('redirect_uri', 'http://localhost:3000/auth/github/callback')
923
- url.searchParams.set('scope', 'user:email')
924
- return Response.redirect(url.href)
543
+ const users = pgTable('_users', {
544
+ id: serial('id').primaryKey(),
545
+ name: text('name').notNull(),
546
+ email: text('email').unique().notNull(),
547
+ active: boolean('active').default(true),
548
+ ...timestamps(),
925
549
  })
926
550
 
927
- // 2. GitHub callback → fetch user info → register/login
928
- app.get('/auth/github/callback', async (req) => {
929
- const { code } = Object.fromEntries(new URL(req.url).searchParams)
930
- if (!code) return new Response('Missing code', { status: 400 })
931
-
932
- // Exchange code for token
933
- const tokenRes = await fetch('https://github.com/login/oauth/access_token', {
934
- method: 'POST',
935
- headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
936
- body: JSON.stringify({
937
- client_id: process.env.GH_CLIENT_ID,
938
- client_secret: process.env.GH_CLIENT_SECRET,
939
- code,
940
- }),
941
- })
942
- const { access_token } = await tokenRes.json() as any
943
-
944
- // Fetch user info from GitHub
945
- const userRes = await fetch('https://api.github.com/user', {
946
- headers: { Authorization: `Bearer ${access_token}` },
947
- })
948
- const ghUser = await userRes.json() as any
949
-
950
- // Find or create local user
951
- const existing = await pg.sql`SELECT * FROM "_users" WHERE email = ${ghUser.email}`
952
- let localUser = existing[0]
953
-
954
- if (!localUser) {
955
- localUser = await auth.register({
956
- email: ghUser.email,
957
- password: crypto.randomUUID(), // Random password — user can only log in via GitHub
958
- name: ghUser.name ?? ghUser.login,
959
- })
960
- }
961
-
962
- // Sign JWT (same format as user())
963
- const token = jwt.sign(
964
- { sub: localUser.id, email: localUser.email, role: localUser.role ?? 'user' },
965
- process.env.JWT_SECRET!,
966
- { expiresIn: '24h' },
967
- )
968
- return Response.json({ token })
969
- })
551
+ await users.create()
552
+ await users.createIndex('email')
970
553
  ```
971
554
 
972
- The same pattern works for Google, WeChat, or any OAuth2 provider.
973
-
974
- ---
975
-
976
- # React SSR with tsx()
555
+ ### BoundTable CRUD
977
556
 
978
557
  ```ts
979
- import { serve, Router } from 'weifuwu'
980
- import { tsx } from 'weifuwu/tsx'
981
-
982
- const app = new Router()
983
- app.use('/', await tsx({ dir: './ui/' }))
984
-
985
- serve(app.handler(), { port: 3000, websocket: app.websocketHandler() })
986
- ```
987
-
988
- ### Directory structure
989
-
990
- ```
991
- ui/
992
- ├── pages/ ← page files
993
- │ ├── page.tsx → GET / (React component, default export)
994
- │ ├── layout.tsx → root layout (HTML shell, receives req/ctx, NOT hydrated)
995
- │ ├── not-found.tsx → 404 error page (rendered for unmatched routes, wrapped in layout)
996
- │ ├── about/page.tsx → GET /about
997
- │ ├── blog/[slug]/
998
- │ │ ├── page.tsx → GET /blog/:slug
999
- │ │ ├── load.ts → data fetching (server-only, default export)
1000
- │ │ └── route.ts → POST /blog/:slug (API, named exports POST/PUT/DELETE/...)
1001
- │ ├── blog/layout.tsx → /blog/* layout (UI structure, receives children, hydrated)
1002
- │ └── api/search/
1003
- │ └── route.ts → GET /api/search (standalone API, no page.tsx needed)
1004
- └── components/ ← component files (auto-detected by HMR)
1005
- └── button.tsx
1006
- ```
1007
-
1008
- ### Development mode
1009
-
1010
- tsx() runs in development mode automatically when `NODE_ENV !== 'production'`:
1011
-
1012
- - **File watching** — chokidar watches the `dir` directory for `.tsx`/`.ts` changes
1013
- - Page files in `pages/` → single-file recompilation + registry update
1014
- - Component files in `components/` → full rebuild of all pages
1015
- - New files are detected automatically
1016
- - **Live reload** — Compiled via esbuild `write: false` + `vm.Script.runInContext` (no disk writes, no `node --watch` conflict)
1017
- - **WebSocket auto-refresh** — `/__weifuwu/livereload` endpoint pushes reload signals; browser refreshes automatically
1018
- - **`node --watch` compatible** — External files (`app.ts`, `middleware/`) handled by `--watch` restart; `ui/` changes handled by tsx() without conflict
1019
-
1020
- ```bash
1021
- node app.ts # development (auto-reload + live refresh)
1022
- NODE_ENV=production node app.ts # production
1023
- ```
1024
-
1025
- ### Tailwind CSS
1026
-
1027
- tsx() includes built-in Tailwind CSS v4 support. If an `app.css` file exists in the `dir` directory, it is compiled automatically through PostCSS + `@tailwindcss/postcss`. If no `app.css` is found, one is created automatically:
558
+ const users = pg.table('_users', { /* column defs */ })
1028
559
 
1029
- ```css
1030
- @import "tailwindcss";
1031
- ```
560
+ const user = await users.insert({ name: 'Alice' })
561
+ const batch = await users.insertMany([{ name: 'Alice' }, { name: 'Bob' }])
562
+ const found = await users.read(1)
563
+ const { count, data } = await users.readMany({ role: 'admin' }, { orderBy: { name: 'asc' }, limit: 10 })
564
+ await users.update(1, { name: 'Bob' })
565
+ await users.delete(1)
1032
566
 
1033
- Write `className` directly in your components — no CLI, no configuration:
567
+ // Upsert
568
+ await users.upsert({ email: 'alice@test.com' }, 'email')
1034
569
 
1035
- ```tsx
1036
- export default function Home() {
1037
- return <h1 className="text-3xl font-bold text-blue-600">Hello</h1>
1038
- }
570
+ // Count
571
+ const total = await users.count()
572
+ const admins = await users.count({ role: 'admin' })
1039
573
  ```
1040
574
 
1041
- ### `@` alias
575
+ ### Where helpers
1042
576
 
1043
- If your project has a `tsconfig.json` or `jsconfig.json` with `compilerOptions.paths`, tsx() reads it automatically and passes aliases to all esbuild builds (SSR compilation, hydration bundles, and hot reload):
577
+ ```ts
578
+ import { eq, gte, contains, and, or } from 'weifuwu'
1044
579
 
1045
- ```json
1046
- {
1047
- "compilerOptions": {
1048
- "paths": {
1049
- "@/*": ["./ui/*"]
1050
- }
1051
- }
1052
- }
580
+ const { data } = await users.readMany(
581
+ and(eq('role', 'admin'), gte('created_at', '2026-01-01')),
582
+ { orderBy: { name: 'asc' } },
583
+ )
1053
584
  ```
1054
585
 
1055
- ### shadcn/ui
586
+ | Helper | SQL | Example |
587
+ |--------|-----|---------|
588
+ | `eq(col, val)` | `= $1` | `eq('level', 'error')` |
589
+ | `ne(col, val)` | `!= $1` | `ne('status', 'archived')` |
590
+ | `gt(col, val)` | `> $1` | `gt('age', 18)` |
591
+ | `gte(col, val)` | `>= $1` | `gte('created_at', '2026-01-01')` |
592
+ | `lt(col, val)` | `< $1` | `lt('id', beforeId)` |
593
+ | `lte(col, val)` | `<= $1` | `lte('score', 100)` |
594
+ | `isNull(col)` | `IS NULL` | `isNull('deleted_at')` |
595
+ | `isNotNull(col)` | `IS NOT NULL` | `isNotNull('email')` |
596
+ | `like(col, pattern)` | `LIKE $1` | `like('name', 'Alice%')` |
597
+ | `contains(col, obj)` | `@> $1::jsonb` | `contains('metadata', { service: 'auth' })` |
598
+ | `in_(col, arr)` | `= ANY($1)` | `in_('id', [1, 2, 3])` |
599
+ | `and(...conds)` | `(... AND ...)` | `and(eq('a', 1), eq('b', 2))` |
600
+ | `or(...conds)` | `(... OR ...)` | `or(eq('a', 1), eq('b', 2))` |
601
+ | `not(cond)` | `NOT (...)` | `not(eq('status', 'archived'))` |
1056
602
 
1057
- tsx() works with [shadcn/ui](https://ui.shadcn.com) out of the box.
603
+ ### Transactions
1058
604
 
1059
- ```bash
1060
- npx shadcn@latest init
1061
- # Style: your preference
1062
- # Base color: your preference
1063
- # CSS file path: ui/app.css
1064
- # Import alias: @/ → ./ui/
605
+ ```ts
606
+ const result = await pg.transaction(async (tx) => {
607
+ const users = pg.table('_users', { ... }).withSql(tx)
608
+ const user = await users.insert({ name: 'Alice' })
609
+ return user
610
+ })
1065
611
  ```
1066
612
 
1067
- ### page.tsx — page component
613
+ ### Column types
1068
614
 
1069
- ```tsx
1070
- export default function Page({ params, query }: {
1071
- params: { slug: string }
1072
- query: Record<string, string>
1073
- }) {
1074
- return <article><h1>{params.slug}</h1></article>
1075
- }
1076
- ```
615
+ | Builder | DDL | TS Type |
616
+ |---------|-----|---------|
617
+ | `serial()` | `SERIAL` | `number` |
618
+ | `uuid()` | `UUID` | `string` |
619
+ | `text()` | `TEXT` | `string` |
620
+ | `integer()` | `INTEGER` | `number` |
621
+ | `boolean()` | `BOOLEAN` | `boolean` |
622
+ | `timestamptz()` | `TIMESTAMPTZ` | `string` |
623
+ | `jsonb<T>()` | `JSONB` | `T` |
624
+ | `textArray()` | `TEXT[]` | `string[]` |
625
+ | `vector(name, dims)` | `vector(N)` | `number[]` |
626
+ | `timestamps()` | Two TIMESTAMPTZ columns | `{ created_at, updated_at }` |
1077
627
 
1078
- ### load.ts — data fetching (server-only)
628
+ ---
629
+
630
+ ## Auth
1079
631
 
1080
632
  ```ts
1081
- export default async function load({ params, query }: {
1082
- params: Record<string, string>
1083
- query: Record<string, string>
1084
- }) {
1085
- const data = await db.query(params.slug)
1086
- return { data } // merged into props passed to page.tsx
1087
- }
1088
- ```
633
+ import { user, postgres } from 'weifuwu'
1089
634
 
1090
- ### layout.tsx
635
+ const pg = postgres()
636
+ const auth = user({ pg, jwtSecret: process.env.JWT_SECRET! })
637
+ await auth.migrate()
1091
638
 
1092
- **Root layout** (`pages/layout.tsx`) — receives `{ children, req, ctx }`:
639
+ app.use('/auth', auth.router())
1093
640
 
1094
- ```tsx
1095
- export default function RootLayout({ children, req, ctx }: {
1096
- children: React.ReactNode
1097
- req: Request
1098
- ctx: Context
1099
- }) {
1100
- return (
1101
- <html>
1102
- <head><title>App</title></head>
1103
- <body><div id="__weifuwu_root">{children}</div></body>
1104
- </html>
1105
- )
1106
- }
1107
- ```
641
+ // POST /auth/register { email, password, name }
642
+ // POST /auth/login { email, password }
1108
643
 
1109
- **Nested layouts** (`pages/blog/layout.tsx`) receives only `{ children }`.
644
+ app.get('/me', auth.middleware(), (req, ctx) =>
645
+ Response.json(ctx.user)
646
+ )
647
+ ```
1110
648
 
1111
- ### route.ts — API (co-located with page)
649
+ ### OAuth2 Server
1112
650
 
1113
651
  ```ts
1114
- export const POST: Handler = async (req, ctx) => {
1115
- const body = await req.json()
1116
- return Response.json({ ...body, slug: ctx.params.slug })
1117
- }
1118
- ```
652
+ const auth = user({ pg, jwtSecret, oauth2: { server: true } })
653
+ await auth.migrate()
1119
654
 
1120
- ### not-found.tsx 404 page
655
+ // Register OAuth2 client
656
+ const client = await auth.registerClient({ name: 'My App', redirectUris: ['https://app.com/cb'] })
1121
657
 
1122
- ```tsx
1123
- export default function NotFound() {
1124
- return <h1 class="text-4xl">404 – Not Found</h1>
1125
- }
658
+ // Authorization code + PKCE flow built-in
1126
659
  ```
1127
660
 
1128
- ---
661
+ | Grant | Use Case |
662
+ |-------|----------|
663
+ | `authorization_code` (client_secret) | Server-side apps |
664
+ | `authorization_code` (PKCE) | SPA / Mobile apps |
665
+ | `client_credentials` | Machine-to-machine |
1129
666
 
1130
- # AI: Streaming & Workflow
667
+ ---
1131
668
 
1132
- ## AI streaming
669
+ ## WebSocket & Real-time
1133
670
 
1134
- Server-sent event streaming via the Vercel AI SDK:
671
+ ### Server-side
1135
672
 
1136
673
  ```ts
1137
- import { serve, Router, aiStream, openai } from 'weifuwu'
1138
-
1139
- const app = new Router()
1140
- const chat = await aiStream(async (req, ctx) => {
1141
- const { messages } = await req.json()
1142
- return { model: openai('gpt-4o'), messages }
674
+ app.ws('/chat/:room', {
675
+ open(ws, ctx) { ws.send(`joined room ${ctx.params.room}`) },
676
+ message(ws, ctx, data) { /* handle message */ },
677
+ close(ws, ctx) { /* cleanup */ },
678
+ error(ws, ctx, err) { /* log */ },
1143
679
  })
1144
- app.use('/chat', chat.router())
1145
-
1146
- serve(app.handler(), { port: 3000 })
1147
680
  ```
1148
681
 
1149
- ## runWorkflow
1150
-
1151
- Multi-step DAG execution engine — packaged as a single AI SDK `Tool`. Use it with `streamText()` or `generateText()` when the LLM needs conditional logic, loops, or multi-step tool orchestration.
682
+ ### Cross-process with createHub
1152
683
 
1153
684
  ```ts
1154
- import { tool, streamText, runWorkflow } from 'weifuwu'
1155
- import { z } from 'zod'
685
+ import { createHub, redis } from 'weifuwu'
1156
686
 
1157
- const tools = {
1158
- queryUser: tool({
1159
- description: 'Query user info',
1160
- inputSchema: z.object({ userId: z.string() }),
1161
- execute: async ({ userId }) => ({ id: userId, email: 'user@test.com', name: 'Test' }),
1162
- }),
1163
- sendEmail: tool({
1164
- description: 'Send an email',
1165
- inputSchema: z.object({ to: z.string(), subject: z.string() }),
1166
- execute: async ({ to, subject }) => ({ sent: true }),
1167
- }),
1168
- runWF: runWorkflow({ tools: { queryUser, sendEmail } }),
1169
- }
687
+ const hub = createHub({ redis }) // omit redis for in-process only
1170
688
 
1171
- const result = await streamText({
1172
- model,
1173
- tools,
1174
- messages: [{ role: 'user', content: 'Query user 123, send welcome email if exists' }],
689
+ app.ws('/chat/:room', {
690
+ open(ws, ctx) { hub.join(`room:${ctx.params.room}`, ws) },
691
+ message(ws, ctx, data) { hub.broadcast(`room:${ctx.params.room}`, { text: data.toString() }) },
692
+ close(ws) { hub.leave(ws) },
1175
693
  })
1176
694
  ```
1177
695
 
1178
- ### Node types
1179
-
1180
- 7 built-in node types for defining the execution graph:
1181
-
1182
- | Node | Purpose | Input |
1183
- |------|---------|-------|
1184
- | `call` | Call a registered AI SDK Tool | `{ tool: "name", args: {...} }` |
1185
- | `set` | Assign a variable | `{ name: "x", value: 42 }` |
1186
- | `get` | Read a variable | `{ name: "x" }` |
1187
- | `eval` | Evaluate an expression | `{ expression: "$var.x + 1" }` |
1188
- | `if` | Conditional branch | `{ conditions: [{ test: ..., body: [nodes] }] }` |
1189
- | `while` | Loop | `{ condition: "$var.i < 5" }, body: [nodes]` |
1190
- | `http` | HTTP request | `{ url: "https://...", method: "GET" }` |
696
+ ---
1191
697
 
1192
- ### Reference syntax
698
+ ## AI
1193
699
 
1194
- | Pattern | Meaning | Example |
1195
- |---------|---------|---------|
1196
- | `$var.x` | Variable `x` | `$var.counter` |
1197
- | `$nodes.u.output` | Full output of node `u` | `$nodes.u.output` |
1198
- | `$nodes.u.output.field` | Specific field | `$nodes.u.output.email` |
1199
- | `$input.userId` | Input param | `$input.userId` |
700
+ ### Streaming
1200
701
 
1201
- ---
702
+ ```ts
703
+ import { aiStream, openai } from 'weifuwu'
1202
704
 
1203
- # AI Agent
705
+ const chat = await aiStream(async (req) => ({
706
+ model: openai('gpt-4o'),
707
+ messages: (await req.json()).messages,
708
+ }))
709
+ app.use('/chat', chat.router())
710
+ ```
1204
711
 
1205
- Server-side AI agents with OpenAI-compatible API. Built-in chat, tool-use (tool-calling), and knowledge (RAG) types. Works out of the box with Ollama or any OpenAI-compatible provider.
712
+ ### AI Agents
1206
713
 
1207
714
  ```ts
1208
715
  import { agent } from 'weifuwu'
1209
716
 
1210
717
  const agents = agent({ pg })
1211
-
1212
718
  await agents.migrate()
1213
719
  app.use('/api', agents.router())
720
+
721
+ await agents.addKnowledge(agentId, 'Title', 'Document content...')
722
+ ```
723
+
724
+ ### DAG Workflow
725
+
726
+ ```ts
727
+ import { runWorkflow, tool, streamText } from 'weifuwu'
728
+ import { z } from 'zod'
729
+
730
+ const tools = { queryUser: tool({ ... }) }
731
+ const wf = runWorkflow({ tools })
1214
732
  ```
1215
733
 
1216
- | Type | Description | Execution |
1217
- |------|-------------|-----------|
1218
- | `chat` | Pure conversation | `streamText()` / `generateText()` |
1219
- | `tool-use` | Tool-calling agent | `streamText({ tools })` |
734
+ ---
1220
735
 
1221
- ### Knowledge (RAG)
736
+ ## Data Layer
1222
737
 
1223
- Add documents to any agent — `searchKnowledge` tool auto-injected:
738
+ ### Redis
1224
739
 
1225
740
  ```ts
1226
- await agents.addKnowledge(agentId, 'Title', 'Document content...')
1227
- ```
741
+ import { redis } from 'weifuwu'
1228
742
 
1229
- ### Streaming
743
+ const r = redis() // reads REDIS_URL
744
+ app.use(r) // injects ctx.redis
1230
745
 
1231
- ```http
1232
- POST /agents/:id/run { input: "hello", stream: true }
1233
- → event-stream (fullStream SSE: text-delta, tool-call, tool-result, finish)
746
+ await ctx.redis.set('key', 'value')
1234
747
  ```
1235
748
 
1236
- ### Programmatic API
749
+ ### Queue
1237
750
 
1238
751
  ```ts
1239
- const result = await agents.run(agentId, { input: 'hello', stream: false })
1240
- // { output: "Hello!", elapsed: 1234 }
752
+ import { queue, redis } from 'weifuwu'
753
+
754
+ const q = queue({ redis })
755
+ await q.add('send-email', { to: 'user@test.com' }, { cron: '0 8 * * *' })
1241
756
  ```
1242
757
 
1243
758
  ---
1244
759
 
1245
- # GraphQL
760
+ ## iii — Worker / Function / Trigger
1246
761
 
1247
- Dynamic GraphQL schema generated per-request based on the authenticated tenant's tables.
762
+ Optional module for organizing service logic as Worker + Function + Trigger, plus a pure WebSocket SDK for remote workers.
1248
763
 
1249
- ```graphql
1250
- type Article {
1251
- id: ID!
1252
- title: String!
1253
- content: String
1254
- status: String
1255
- comments(limit: Int, offset: Int): [Comment!]!
1256
- }
764
+ ```ts
765
+ import { iii, createWorker, registerWorker } from 'weifuwu'
1257
766
 
1258
- type Query {
1259
- articles(limit: Int, offset: Int): [Article!]!
1260
- getArticle(id: ID!): Article
1261
- }
767
+ const engine = iii({ pg, redis })
768
+ app.use('/iii', engine.router())
1262
769
 
1263
- type Mutation {
1264
- createArticle(data: CreateArticleInput!): Article!
1265
- updateArticle(id: ID!, data: PatchArticleInput!): Article!
1266
- deleteArticle(id: ID!): Boolean!
1267
- }
770
+ const w = createWorker('orders')
771
+ w.registerFunction('orders::create', async (payload) => {
772
+ return db.query('INSERT INTO orders ...', [payload.items])
773
+ })
774
+ engine.addWorker(w)
775
+
776
+ // Invoke
777
+ await engine.trigger({ function_id: 'orders::create', payload: { items: ['apple'] } })
1268
778
  ```
1269
779
 
1270
- Built with `graphql-js` native constructors (`GraphQLObjectType`), no SDL generation, no `makeExecutableSchema`.
780
+ ### Built-in stream functions
1271
781
 
1272
- ---
782
+ | Function | Description |
783
+ |----------|-------------|
784
+ | `stream::set(stream_name, group_id, item_id, data)` | Write + persist + notify |
785
+ | `stream::get(stream_name, group_id, item_id)` | Read single item |
786
+ | `stream::delete(stream_name, group_id, item_id)` | Delete + notify |
787
+ | `stream::list(stream_name, group_id)` | List items in a group |
788
+ | `stream::list_groups(stream_name)` | List groups in a stream |
789
+ | `stream::list_all()` | List all streams |
790
+ | `stream::send(stream_name, group_id, type, data, id?)` | Push event without persisting |
791
+
792
+ ### Storage backends
793
+
794
+ | Config | Persistence | Broadcast |
795
+ |--------|-------------|-----------|
796
+ | `iii({})` | In-memory | — |
797
+ | `iii({ pg })` | PG table | — |
798
+ | `iii({ redis })` | Redis Hash | Redis pub/sub |
799
+ | `iii({ pg, redis })` | PG table | Redis pub/sub |
800
+
801
+ ### Trigger actions
802
+
803
+ | Action | Behavior |
804
+ |--------|----------|
805
+ | `'sync'` (default) | Wait for result |
806
+ | `'void'` | Fire-and-forget |
807
+
808
+ ### REST API
809
+
810
+ | Path | Description |
811
+ |------|-------------|
812
+ | `GET /iii/workers` | List connected workers |
813
+ | `GET /iii/functions` | List registered functions |
814
+ | `GET /iii/triggers` | List registered triggers |
815
+ | `POST /iii/trigger/:fnId` | Invoke a function |
816
+ | `WS /iii/worker` | Remote worker connection |
1273
817
 
1274
- # Tenant BaaS
818
+ ---
1275
819
 
1276
- Built-in multi-tenant backend-as-a-service — define tables at runtime via API, get RESTful CRUD + GraphQL automatically, with row-level tenant isolation.
820
+ ## Multi-tenant BaaS
1277
821
 
1278
822
  ```ts
1279
- import { serve, Router, postgres, user, tenant } from 'weifuwu'
823
+ import { tenant } from 'weifuwu'
1280
824
 
1281
- const pg = postgres()
1282
- const u = user({ pg, jwtSecret: process.env.JWT_SECRET! })
1283
825
  const t = tenant({ pg, usersTable: '_users' })
826
+ await t.migrate()
1284
827
 
1285
- await pg.migrate()
1286
- await u.migrate()
1287
- await t.migrate() // creates _tenants, _tenant_members, _user_tables
1288
-
1289
- const app = new Router()
1290
- app.use('/auth', u.router())
1291
- app.use('/api', u.middleware()) // → ctx.user
1292
828
  app.use('/api', t.middleware()) // → ctx.tenant
1293
- app.use('/api', t.router()) // → management + data CRUD
829
+ app.use('/api', t.router()) // → dynamic CRUD
1294
830
  app.use('/graphql', t.graphql()) // → dynamic GraphQL
1295
831
  ```
1296
832
 
1297
- ## System tables
1298
-
1299
- | Table | Purpose |
1300
- |-------|---------|
1301
- | `_tenants` | Tenant records (`id TEXT PK DEFAULT gen_random_uuid()`, `name`, `created_at`) |
1302
- | `_tenant_members` | User-tenant membership (`tenant_id`, `user_id`, `role`) |
1303
- | `_user_tables` | Dynamic table definitions (`tenant_id`, `slug`, `fields JSONB`) |
1304
-
1305
- ## Dynamic table API
1306
-
1307
- Create a table at runtime:
833
+ ### Dynamic table API
1308
834
 
1309
835
  ```json
1310
836
  POST /api/tables
1311
- {
1312
- "slug": "articles",
1313
- "fields": [
1314
- { "name": "title", "type": "string", "required": true },
1315
- { "name": "content", "type": "text" },
1316
- { "name": "status", "type": "enum", "options": ["draft", "published"], "default": "draft" },
1317
- { "name": "views", "type": "integer", "default": 0 },
1318
- { "name": "embedding", "type": "vector", "dimensions": 1536, "index": "hnsw" }
1319
- ]
1320
- }
837
+ { "slug": "articles", "fields": [
838
+ { "name": "title", "type": "string", "required": true },
839
+ { "name": "views", "type": "integer", "default": 0 }
840
+ ]}
1321
841
  ```
1322
842
 
1323
- ## Field types
1324
-
1325
- | type | PostgreSQL | Index support |
1326
- |------|-----------|---------------|
1327
- | `string` | `TEXT` | `true`, `unique` |
1328
- | `integer` | `INTEGER` | `true`, `desc`, `unique` |
1329
- | `float` | `DOUBLE PRECISION` | `true`, `desc` |
1330
- | `boolean` | `BOOLEAN` | `true` |
1331
- | `text` | `TEXT` | `true` |
1332
- | `datetime` | `TIMESTAMPTZ` | `true`, `desc` |
1333
- | `date` | `DATE` | `true`, `desc` |
1334
- | `enum` | `TEXT` (with validation) | `true` |
1335
- | `json` | `JSONB` | `gin` |
1336
- | `vector` | `vector(n)` (pgvector) | `hnsw` (HNSW, vector_cosine_ops) |
1337
-
1338
- ## RESTful API
1339
-
1340
- All routes require `ctx.tenant` (set by `t.middleware()`). All queries automatically filter by `tenant_id`.
1341
-
1342
- | Route | Method | Description |
1343
- |-------|--------|-------------|
1344
- | `/sys/tenants` | POST | Create tenant, caller becomes admin |
1345
- | `/sys/tenants` | GET | List user's tenants |
1346
- | `/sys/tenants/invite` | POST | Invite user by email (admin) |
1347
- | `/sys/tenants/members/:userId` | DELETE | Remove member (admin) |
1348
- | `/sys/tables` | POST/GET | Create / list dynamic tables |
1349
- | `/sys/tables/:slug` | GET/PATCH/DELETE | Get schema / add fields / drop table |
1350
- | `/:slug` | GET | List rows (limit, offset, sort) |
1351
- | `/:slug` | POST | Create row |
1352
- | `/:slug/:id` | GET/PATCH/DELETE | Get / update / delete row |
1353
- | `/:slug/:id/:_nested` | GET | List related rows (has_many / M2M) |
1354
- | `/:slug/:id/:_nested` | POST | Create related row (auto-fills relation field) |
843
+ Field types: `string`, `integer`, `float`, `boolean`, `text`, `datetime`, `date`, `enum`, `json`, `vector`.
844
+
845
+ ### REST API
846
+
847
+ | Method | Path | Description |
848
+ |--------|------|-------------|
849
+ | GET/POST | `/sys/tables` | List / create dynamic tables |
850
+ | GET/POST/PATCH/DELETE | `/:slug[/:id]` | Dynamic CRUD |
851
+ | POST/POST | `/:slug/:id/:nested` | Related rows |
1355
852
 
1356
853
  ---
1357
854
 
1358
- # Messager
855
+ ## Messager
1359
856
 
1360
857
  Real-time chat with channels, WebSocket, and agent routing.
1361
858
 
1362
859
  ```ts
1363
860
  import { messager, agent, redis } from 'weifuwu'
1364
861
 
1365
- const agents = agent({ pg })
1366
-
1367
- // Single process (no cross-process broadcast):
1368
- // const msg = messager({ pg, agents })
1369
- // Multi-process (Redis pub/sub broadcast):
1370
862
  const msg = messager({ pg, agents, redis: redis() })
1371
-
1372
863
  await msg.migrate()
1373
- app.use('/api', msg.router())
1374
864
  app.ws('/ws', u.middleware(), msg.wsHandler())
1375
865
  ```
1376
866
 
1377
- ## Channels
1378
-
1379
- ```http
1380
- POST /channels name, type (channel|dm), members
1381
- GET /channels
1382
- GET /channels/:id
1383
- ```
1384
-
1385
- ## Messages
867
+ ### Channels & Messages
1386
868
 
1387
869
  ```http
1388
- GET /channels/:id/messages ?limit=50&before={id}
1389
- POST /channels/:id/messages content, sender_type, type
1390
- POST /channels/:id/read last_message_id
870
+ POST /api/channels { name, type, members }
871
+ POST /api/channels/:id/messages { content }
872
+ GET /api/channels/:id/messages
1391
873
  ```
1392
874
 
1393
- ## WebSocket
875
+ ### WebSocket events
1394
876
 
1395
877
  ```json
1396
- { "type": "message", "channel_id": 1, "content": "Hi" }
1397
- { "type": "typing", "channel_id": 1, "is_typing": true }
1398
- { "type": "read", "channel_id": 1, "last_message_id": 42 }
878
+ { "type": "message", "channel_id": 1, "content": "Hi" }
879
+ { "type": "typing", "channel_id": 1, "is_typing": true }
880
+ { "type": "read", "channel_id": 1, "last_message_id": 42 }
1399
881
  ```
1400
882
 
1401
- ## Programmatic send
883
+ ### Programmatic send
1402
884
 
1403
885
  ```ts
1404
886
  await msg.send(channelId, 'System message', { sender_type: 'system' })
@@ -1406,369 +888,214 @@ await msg.send(channelId, 'System message', { sender_type: 'system' })
1406
888
 
1407
889
  ---
1408
890
 
1409
- # LogDB — Structured Event Logging
891
+ ## LogDB
1410
892
 
1411
- PostgreSQL-backed structured logging with monthly partitioning, metadata search, and built-in REST API.
893
+ PostgreSQL-backed structured event logging with monthly partitioning.
1412
894
 
1413
895
  ```ts
1414
- import { serve, Router, logdb, postgres } from 'weifuwu'
896
+ import { logdb } from 'weifuwu'
1415
897
 
1416
- const pg = postgres()
1417
898
  const logger = logdb({ pg })
1418
-
1419
- await logger.migrate() // create table + partitions
1420
- app.use('/logs', logger.router()) // mount REST API
1421
- ```
1422
-
1423
- ## Module API
1424
-
1425
- ```ts
1426
- const logger = logdb({
1427
- pg: PostgresClient,
1428
- table?: string // default: '_log_entries'
1429
- })
899
+ await logger.migrate()
900
+ app.use('/logs', logger.router())
1430
901
  ```
1431
902
 
1432
- | Method | Returns | Description |
1433
- |--------|---------|-------------|
1434
- | `log(input)` | `LogEntry` | Insert a log entry programmatically |
1435
- | `router()` | `Router` | REST API routes: `POST /`, `GET /`, `GET /:id` |
1436
- | `migrate()` | `Promise<void>` | Create partitioned table + month partitions |
1437
- | `clean(n)` | `Promise<number>` | Drop partitions older than `n` months |
1438
- | `close()` | `Promise<void>` | Close database connection |
1439
-
1440
- ## Log entries
1441
-
1442
- ```ts
1443
- interface LogEntryInput {
1444
- level: string // info, warn, error, debug
1445
- source: string // api, ui, system, or custom
1446
- message: string
1447
- metadata?: Record<string, unknown>
1448
- }
1449
- ```
1450
-
1451
- ## REST API
903
+ ### REST API
1452
904
 
1453
905
  | Method | Path | Description |
1454
906
  |--------|------|-------------|
1455
- | `POST /` | Create a log entry | Returns `LogEntry` with status 201 |
1456
- | `GET /` | Query log entries | Returns `{ entries: LogEntry[], total: number }` |
1457
- | `GET /:id` | Get single entry | Returns `LogEntry` or 404 |
1458
-
1459
- ### Query parameters (`GET /`)
1460
-
1461
- | Param | Example | Description |
1462
- |-------|---------|-------------|
1463
- | `level` | `?level=error` | Filter by level (exact match) |
1464
- | `source` | `?source=api` | Filter by source (exact match) |
1465
- | `after` | `?after=2026-01-01` | Entries on or after this timestamp |
1466
- | `before` | `?before=2026-03-01` | Entries before this timestamp |
1467
- | `meta.*` | `?meta.service=auth&meta.env=prod` | Filter by metadata key/value |
1468
- | `limit` | `?limit=20` | Page size (default: 50) |
1469
- | `offset` | `?offset=40` | Page offset (default: 0) |
1470
-
1471
- ## Partitioning
1472
-
1473
- Logs are stored in a PostgreSQL range-partitioned table by `created_at`. Partitions are pre-created for the current month + 12 months ahead. This keeps each partition small, enables partition-pruning for time-range queries, and allows instant retention via `DROP TABLE`.
907
+ | `POST` | `/` | Create log entry |
908
+ | `GET` | `/` | Query entries (supports `?level=`, `?source=`, `?after=`, `?before=`, `?meta.*=`) |
909
+ | `GET` | `/:id` | Get single entry |
1474
910
 
1475
911
  ### Retention
1476
912
 
1477
913
  ```ts
1478
- // Drop all partitions older than 12 months
1479
- const dropped = await logger.clean(12)
1480
- console.log(`Dropped ${dropped} old partitions`)
914
+ await logger.clean(12) // Drop partitions older than 12 months
1481
915
  ```
1482
916
 
1483
- The `migrate()` method creates the parent table and pre-creates partitions. The `log()` method checks for the current month's partition and creates it if missing — safe across month boundaries without re-running migration.
1484
-
1485
917
  ---
1486
918
 
1487
- # Opencode
1488
-
1489
- AI programming assistant — chat with LLM agents that have access to filesystem tools, skills, and isolated session workspaces.
919
+ ## SEO
1490
920
 
1491
921
  ```ts
1492
- import { serve, Router, postgres, opencode } from 'weifuwu'
922
+ import { seo, seoMiddleware, seoTags } from 'weifuwu'
1493
923
 
1494
- const app = new Router()
1495
- const pg = postgres()
1496
- const oc = await opencode({ pg, permissions: { ... } })
924
+ app.use(seo({
925
+ baseUrl: 'https://example.com',
926
+ robots: [{ userAgent: '*', allow: '/', disallow: ['/admin'] }],
927
+ sitemap: {
928
+ urls: [{ loc: '/', changefreq: 'daily', priority: 1.0 }],
929
+ async resolve() { /* dynamic URLs */ },
930
+ cacheTTL: 3_600_000,
931
+ },
932
+ }))
1497
933
 
1498
- await oc.migrate()
1499
- app.use('/opencode', await oc.router())
1500
- app.ws('/opencode', oc.wsHandler())
934
+ // Middleware: X-Robots-Tag header
935
+ app.use(seoMiddleware({ headers: { 'X-Robots-Tag': (path) => path.startsWith('/admin') ? 'noindex' : undefined } }))
1501
936
 
1502
- serve(app.handler(), { port: 3000, websocket: app.websocketHandler() })
937
+ // Tag generator for SSR
938
+ const tags = seoTags({ title: 'My Page', description: '...', ogImage: '/og.png', canonical: 'https://...' })
1503
939
  ```
1504
940
 
1505
- ### Tools
1506
-
1507
- | Tool | Description |
1508
- |------|-------------|
1509
- | `bash` | Execute shell commands in the workspace |
1510
- | `read` | Read files with offset/limit |
1511
- | `write` | Create or overwrite files |
1512
- | `edit` | Exact string replacements |
1513
- | `grep` | Regex content search |
1514
- | `glob` | Glob pattern file search |
1515
- | `web` | Fetch URL content |
1516
- | `question` | Ask the user for input |
1517
- | `skill` | Load a skill on demand |
1518
-
1519
- ### Permissions
1520
-
1521
- Control tool access per conversation:
1522
-
1523
- ```ts
1524
- const oc = await opencode({
1525
- pg,
1526
- permissions: {
1527
- bash: { allow: true },
1528
- read: { allow: true },
1529
- write: { allow: false },
1530
- edit: { allow: false },
1531
- skill: { '*': { allow: true }, 'internal-*': { allow: false } },
1532
- },
1533
- })
1534
- ```
941
+ | Endpoint | Description |
942
+ |----------|-------------|
943
+ | `GET /robots.txt` | Generated robots.txt |
944
+ | `GET /sitemap.xml` | Generated XML sitemap (cached) |
1535
945
 
1536
946
  ---
1537
947
 
1538
- # SEO
948
+ ## Opencode
1539
949
 
1540
- Built-in SEO module`robots.txt`, `sitemap.xml`, indexing headers, and meta tag utilities.
950
+ AI programming assistantchat with LLM agents that have filesystem access.
1541
951
 
1542
952
  ```ts
1543
- import { seo, seoMiddleware, seoTags } from 'weifuwu'
953
+ import { opencode } from 'weifuwu'
1544
954
 
1545
- const app = new Router()
1546
-
1547
- // robots.txt + sitemap.xml
1548
- app.use(seo({
1549
- baseUrl: 'https://example.com',
1550
- robots: [
1551
- { userAgent: '*', allow: '/', disallow: ['/admin', '/api'] },
1552
- ],
1553
- sitemap: {
1554
- urls: [
1555
- { loc: '/', changefreq: 'daily', priority: 1.0 },
1556
- { loc: '/about', changefreq: 'monthly', priority: 0.8 },
1557
- ],
1558
- // Dynamic URLs from database
1559
- async resolve() {
1560
- const articles = await db.query('SELECT slug, updated_at FROM articles')
1561
- return articles.map(a => ({
1562
- loc: `/blog/${a.slug}`,
1563
- lastmod: a.updated_at,
1564
- }))
1565
- },
1566
- cacheTTL: 3_600_000, // re-generate every hour (default)
1567
- },
1568
- }))
955
+ const oc = await opencode({ pg, permissions: { bash: { allow: true }, write: { allow: false } } })
956
+ await oc.migrate()
957
+ app.use('/opencode', await oc.router())
958
+ app.ws('/opencode', oc.wsHandler())
1569
959
  ```
1570
960
 
1571
- ### Endpoints
961
+ ---
1572
962
 
1573
- | Path | Description |
1574
- |------|-------------|
1575
- | `GET /robots.txt` | Generated robots.txt with optional Sitemap reference |
1576
- | `GET /sitemap.xml` | Generated XML sitemap with caching |
963
+ ## Deploy
1577
964
 
1578
- ### seoMiddleware Indexing control
965
+ Self-hosted PaaS: multi-app proxy, zero-downtime updates, auto SSL.
1579
966
 
1580
967
  ```ts
1581
- // Global block all paths
1582
- app.use(seoMiddleware({ headers: { 'X-Robots-Tag': 'noindex' } }))
968
+ import { deploy, defineConfig } from 'weifuwu'
1583
969
 
1584
- // Per-path via function
1585
- app.use(seoMiddleware({
1586
- headers: {
1587
- 'X-Robots-Tag': (path) => path.startsWith('/admin') ? 'noindex' : undefined,
1588
- },
1589
- }))
970
+ const config = defineConfig({
971
+ apps: [{ name: 'api', dir: './api', domain: 'api.example.com', port: 3001 }],
972
+ })
973
+ await deploy(config)
1590
974
  ```
1591
975
 
1592
- ### seoTags — Meta / OG / Twitter Card
976
+ ---
1593
977
 
1594
- Generate SEO meta tags for SSR pages:
978
+ ## Health check
1595
979
 
1596
980
  ```ts
1597
- const tags = seoTags({
1598
- title: 'My Page',
1599
- description: 'A great page about things',
1600
- ogImage: 'https://example.com/og.png',
1601
- twitterCard: 'summary_large_image',
1602
- canonical: 'https://example.com/page',
1603
- })
1604
- // → <title>My Page</title>
1605
- // → <meta property="og:title" content="My Page">
1606
- // → <meta name="twitter:card" content="summary_large_image">
1607
- // → <link rel="canonical" href="https://example.com/page">
1608
- // ...
1609
- ```
981
+ import { health } from 'weifuwu'
1610
982
 
1611
- Use in `layout.tsx` or `page.tsx` with `tsx()`:
983
+ app.use(health()) // GET /health 200
1612
984
 
1613
- ```tsx
1614
- export default function RootLayout({ children }) {
1615
- return (
1616
- <html>
1617
- <head>{seoTags({ title: 'My App' })}</head>
1618
- <body>{children}</body>
1619
- </html>
1620
- )
1621
- }
985
+ // Custom checks
986
+ app.use(health({
987
+ checks: { db: async () => { await pg.sql`SELECT 1`; return { ok: true } } },
988
+ }))
1622
989
  ```
1623
990
 
1624
- # Security
991
+ ---
1625
992
 
1626
- ## Helmet — Security headers
993
+ ## Internationalization
1627
994
 
1628
995
  ```ts
1629
- import { helmet } from 'weifuwu'
1630
-
1631
- // Apply all security headers with safe defaults
1632
- app.use(helmet())
996
+ import { i18n } from 'weifuwu'
1633
997
 
1634
- // Customize individual headers (any can be set to false to remove)
1635
- app.use(helmet({
1636
- contentSecurityPolicy: "default-src 'self'",
1637
- xFrameOptions: 'DENY',
1638
- strictTransportSecurity: 'max-age=63072000; includeSubDomains; preload',
1639
- }))
998
+ app.use(i18n({ dir: './locales', defaultLocale: 'en' }))
1640
999
 
1641
- // Middleware-order: set after helmet to override
1642
- app.use(helmet({ xFrameOptions: false })) // remove a header
1000
+ // In handlers: ctx.t('greeting') "Hello"
1001
+ // In layout: ctx.locale "en"
1643
1002
  ```
1644
1003
 
1645
- 13 security headers set by default:
1646
-
1647
- | Header | Default |
1648
- |--------|---------|
1649
- | `Content-Security-Policy` | `default-src 'self'; ...` |
1650
- | `X-Content-Type-Options` | `nosniff` |
1651
- | `X-Frame-Options` | `SAMEORIGIN` |
1652
- | `Strict-Transport-Security` | `max-age=15552000; includeSubDomains` |
1653
- | `X-XSS-Protection` | `0` |
1654
- | `Referrer-Policy` | `no-referrer` |
1655
- | `Permissions-Policy` | `camera=(),geolocation=(),...` |
1656
- | `Cross-Origin-Embedder-Policy` | `require-corp` |
1657
- | `Cross-Origin-Opener-Policy` | `same-origin` |
1658
- | `Cross-Origin-Resource-Policy` | `same-origin` |
1659
- | `Origin-Agent-Cluster` | `?1` |
1660
- | `X-DNS-Prefetch-Control` | `off` |
1661
- | `X-Download-Options` | `noopen` |
1662
- | `X-Permitted-Cross-Domain-Policies` | `none` |
1004
+ Locale detection: `Accept-Language` header browser preference. Falls back to `defaultLocale`.
1663
1005
 
1664
- Does not override response headers already set by the application — your explicit headers take precedence.
1006
+ ---
1665
1007
 
1666
- ## Request ID
1008
+ ## Email
1667
1009
 
1668
1010
  ```ts
1669
- import { requestId } from 'weifuwu'
1670
-
1671
- // Every response gets X-Request-ID
1672
- app.use(requestId())
1673
-
1674
- // Custom header name
1675
- app.use(requestId({ header: 'X-Trace-Id' }))
1676
-
1677
- // Custom ID generator
1678
- app.use(requestId({ generator: () => crypto.randomUUID() }))
1011
+ import { mailer } from 'weifuwu'
1679
1012
 
1680
- // Access the ID in handlers via ctx.requestId
1681
- app.get('/log', (req, ctx) => {
1682
- console.log(`Handling request ${ctx.requestId}`)
1683
- return Response.json({ id: ctx.requestId })
1684
- })
1013
+ const mail = mailer({ host: 'smtp.example.com', port: 587, auth: { user: 'user', pass: 'pass' } })
1014
+ await mail.send({ to: 'user@test.com', subject: 'Hello', text: 'Body', html: '<p>Body</p>' })
1685
1015
  ```
1686
1016
 
1687
- Preserves incoming `X-Request-ID` for distributed tracing — if the upstream service already set it, the value is reused and propagated.
1017
+ ---
1688
1018
 
1689
1019
  ## Server-Sent Events
1690
1020
 
1691
1021
  ```ts
1692
- import { createSSEStream, formatSSE } from 'weifuwu'
1022
+ import { createSSEStream, formatSSE, formatSSEData } from 'weifuwu'
1693
1023
 
1694
- async function* eventStream() {
1695
- yield { type: 'ping', data: { time: Date.now() } }
1696
- yield { type: 'message', data: { text: 'hello' } }
1024
+ async function* events() {
1025
+ yield formatSSE('chat', 'Hello')
1026
+ yield formatSSE('chat', 'World')
1697
1027
  }
1698
1028
 
1699
- app.get('/events', () => createSSEStream(eventStream()))
1029
+ app.get('/stream', (req, ctx) => createSSEStream(events()))
1700
1030
  ```
1701
1031
 
1032
+ ---
1033
+
1034
+ ## Utility functions
1035
+
1702
1036
  | Function | Description |
1703
1037
  |----------|-------------|
1704
- | `createSSEStream(iterable, opts?)` | Returns a `Response` with `Content-Type: text/event-stream` |
1705
- | `formatSSE(event, data)` | Formats an SSE event string (`event: ...\ndata: ...\n\n`) |
1706
- | `formatSSEData(data)` | Formats SSE data-only string (`data: ...\n\n`) |
1707
-
1708
- # Health, i18n, Email & Testing
1038
+ | `loadEnv(path?)` | Load `.env` into `process.env` |
1039
+ | `serveStatic(root, options?)` | Static file serving |
1040
+ | `getCookies(req)` | Parse cookies from request |
1041
+ | `setCookie(res, name, value, opts?)` | Set cookie on response |
1042
+ | `deleteCookie(res, name, opts?)` | Delete cookie from response |
1043
+ | `createTestServer(handler)` | One-line test server → `{ server, url }` |
1044
+ | `createSSEStream(iterable, opts?)` | SSE response from AsyncIterable |
1045
+ | `formatSSE(event, data)` | Format SSE event string |
1046
+ | `formatSSEData(data)` | Format SSE data string |
1047
+ | `runWorkflow(options)` | DAG execution engine as AI SDK Tool |
1048
+ | `useWebsocket(url, opts?)` | React auto-reconnecting WebSocket hook |
1049
+ | `useAction(url, opts?)` | React async form submission hook |
1050
+ | `navigate(href)` | Client-side page navigation |
1051
+ | `useNavigate()` | React hook returning navigate function |
1052
+ | `csrf(options?)` | CSRF protection middleware |
1053
+ | `seoTags(config)` | Generate `<title>`, `<meta>`, OG, Twitter Card tags |
1054
+ | `createHub(options?)` | WebSocket channel hub |
1709
1055
 
1710
- ## Health check
1056
+ ### AI SDK re-exports
1711
1057
 
1712
1058
  ```ts
1713
- import { serve, Router, health } from 'weifuwu'
1714
-
1715
- const app = new Router()
1716
- app.use(health()) // GET /health → 200
1717
- app.use(health({ path: '/healthz' })) // custom path
1718
- app.use(health({
1719
- check: async () => { await db.sql`SELECT 1` }, // fail → 503
1720
- }))
1721
- serve(app.handler(), { port: 3000 })
1059
+ streamText, generateText, streamObject, generateObject,
1060
+ tool, embed, embedMany, smoothStream,
1061
+ openai, createOpenAI
1722
1062
  ```
1723
1063
 
1724
- Returns a `Router` — mount with `app.use()`.
1725
-
1726
- ## Internationalization
1064
+ ### pgTable helpers
1727
1065
 
1728
1066
  ```ts
1729
- import { i18n } from 'weifuwu'
1730
-
1731
- app.use(i18n({ dir: './locales', defaultLocale: 'en' }))
1732
-
1733
- // In any handler after i18n middleware:
1734
- app.get('/hello', (req, ctx) => {
1735
- const msg = ctx.t('greeting', { name: 'World' })
1736
- return Response.json({ message: msg, locale: ctx.locale })
1737
- })
1067
+ pgTable(name, columns), pg.table(name, columns),
1068
+ serial, uuid, text, integer, boolean, timestamptz, jsonb, textArray, vector, timestamps,
1069
+ eq, ne, gt, gte, lt, lte, isNull, isNotNull, like, contains, in_, and, or, not,
1070
+ PgModule
1738
1071
  ```
1739
1072
 
1740
- Locale detection: `Cookie: locale=zh` → `Accept-Language: zh-CN` → `defaultLocale`.
1073
+ ---
1741
1074
 
1742
- ## Email
1075
+ ## Testing
1743
1076
 
1744
1077
  ```ts
1745
- import { mailer } from 'weifuwu'
1746
-
1747
- // SMTP transport
1748
- const mail = mailer({
1749
- transport: 'smtp://user:pass@smtp.example.com',
1750
- from: 'noreply@example.com',
1751
- })
1752
- await mail.send({ to: 'user@example.com', subject: 'Welcome', html: '<h1>Hi!</h1>' })
1753
- await mail.close()
1078
+ import { describe, it } from 'node:test'
1079
+ import assert from 'node:assert/strict'
1080
+ import { Router } from 'weifuwu'
1754
1081
 
1755
- // Custom transport (Resend, SES, SendGrid, etc.)
1756
- const mail2 = mailer({
1757
- send: async (msg) => { await resend.emails.send(msg) },
1082
+ describe('hello', () => {
1083
+ it('returns 200', async () => {
1084
+ const r = new Router()
1085
+ r.get('/', () => new Response('ok'))
1086
+ const res = await r.handler()(new Request('http://localhost/'), {} as any)
1087
+ assert.equal(res.status, 200)
1088
+ })
1758
1089
  })
1759
- await mail2.send({ to: 'user@example.com', subject: 'Hi', text: 'Hello' })
1760
- await mail2.close()
1761
1090
  ```
1762
1091
 
1763
- ## Test utilities
1092
+ For end-to-end tests:
1764
1093
 
1765
1094
  ```ts
1766
1095
  import { createTestServer } from 'weifuwu'
1767
1096
 
1768
- const { server, url } = await createTestServer(app.handler())
1769
- const res = await fetch(`${url}/api/users`)
1770
- assert.equal(res.status, 200)
1771
- server.stop()
1097
+ const { server, url } = await createTestServer(handler)
1098
+ const res = await fetch(`${url}/api/ping`)
1772
1099
  ```
1773
1100
 
1774
1101
  ---