weifuwu 0.11.0 → 0.13.0

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
@@ -1,3 +1,8 @@
1
+ ---
2
+ name: weifuwu
3
+ description: Web-standard HTTP framework for Node.js — (req, ctx) => Response
4
+ ---
5
+
1
6
  # weifuwu
2
7
 
3
8
  **Web-standard HTTP framework for Node.js.** `(req, ctx) => Response` — no framework-specific objects, just the Web API your browser already speaks.
@@ -11,26 +16,22 @@ Everything follows the same `(req, ctx) => Response` contract. The Router handle
11
16
  ## Features
12
17
 
13
18
  - **Web Standard** — `Request` / `Response` / `ReadableStream`, zero abstractions
14
- - **Trie router** — static > param > wildcard, sub-router mounting, path params
15
- - **Middleware** — global, path-scoped, route-level — onion model, short-circuit
16
- - **Middleware modules** — `auth()`, `cors()`, `logger()`, `rateLimit()`, `compress()`, `validate()`, `upload()`
17
- - **React SSR + Hydration** — `tsx({ dir })` page.tsx / load.ts / layout.tsx / route.ts / not-found.tsx
18
- - **WebSocket** — `router.ws()` with upgrade middleware (auth before connect)
19
- - **GraphQL** — `graphql(handler)` sub-Router with GraphiQL IDE
20
- - **AI streaming** — `ai(handler)` sub-Router via Vercel AI SDK
21
- - **DAG workflow tool** — `runWorkflow()` multi-step execution engine as a single AI SDK `Tool`
22
- - **AI Agent** — `agent()` server-side AI agents with chat/tool-use/knowledge types, OpenAI-compatible, Ollama-ready
23
- - **Messaging** — `messager()` real-time chat with channels, WebSocket, agent routing, webhook support
24
- - **Tenant BaaS** — `tenant()` — multi-tenant dynamic tables, auto REST + GraphQL, row-level isolation, pgvector/HNSW
25
- - **Redis** — `redis()` ioredis client, `ctx.redis`, middleware
26
- - **Queue** — `queue()` Redis-backed job queue with immediate, delayed, and cron scheduling
27
- - **Auth** — `user()` register/login/JWT + OAuth2 Server (authorization code + PKCE + client_credentials)
28
- - **Static files** — `serveStatic()` with ETag, 304, MIME, directory index
29
- - **Cookie** — `getCookies()`, `setCookie()`, `deleteCookie()` immutable
30
- - **Error handling** — global `onError()`
31
- - **Deploy** — `deploy()` — self-hosted PaaS: multi-app reverse proxy, subdomain routing, zero-downtime updates, auto SSL, Git-based deployment
32
- - **Zero build** — native TypeScript in Node.js v24+
33
- - **Zero deps** (core) — only `node:http` and `node:stream`
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, 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
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
+ - **i18n** — locale detection, JSON translations, `ctx.t()`
32
+ - **Email** — SMTP or custom transport
33
+ - **Health check** — configurable `/health` endpoint
34
+ - **Test utilities** — `createTestServer()` — one-line test server setup
34
35
 
35
36
  ## Quick start
36
37
 
@@ -41,1376 +42,111 @@ import { serve } from 'weifuwu'
41
42
  serve((req, ctx) => new Response('Hello, World!'), { port: 3000 })
42
43
  ```
43
44
 
44
- ### React + Tailwind
45
-
46
- ```bash
47
- npm install weifuwu
48
- mkdir -p ui/pages
49
- ```
50
-
51
- ```ts
52
- // app.ts
53
- import { serve, Router } from 'weifuwu'
54
-
55
- const app = new Router()
56
- app.use('/', await tsx({ dir: './ui/' }))
57
- serve(app.handler(), { port: 3000, websocket: app.websocketHandler() })
58
- ```
59
-
60
- ```tsx
61
- // ui/pages/page.tsx
62
- export default function Home() {
63
- return <h1 className="text-3xl font-bold text-blue-600">Hello</h1>
64
- }
65
- ```
66
-
67
- ```bash
68
- node app.ts
69
- ```
70
-
71
- Open http://localhost:3000 — Tailwind CSS is compiled automatically, pages hot-reload on save.
72
-
73
- ## Router
74
-
75
- ```ts
76
- import { serve, Router } from 'weifuwu'
77
-
78
- const app = new Router()
79
- .use((req, ctx, next) => {
80
- console.log(`${req.method} ${new URL(req.url).pathname}`)
81
- return next(req, ctx)
82
- })
83
- .get('/hello/:name', (req, ctx) =>
84
- Response.json({ message: `Hello, ${ctx.params.name}!` }),
85
- )
86
- .post('/data', async (req, ctx) => {
87
- const body = await req.json()
88
- return Response.json(body, { status: 201 })
89
- })
90
-
91
- serve(app.handler(), { port: 3000 })
92
- ```
93
-
94
- ## Built-in middleware
95
-
96
- ### Auth
45
+ ### Full app
97
46
 
98
47
  ```ts
99
- import { auth } from 'weifuwu'
100
-
101
- // Static bearer token
102
- app.use(auth({ token: 'sk-123' }))
103
-
104
- // Custom verify (JWT, DB, etc.) — return object to set ctx.user
105
- app.use(auth({
106
- verify: async (token) => {
107
- const user = await db.findUserByToken(token)
108
- return user ? { sub: user.id, role: user.role } : null
109
- },
110
- }))
111
-
112
- // Proxy validation to external auth service
113
- app.get('/protected', auth({ proxy: 'http://auth:3000/validate' }), handler)
114
-
115
- // Custom header
116
- app.use(auth({ header: 'X-API-Key', token: 'my-key' }))
117
- ```
118
-
119
- ### CORS
120
-
121
- ```ts
122
- import { cors } from 'weifuwu'
123
-
124
- app.use(cors()) // allow all
125
- app.use(cors({ origin: ['https://example.com'] })) // whitelist
126
- app.use(cors({ origin: (o) => o.endsWith('.trusted.com') ? o : false }))
127
- app.use(cors({ credentials: true, maxAge: 3600 }))
128
- ```
129
-
130
- ### Logger
131
-
132
- ```ts
133
- import { logger } from 'weifuwu'
134
-
135
- app.use(logger()) // GET /hello 200 5ms
136
- app.use(logger({ format: 'combined' })) // with query params
137
- ```
138
-
139
- ### Rate limit
140
-
141
- ```ts
142
- import { rateLimit } from 'weifuwu'
143
-
144
- app.use(rateLimit({ max: 100, window: 60_000 })) // 100 req/min
145
- app.get('/api', rateLimit({ max: 10 }), handler) // per-route
146
-
147
- // Custom key (by API key, user ID, etc.)
148
- app.use(rateLimit({
149
- max: 1000,
150
- key: (req) => req.headers.get('x-api-key') ?? 'anonymous',
151
- }))
152
- ```
153
-
154
- ### Compression
155
-
156
- ```ts
157
- import { compress } from 'weifuwu'
158
-
159
- app.use(compress()) // brotli > gzip > deflate
160
- app.use(compress({ threshold: 2048 })) // only compress > 2KB
161
- ```
162
-
163
- ## Validation
164
-
165
- ```ts
166
- import { z } from 'zod'
167
- import { validate } from 'weifuwu'
168
-
169
- const CreateUser = z.object({
170
- name: z.string().min(1),
171
- email: z.string().email(),
172
- })
173
-
174
- router.post('/users',
175
- validate({ body: CreateUser }),
176
- (req, ctx) => {
177
- // ctx.parsed.body — typed & validated
178
- },
179
- )
180
- ```
181
-
182
- ## File upload
183
-
184
- ```ts
185
- import { upload } from 'weifuwu'
186
-
187
- router.post('/upload',
188
- upload({ dir: './uploads', maxFileSize: 10_485_760 }),
189
- (req, ctx) => {
190
- // ctx.parsed.files.avatar → { name, type, size, path }
191
- // ctx.parsed.fields.title → 'hello'
192
- },
193
- )
194
- ```
195
-
196
- ## Cookie
197
-
198
- ```ts
199
- import { getCookies, setCookie, deleteCookie } from 'weifuwu'
200
-
201
- // Read
202
- const cookies = getCookies(req) // { session: 'abc' }
203
-
204
- // Set (immutable — returns new Response)
205
- let res = new Response('ok')
206
- res = setCookie(res, 'session', 'token', { httpOnly: true, secure: true, maxAge: 3600 })
207
-
208
- // Delete
209
- res = deleteCookie(res, 'session')
210
- ```
211
-
212
- ## Static files
213
-
214
- ```ts
215
- import { serveStatic } from 'weifuwu'
216
-
217
- router.get('/static/*', serveStatic('./public'))
218
- ```
219
-
220
- Features: MIME type detection (20+ types), ETag + If-None-Match (304), directory index (index.html), path traversal protection, Cache-Control.
221
-
222
- ## PostgreSQL
223
-
224
- Built-in PostgreSQL client — connection management, type-safe DDL, transactions, and module lifecycle.
225
-
226
- ```ts
227
- import { serve, Router, postgres } from 'weifuwu'
228
-
229
- const app = new Router()
230
- const pg = postgres() // reads DATABASE_URL
231
- app.use(pg) // injects ctx.sql into handlers
232
- ```
233
-
234
- ### Type-safe DDL with schema builder
235
-
236
- Define tables declaratively with type inference — no raw SQL for common operations, no Zod needed:
237
-
238
- ```ts
239
- import { pgTable, serial, uuid, text, integer, boolean, timestamptz, jsonb, sql } from 'weifuwu'
240
-
241
- const users = pgTable('_users', {
242
- id: serial('id').primaryKey(),
243
- name: text('name').notNull(),
244
- email: text('email').unique().notNull(),
245
- age: integer('age'),
246
- active: boolean('active').default(true),
247
- createdAt: timestamptz('created_at').default(sql`NOW()`),
248
- metadata: jsonb<{ role: string }>('metadata'),
249
- })
250
- ```
251
-
252
- Supports 10 column types:
253
- | Builder | DDL | TS Type |
254
- |---------|-----|---------|
255
- | `serial()` | `SERIAL` | `number` |
256
- | `uuid()` | `UUID` | `string` |
257
- | `text()` | `TEXT` | `string` |
258
- | `integer()` | `INTEGER` | `number` |
259
- | `boolean()` | `BOOLEAN` | `boolean` |
260
- | `timestamptz()` | `TIMESTAMPTZ` | `string` |
261
- | `jsonb<T>()` | `JSONB` | `T` |
262
- | `textArray()` | `TEXT[]` | `string[]` |
263
- | `vector(name, dims)` | `vector(N)` | `number[]` |
264
-
265
- Column constraints chainable: `.primaryKey()`, `.notNull()`, `.nullable()`, `.default(value | sql\`...\`)`, `.unique()`, `.references(table, column?, onDelete?)`.
266
-
267
- ### DDL execution
268
-
269
- ```ts
270
- await users.create() // CREATE TABLE IF NOT EXISTS
271
- await users.createIndex('email') // CREATE INDEX
272
- await users.createUniqueIndex('slug') // CREATE UNIQUE INDEX
273
- await users.createIndex('created_at', { desc: true })
274
- await users.createIndex(['a', 'b']) // multi-column
275
- await users.createIndex('embedding', { // pgvector HNSW
276
- type: 'hnsw', operator: 'vector_cosine_ops',
277
- })
278
- await users.drop({ cascade: true })
279
- ```
280
-
281
- ### Type-safe CRUD with BoundTable
282
-
283
- Two usage paths — use `pg.table()` when you have a `pg` handle, or `pgTable()` with explicit `sql`:
284
-
285
- ```ts
286
- // pg.table() — auto-binds sql, no need to pass it
287
- const users = pg.table('_users', {
288
- id: serial('id').primaryKey(),
289
- name: text('name').notNull(),
290
- email: text('email').unique(),
291
- active: boolean('active').default(true),
292
- createdAt: timestamptz('created_at').default(sql`NOW()`),
293
- })
294
-
295
- // INSERT ... RETURNING * — auto-strips serial id
296
- const user = await users.insert({ name: 'Alice', email: 'alice@test.com' })
297
- // → { id: 1, name: 'Alice', email: 'alice@test.com', active: true, ... }
298
-
299
- // SELECT ... WHERE id = ? LIMIT 1
300
- const found = await users.findById(1)
301
-
302
- // SELECT ... WHERE ... [ORDER BY ...] [LIMIT ...] [OFFSET ...]
303
- const admins = await users.find({ role: 'admin' })
304
- const sorted = await users.find({ active: true }, { orderBy: { name: 'asc' } })
305
- const page = await users.find(undefined, { limit: 10, offset: 0 })
306
- const filtered = await users.find({ role: 'admin' }, { orderBy: { name: 'desc' }, limit: 5 })
307
-
308
- // UPDATE ... SET ... WHERE ... RETURNING *
309
- const updated = await users.update({ id: 1 }, { name: 'Bob' })
310
- // With SQL expressions:
311
- await users.update({ id: 1 }, { name: 'Bob', updated_at: sql`NOW()` })
312
-
313
- // DELETE ... WHERE ... RETURNING 1
314
- const ok = await users.delete({ id: 1 })
315
- ```
316
-
317
- When using `pgTable()` directly (without `pg`), pass `sql` as the first argument:
318
-
319
- ```ts
320
- const t = pgTable('_users', { ... })
321
- await t.insert(ctx.sql, { name: 'Alice' })
322
- await t.find(ctx.sql, { role: 'admin' }, { orderBy: { name: 'asc' } })
323
- ```
324
-
325
- ### Complex queries use raw SQL
326
-
327
- ```ts
328
- app.get('/users/stats', async (req, ctx) => {
329
- const rows = await ctx.sql`
330
- SELECT u.*, count(p.id) as posts
331
- FROM ${users} u LEFT JOIN posts p ON p.user_id = u.id
332
- GROUP BY u.id
333
- `
334
- return Response.json(rows)
335
- })
336
- ```
337
-
338
- ### Transactions
339
-
340
- ```ts
341
- const result = await pg.transaction(async (tx) => {
342
- const [user] = await tx`INSERT INTO "_users" (...) VALUES (...) RETURNING *`
343
- const [wallet] = await tx`INSERT INTO "_wallets" ("user_id") VALUES (${user.id}) RETURNING *`
344
- return { user, wallet }
345
- })
346
- ```
347
-
348
- ### Connection lifecycle
349
-
350
- ```ts
351
- const pg = postgres() // reads DATABASE_URL
352
- const pg = postgres('postgres://...') // explicit connection
353
- const pg = postgres({
354
- connection: 'postgres://...',
355
- max: 10, // pool size
356
- ssl: { rejectUnauthorized: false }, // SSL options
357
- idle_timeout: 30, // idle timeout (s)
358
- connect_timeout: 10, // connection timeout (s)
359
- closeTimeout: 5, // close grace period (s)
360
- signal: ac.signal, // abort → sql.end()
361
- })
362
- await pg.close()
363
- ```
364
-
365
- ### Module base class
366
-
367
- Every database module (`opencode`, `messager`, `tenant`, `agent`, `user`) extends `PgModule`:
368
-
369
- ```ts
370
- import { PgModule } from 'weifuwu'
371
-
372
- class MyModule extends PgModule {
373
- constructor(pg: PostgresClient) {
374
- super(pg) // sets this.sql = pg.sql
375
- }
376
- async migrate() { /* override */ }
377
- // close() inherited — calls pg.close() automatically
378
- }
379
- ```
380
-
381
- ## Opencode
382
-
383
- AI programming assistant — chat with LLM agents that have access to filesystem tools, skills, and isolated session workspaces.
384
-
385
- ```ts
386
- import { serve, Router, postgres, opencode } from 'weifuwu'
387
-
388
- const app = new Router()
389
- const pg = postgres()
390
- const oc = await opencode({ pg, permissions: { ... } })
391
-
392
- await oc.migrate()
393
- app.use('/opencode', await oc.router())
394
- app.ws('/opencode', oc.wsHandler())
395
-
396
- serve(app.handler(), { port: 3000, websocket: app.websocketHandler() })
397
- ```
398
-
399
- ### Session-isolated workspaces
400
-
401
- Each session gets its own sandbox directory — tools operate within it, files cannot escape:
402
-
403
- ```
404
- cwd/.sessions/opencode/1/ ← session 1's workspace
405
- cwd/.sessions/opencode/2/ ← session 2's workspace
406
- cwd/.sessions/chat/3/ ← different mount point
407
- ```
408
-
409
- Workspaces are computed from `cwd { ctx.mountPath } { sessionId }`. The system prompt shows the session's workspace so the LLM knows where it is.
410
-
411
- ### Tools
412
-
413
- | Tool | Description |
414
- |------|-------------|
415
- | `bash` | Execute shell commands in the workspace |
416
- | `read` | Read files with offset/limit |
417
- | `write` | Create or overwrite files |
418
- | `edit` | Exact string replacements |
419
- | `grep` | Regex content search |
420
- | `glob` | Glob pattern file search |
421
- | `web` | Fetch URL content |
422
- | `question` | Ask the user for input |
423
- | `skill` | Load a skill on demand |
424
-
425
- ### Skills
426
-
427
- Skills are discovered from filesystem and loaded on demand via the `skill` tool — no system prompt bloat:
428
-
429
- - Project: `.opencode/skills/{name}/SKILL.md`
430
- - Global: `~/.config/opencode/skills/{name}/SKILL.md`
431
- - Also reads: `.claude/skills/`, `.agents/skills/` (project + global)
432
-
433
- ```ts
434
- const oc = await opencode({
435
- pg,
436
- skills: [{ name: 'git', description: 'Git workflow', content: '...' }],
437
- })
438
- ```
439
-
440
- ### Permissions
441
-
442
- Control tool access per conversation:
443
-
444
- ```ts
445
- const oc = await opencode({
446
- pg,
447
- permissions: {
448
- bash: { allow: true },
449
- read: { allow: true },
450
- write: { allow: false },
451
- edit: { allow: false },
452
- skill: { '*': { allow: true }, 'internal-*': { allow: false } },
453
- },
454
- })
455
- ```
456
-
457
- ### Workspace isolation
458
-
459
- ```ts
460
- const oc = await opencode({ pg, permissions })
461
- // All sessions inherit the instance's workspace (default: process.cwd())
462
- // Sessions cannot override their workspace
463
- // Different mount points = different opencode() instances = isolated workspaces
464
- ```
465
-
466
- ```ts
467
- import { serve, Router, postgres, user } from 'weifuwu'
48
+ import { serve, Router, postgres, user, aiStream, graphql } from 'weifuwu'
49
+ import { openai } from '@ai-sdk/openai'
468
50
 
469
51
  const app = new Router()
470
52
  const pg = postgres()
471
- await pg.migrate()
472
53
 
54
+ // Auth
473
55
  const auth = user({ pg, jwtSecret: process.env.JWT_SECRET! })
474
-
475
- // POST /auth/register { email, password, name }
476
- // POST /auth/login { email, password }
477
- // GET /auth/oauth/authorize?client_id=...&redirect_uri=...&response_type=code
478
- // POST /auth/oauth/consent
479
- // POST /auth/oauth/token (grant_type=authorization_code|client_credentials)
56
+ await auth.migrate()
480
57
  app.use('/auth', auth.router())
481
58
 
482
- // Protected routes — verifies JWT, sets ctx.user
483
- app.get('/me', auth.middleware(), async (req, ctx) => {
484
- return Response.json(ctx.user)
485
- // { id, email, name, role }
486
- })
487
- ```
488
-
489
- 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()`.
490
-
491
- ### OAuth2 Server
492
-
493
- Enable OAuth2 Server to let third-party apps (SPA, mobile, microservices) authenticate users through your app.
494
-
495
- ```ts
496
- const auth = user({
497
- pg,
498
- jwtSecret: process.env.JWT_SECRET!,
499
- oauth2: { server: true },
500
- })
501
-
502
- await auth.migrate() // creates _users + _oauth2_clients + _oauth2_codes + _oauth2_tokens
503
-
504
- // Register a client app (programmatic — CLI, admin UI, seed script)
505
- const client = await auth.registerClient({
506
- name: 'My SPA',
507
- redirectUris: ['https://myapp.com/callback'],
508
- })
509
- // → { clientId, clientSecret, name, redirectUris }
510
-
511
- // Use auth middleware to protect routes — OAuth2 JWT tokens work seamlessly
512
- app.get('/api/data', auth.middleware(), handler)
513
- ```
514
-
515
- #### Supported Grant Types
516
-
517
- | Grant | Use Case | PKCE |
518
- |-------|----------|------|
519
- | `authorization_code` (with client_secret) | Server-side apps | Optional |
520
- | `authorization_code` (with `code_challenge`/`code_verifier`) | SPA / Mobile apps | Required |
521
- | `client_credentials` | Machine-to-machine | — |
522
-
523
- #### Flow (Authorization Code + PKCE)
524
-
525
- ```
526
- 1. 第三方 App 引导用户:
527
- GET /oauth/authorize?client_id=xxx&redirect_uri=https://app.com/cb
528
- &response_type=code&code_challenge=S256&state=yyy
529
-
530
- 2. 用户未登录 → 302 到 /login?redirect=... → 登录后自动回到授权页
531
-
532
- 3. 用户确认授权 → POST /oauth/consent { approve: true, client_id, ... }
533
- 302 redirect_uri?code=xxx&state=yyy
534
-
535
- 4. 第三方 App POST /oauth/token
536
- { grant_type: authorization_code, code, client_id, client_secret,
537
- redirect_uri, code_verifier }
538
- → { access_token, token_type: "Bearer", expires_in, refresh_token }
539
-
540
- 5. access_token 是标准 JWT,auth.middleware() 和 auth.verify() 直接可用
541
- ```
542
-
543
- #### Client Management
544
-
545
- ```ts
546
- const client = await auth.registerClient({ name, redirectUris })
547
- const found = await auth.getClient(client.clientId)
548
- await auth.revokeClient(client.clientId)
549
- ```
550
-
551
- #### Using OAuth2 Tokens with the Built-in Auth Middleware
552
-
553
- OAuth2 Server 签发的 `access_token` 与密码登录的 JWT 使用同一 `jwtSecret`,payload 向下兼容(`sub`、`email`、`role`),所以 `auth()` 无需任何修改即可验证 OAuth2 签发的 token:
554
-
555
- ```ts
556
- import { auth } from 'weifuwu'
557
-
558
- // 同一个 auth() 中间件同时支持密码登录 JWT 和 OAuth2 JWT
559
- app.get('/api', auth({ verify: (token) => auth.verify(token) }), handler)
560
- ```
561
-
562
- For `client_credentials` tokens (machine-to-machine), `verify()` returns `null` since no user is associated.
563
-
564
- ## Tenant BaaS
565
-
566
- Built-in multi-tenant backend-as-a-service — define tables at runtime via API, get RESTful CRUD + GraphQL automatically, with row-level tenant isolation.
567
-
568
- ```ts
569
- import { serve, Router, postgres, user, tenant } from 'weifuwu'
570
-
571
- const pg = postgres()
572
- const u = user({ pg, jwtSecret: process.env.JWT_SECRET! })
573
- const t = tenant({ pg, usersTable: '_users' })
574
-
575
- await pg.migrate()
576
- await u.migrate()
577
- await t.migrate() // creates _tenants, _tenant_members, _user_tables
578
-
579
- const app = new Router()
580
- app.use('/auth', u.router())
581
- app.use('/api', u.middleware()) // → ctx.user
582
- app.use('/api', t.middleware()) // → ctx.tenant
583
- app.use('/api', t.router()) // → management + data CRUD
584
- app.use('/graphql', t.graphql()) // → dynamic GraphQL
585
- ```
586
-
587
- ### System tables
588
-
589
- | Table | Purpose |
590
- |-------|---------|
591
- | `_tenants` | Tenant records (`id TEXT PK DEFAULT gen_random_uuid()`, `name`, `created_at`) |
592
- | `_tenant_members` | User-tenant membership (`tenant_id`, `user_id`, `role`) |
593
- | `_user_tables` | Dynamic table definitions (`tenant_id`, `slug`, `fields JSONB`) |
594
-
595
- ### Dynamic table API
596
-
597
- Create a table at runtime:
598
-
599
- ```json
600
- POST /api/tables
601
- {
602
- "slug": "articles",
603
- "fields": [
604
- { "name": "title", "type": "string", "required": true },
605
- { "name": "content", "type": "text" },
606
- { "name": "status", "type": "enum", "options": ["draft", "published"], "default": "draft" },
607
- { "name": "views", "type": "integer", "default": 0 },
608
- { "name": "embedding", "type": "vector", "dimensions": 1536, "index": "hnsw" }
609
- ]
610
- }
611
- ```
612
-
613
- → Creates a PostgreSQL table with `id SERIAL PK`, `tenant_id TEXT NOT NULL`, and the specified columns, plus indexes. The table name is internally scoped to the tenant.
614
-
615
- ### Field types
616
-
617
- | type | PostgreSQL | Index support |
618
- |------|-----------|---------------|
619
- | `string` | `TEXT` | `true`, `unique` |
620
- | `integer` | `INTEGER` | `true`, `desc`, `unique` |
621
- | `float` | `DOUBLE PRECISION` | `true`, `desc` |
622
- | `boolean` | `BOOLEAN` | `true` |
623
- | `text` | `TEXT` | `true` |
624
- | `datetime` | `TIMESTAMPTZ` | `true`, `desc` |
625
- | `date` | `DATE` | `true`, `desc` |
626
- | `enum` | `TEXT` (with validation) | `true` |
627
- | `json` | `JSONB` | `gin` |
628
- | `vector` | `vector(n)` (pgvector) | `hnsw` (HNSW, vector_cosine_ops) |
629
-
630
- ### Relationships
631
-
632
- Declare a foreign key via the `relation` field:
633
-
634
- ```json
635
- { "name": "article_id", "type": "integer", "relation": { "table": "articles", "onDelete": "cascade" } }
636
- ```
637
-
638
- Supported relationship patterns:
639
-
640
- | Pattern | Detection | REST | GraphQL |
641
- |---------|-----------|------|---------|
642
- | **belongs_to** | Field with `relation` | — | `comment.article` resolver |
643
- | **has_many** | Another table has a relation pointing here | `GET /api/articles/:id/comments` | `article.comments` resolver |
644
- | **M2M** | Junction table with exactly two relation fields | `GET /api/articles/:id/tags` (bypasses junction) | `article.tags` / `tag.articles` resolver |
645
- | **Self-ref** | Relation field pointing to same table | — | With depth control |
646
-
647
- ### RESTful API
648
-
649
- All routes require `ctx.tenant` (set by `t.middleware()`). All queries automatically filter by `tenant_id`.
650
-
651
- | Route | Method | Description |
652
- |-------|--------|-------------|
653
- | `/sys/tenants` | POST | Create tenant, caller becomes admin |
654
- | `/sys/tenants` | GET | List user's tenants |
655
- | `/sys/tenants/invite` | POST | Invite user by email (admin) |
656
- | `/sys/tenants/members/:userId` | DELETE | Remove member (admin) |
657
- | `/sys/tables` | POST/GET | Create / list dynamic tables |
658
- | `/sys/tables/:slug` | GET/PATCH/DELETE | Get schema / add fields / drop table |
659
- | `/:slug` | GET | List rows (limit, offset, sort) |
660
- | `/:slug` | POST | Create row |
661
- | `/:slug/:id` | GET/PATCH/DELETE | Get / update / delete row |
662
- | `/:slug/:id/:_nested` | GET | List related rows (has_many / M2M) |
663
- | `/:slug/:id/:_nested` | POST | Create related row (auto-fills relation field) |
664
-
665
- ### Vector search
666
-
667
- ```http
668
- GET /api/articles?search_vector=[0.1,0.2,...]&search_field=embedding&search_limit=10
669
- ```
670
-
671
- Returns rows ordered by cosine distance (`<=>`), includes `_distance` field. Supports `l2` (`<->`) and `ip` (`<#>`):
672
-
673
- ```http
674
- GET /api/articles?search_vector=[...]&search_field=embedding&search_distance=l2
675
- ```
676
-
677
- ### GraphQL
678
-
679
- Dynamic GraphQL schema generated per-request based on the authenticated tenant's tables:
680
-
681
- ```graphql
682
- type Article {
683
- id: ID!
684
- title: String!
685
- content: String
686
- status: String
687
- comments(limit: Int, offset: Int): [Comment!]!
688
- }
689
-
690
- type Query {
691
- articles(limit: Int, offset: Int): [Article!]!
692
- getArticle(id: ID!): Article
693
- }
694
-
695
- type Mutation {
696
- createArticle(data: CreateArticleInput!): Article!
697
- updateArticle(id: ID!, data: PatchArticleInput!): Article!
698
- deleteArticle(id: ID!): Boolean!
699
- }
700
- ```
701
-
702
- Built with `graphql-js` native constructors (`GraphQLObjectType`), no SDL generation, no `makeExecutableSchema`.
703
-
704
- ### Middleware
705
-
706
- `t.middleware()` extracts the tenant context:
707
-
708
- 1. Requires `ctx.user` (from `u.middleware()`)
709
- 2. Looks up user's tenant memberships
710
- 3. Single tenant → automatically set `ctx.tenant`
711
- 4. Multiple tenants → require `X-Tenant-ID` header, return 300 with tenant list if missing
712
- 5. No tenants → 403
713
-
714
- ### Tenant lifecycle
715
-
716
- ```ts
717
- const t = tenant({ pg, usersTable: '_users' })
718
-
719
- // Create a tenant — the caller becomes admin
720
- const tenant = await (await fetch('http://localhost/api/sys/tenants', {
721
- method: 'POST',
722
- headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer <jwt>' },
723
- body: JSON.stringify({ name: 'Acme Corp' }),
724
- })).json()
725
- // → { id: "uuid", name: "Acme Corp", created_at: "..." }
726
-
727
- // Invite a member
728
- await fetch('http://localhost/api/sys/tenants/invite', {
729
- method: 'POST',
730
- headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer <jwt>' },
731
- body: JSON.stringify({ email: 'colleague@acme.com', role: 'member' }),
732
- })
733
- ```
734
-
735
- ## AI Agent
736
-
737
- 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.
738
-
739
- ```ts
740
- import { agent } from 'weifuwu'
741
-
742
- const agents = agent({ pg })
743
-
744
- await agents.migrate()
745
- app.use('/api', agents.router())
746
- ```
747
-
748
- | Type | Description | Execution |
749
- |------|-------------|-----------|
750
- | `chat` | Pure conversation | `streamText()` / `generateText()` |
751
- | `tool-use` | Tool-calling agent | `streamText({ tools })` |
752
-
753
- ### Knowledge (RAG)
754
-
755
- Add documents to any agent — `searchKnowledge` tool auto-injected:
756
-
757
- ```ts
758
- await agents.addKnowledge(agentId, 'Title', 'Document content...')
759
- // The agent automatically calls searchKnowledge when answering
760
- ```
761
-
762
- ### Streaming
763
-
764
- ```http
765
- POST /agents/:id/run { input: "hello", stream: true }
766
- → event-stream (fullStream SSE: text-delta, tool-call, tool-result, finish)
767
- ```
768
-
769
- ### Programmatic API
770
-
771
- ```ts
772
- const result = await agents.run(agentId, { input: 'hello', stream: false })
773
- // { output: "Hello!", elapsed: 1234 }
774
- ```
775
-
776
- ## Messager
777
-
778
- Real-time chat with channels, WebSocket, and agent routing.
779
-
780
- ```ts
781
- import { messager, agent } from 'weifuwu'
782
-
783
- const agents = agent({ pg })
784
- const msg = messager({ pg, agents })
785
-
786
- await msg.migrate()
787
- app.use('/api', msg.router())
788
- app.ws('/ws', u.middleware(), msg.wsHandler())
789
- ```
790
-
791
- ### Channels
792
-
793
- ```http
794
- POST /channels name, type (channel|dm), members
795
- GET /channels
796
- GET /channels/:id
797
- ```
798
-
799
- ### Messages
800
-
801
- ```http
802
- GET /channels/:id/messages ?limit=50&before={id}
803
- POST /channels/:id/messages content, sender_type, type
804
- POST /channels/:id/read last_message_id
805
- ```
806
-
807
- ### WebSocket
808
-
809
- ```json
810
- { "type": "message", "channel_id": 1, "content": "Hi" }
811
- { "type": "typing", "channel_id": 1, "is_typing": true }
812
- { "type": "read", "channel_id": 1, "last_message_id": 42 }
813
- ```
814
-
815
- ### Programmatic send
816
-
817
- ```ts
818
- await msg.send(channelId, 'System message', { sender_type: 'system' })
819
- ```
820
-
821
- ## WebSocket
822
- message(ws, ctx, data) {
823
- ws.send(`echo: ${data}`)
824
- },
825
- close(ws, ctx) {
826
- console.log('disconnected')
827
- },
828
- })
829
-
830
- serve(app.handler(), { port: 3000, websocket: app.websocketHandler() })
831
- ```
832
-
833
- Middleware runs **before** WebSocket upgrade — you can reject connections with HTTP status codes:
834
-
835
- ```ts
836
- app.ws('/secure',
837
- (req, _ctx, next) => {
838
- const auth = req.headers.get('Authorization')
839
- if (!auth) return Response.json({ error: 'Unauthorized' }, { status: 401 })
840
- return next(req, _ctx)
841
- },
842
- { open(ws) { ws.send('authorized') } },
843
- )
844
- ```
845
-
846
- ## GraphQL
847
-
848
- GraphQL endpoint with GraphiQL IDE. Mount as a sub-Router:
849
-
850
- ```ts
851
- import { serve, Router, graphql } from 'weifuwu'
852
-
853
- const app = new Router()
854
- app.use('/graphql', graphql(() => ({
855
- schema: `
856
- type Query { hello: String }
857
- type Mutation { setMessage(msg: String!): String }
858
- `,
859
- resolvers: {
860
- Query: { hello: () => 'world' },
861
- Mutation: { setMessage: (_, { msg }) => msg },
862
- },
863
- graphiql: true,
864
- })))
865
-
866
- serve(app.handler(), { port: 3000 })
867
- ```
868
-
869
- The handler receives `(req, ctx)` so you can customize the schema based on the request.
870
-
871
- ## AI streaming
872
-
873
- Server-sent event streaming via the Vercel AI SDK:
874
-
875
- ```ts
876
- import { serve, Router, ai } from 'weifuwu'
877
- import { openai } from '@ai-sdk/openai'
878
-
879
- const app = new Router()
880
- app.use('/chat', ai(async (req, ctx) => {
881
- const { messages } = await req.json()
882
- return { model: openai('gpt-4o'), messages }
883
- }))
884
-
885
- serve(app.handler(), { port: 3000 })
886
- ```
887
-
888
- ## runWorkflow
889
-
890
- 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.
891
-
892
- ```ts
893
- import { tool, streamText } from 'ai'
894
- import { runWorkflow } from 'weifuwu'
895
- import { z } from 'zod'
896
-
897
- const tools = {
898
- queryUser: tool({
899
- description: 'Query user info',
900
- inputSchema: z.object({ userId: z.string() }),
901
- execute: async ({ userId }) => ({ id: userId, email: 'user@test.com', name: 'Test' }),
902
- }),
903
- sendEmail: tool({
904
- description: 'Send an email',
905
- inputSchema: z.object({ to: z.string(), subject: z.string() }),
906
- execute: async ({ to, subject }) => ({ sent: true }),
907
- }),
908
- runWF: runWorkflow({ tools: { queryUser, sendEmail } }),
909
- }
910
-
911
- // Use in any streamText call — the LLM can decide when to trigger a workflow
912
- const result = await streamText({
913
- model,
914
- tools,
915
- messages: [{ role: 'user', content: '查询用户123,如果存在则发送欢迎邮件' }],
916
- })
917
- ```
918
-
919
- ### Node types
920
-
921
- 7 built-in node types for defining the execution graph:
922
-
923
- | Node | Purpose | Input |
924
- |------|---------|-------|
925
- | `call` | Call a registered AI SDK Tool | `{ tool: "name", args: {...} }` |
926
- | `set` | Assign a variable | `{ name: "x", value: 42 }` |
927
- | `get` | Read a variable | `{ name: "x" }` |
928
- | `eval` | Evaluate an expression | `{ expression: "$var.x + 1" }` |
929
- | `if` | Conditional branch | `{ conditions: [{ test: ..., body: [nodes] }] }` |
930
- | `while` | Loop | `{ condition: "$var.i < 5" }, body: [nodes]` |
931
- | `http` | HTTP request | `{ url: "https://...", method: "GET" }` |
932
-
933
- ### Reference syntax
934
-
935
- | Pattern | Meaning | Example |
936
- |---------|---------|---------|
937
- | `$var.x` | Variable `x` | `$var.counter` |
938
- | `$nodes.u.output` | Full output of node `u` | `$nodes.u.output` |
939
- | `$nodes.u.output.field` | Specific field | `$nodes.u.output.email` |
940
- | `$input.userId` | Input param | `$input.userId` |
941
-
942
- ### LLM generation
943
-
944
- Pass a `model` to `runWorkflow` — the LLM generates the workflow JSON from a goal:
945
-
946
- ```ts
947
- const runWF = runWorkflow({
948
- tools: { queryUser, sendEmail },
59
+ // AI streaming
60
+ const chat = await aiStream(async (req) => ({
949
61
  model: openai('gpt-4o'),
950
- })
951
-
952
- const result = await streamText({
953
- model,
954
- tools: { runWF },
955
- })
956
- ```
957
-
958
- The LLM calls `runWF` with a goal, and `runWorkflow` internally calls `generateText` to produce the workflow nodes, then executes them.
959
-
960
- ## React pages with tsx()
961
-
962
- ```ts
963
- import { serve, Router } from 'weifuwu'
964
- import { tsx } from 'weifuwu/tsx'
965
-
966
- const app = new Router()
967
- app.use('/', await tsx({ dir: './ui/' }))
968
-
969
- serve(app.handler(), { port: 3000, websocket: app.websocketHandler() })
970
- ```
971
-
972
- ### Directory structure
973
-
974
- ```
975
- ui/
976
- ├── pages/ ← 页面文件
977
- │ ├── page.tsx → GET / (React component, default export)
978
- │ ├── layout.tsx → root layout (HTML shell, receives req/ctx, NOT hydrated)
979
- │ ├── not-found.tsx → 404 error page (rendered for unmatched routes, wrapped in layout)
980
- │ ├── about/page.tsx → GET /about
981
- │ ├── blog/[slug]/
982
- │ │ ├── page.tsx → GET /blog/:slug
983
- │ │ ├── load.ts → data fetching (server-only, default export)
984
- │ │ └── route.ts → POST /blog/:slug (API, named exports POST/PUT/DELETE/...)
985
- │ ├── blog/layout.tsx → /blog/* layout (UI structure, receives children, hydrated)
986
- │ └── api/search/
987
- │ └── route.ts → GET /api/search (standalone API, no page.tsx needed)
988
- └── components/ ← 组件文件(会被热更自动感知)
989
- └── button.tsx
990
- ```
991
-
992
- ### Development mode
993
-
994
- tsx() runs in development mode automatically when `NODE_ENV !== 'production'`:
995
-
996
- - **File watching** — chokidar watches the `dir` directory for `.tsx`/`.ts` changes
997
- - Page files in `pages/` → single-file recompilation + registry update
998
- - Component files in `components/` → full rebuild of all pages
999
- - New files are detected automatically
1000
- - **Live reload** — Compiled via esbuild `write: false` + `vm.Script.runInContext` (no disk writes, no `node --watch` conflict)
1001
- - **WebSocket auto-refresh** — `/__weifuwu/livereload` endpoint pushes reload signals; browser refreshes automatically
1002
- - **`node --watch` compatible** — External files (`app.ts`, `middleware/`) handled by `--watch` restart; `ui/` changes handled by tsx() without conflict
1003
-
1004
- ```bash
1005
- node app.ts # development (auto-reload + live refresh)
1006
- NODE_ENV=production node app.ts # production
1007
- ```
1008
-
1009
- ### Tailwind CSS
1010
-
1011
- 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:
1012
-
1013
- ```css
1014
- @import "tailwindcss";
1015
- ```
1016
-
1017
- Write `className` directly in your components — no CLI, no configuration:
1018
-
1019
- ```tsx
1020
- export default function Home() {
1021
- return <h1 className="text-3xl font-bold text-blue-600">Hello</h1>
1022
- }
1023
- ```
1024
-
1025
- In development mode, Tailwind is reprocessed whenever a `.tsx` file changes (new class names are picked up automatically).
1026
-
1027
- ### `@` alias
1028
-
1029
- 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):
1030
-
1031
- ```json
1032
- {
1033
- "compilerOptions": {
1034
- "paths": {
1035
- "@/*": ["./ui/*"]
1036
- }
1037
- }
1038
- }
1039
- ```
1040
-
1041
- This enables imports like `@/components/button` or `@/lib/utils` in both server-rendered and client-hydrated code.
1042
-
1043
- ### shadcn/ui
1044
-
1045
- tsx() works with [shadcn/ui](https://ui.shadcn.com) out of the box. The `@` alias and Tailwind CSS are handled automatically.
1046
-
1047
- ```bash
1048
- # 1. Install shadcn CLI and init (select "other" framework)
1049
- npx shadcn@latest init
1050
-
1051
- # 2. When prompted, configure:
1052
- # - Style: your preference
1053
- # - Base color: your preference
1054
- # - CSS file path: ui/app.css
1055
- # - Import alias: @/ → ./ui/
1056
- # - React hooks: yes
1057
- ```
62
+ messages: (await req.json()).messages,
63
+ }))
64
+ app.use('/chat', chat.router())
1058
65
 
1059
- ```json
1060
- // tsconfig.json (generated by shadcn init)
1061
- {
1062
- "compilerOptions": {
1063
- "paths": {
1064
- "@/*": ["./ui/*"]
1065
- }
1066
- }
1067
- }
1068
- ```
66
+ // GraphQL
67
+ const gql = graphql(() => ({
68
+ schema: `type Query { hello: String }`,
69
+ resolvers: { Query: { hello: () => 'world' } },
70
+ }))
71
+ app.use('/graphql', gql.router())
1069
72
 
1070
- Add components:
73
+ // Static files
74
+ app.get('/static/*', serveStatic('./public'))
1071
75
 
1072
- ```bash
1073
- npx shadcn@latest add button card dialog
76
+ serve(app.handler(), { port: 3000 })
1074
77
  ```
1075
78
 
1076
- Use them in your pages:
1077
-
1078
- ```tsx
1079
- // ui/pages/page.tsx
1080
- import { Button } from '@/components/ui/button'
1081
-
1082
- export default function Home() {
1083
- return <Button variant="outline">Click me</Button>
1084
- }
1085
79
  ```
1086
-
1087
- ```bash
1088
80
  node app.ts
1089
81
  ```
1090
82
 
1091
- ### Backward compatibility
1092
-
1093
- `tsx({ dir: './pages/' })` still works. When there is no `pages/` subdirectory under `dir`, the `dir` itself is used as the pages directory.
1094
-
1095
- ### page.tsx page component
1096
-
1097
- ```tsx
1098
- export default function Page({ params, query }: {
1099
- params: { slug: string }
1100
- query: Record<string, string>
1101
- }) {
1102
- return <article><h1>{params.slug}</h1></article>
1103
- }
1104
- ```
1105
-
1106
- ### load.ts data fetching (server-only)
1107
-
1108
- ```ts
1109
- export default async function load({ params, query }: {
1110
- params: Record<string, string>
1111
- query: Record<string, string>
1112
- }) {
1113
- const data = await db.query(params.slug)
1114
- return { data } // merged into props passed to page.tsx
1115
- }
1116
- ```
1117
-
1118
- ### layout.tsx
1119
-
1120
- **Root layout** (`pages/layout.tsx`) — receives `{ children, req, ctx }`:
1121
-
1122
- ```tsx
1123
- export default function RootLayout({ children, req, ctx }: {
1124
- children: React.ReactNode
1125
- req: Request
1126
- ctx: Context
1127
- }) {
1128
- return (
1129
- <html>
1130
- <head><title>App</title></head>
1131
- <body><div id="__weifuwu_root">{children}</div></body>
1132
- </html>
1133
- )
1134
- }
1135
- ```
1136
-
1137
- **Nested layouts** (`pages/blog/layout.tsx`) — receives only `{ children }`.
1138
-
1139
- ### route.ts — API (co-located with page)
1140
-
1141
- ```ts
1142
- export const POST: Handler = async (req, ctx) => {
1143
- const body = await req.json()
1144
- return Response.json({ ...body, slug: ctx.params.slug })
1145
- }
1146
- ```
1147
-
1148
- ### not-found.tsx — 404 page
1149
-
1150
- ```tsx
1151
- export default function NotFound() {
1152
- return <h1 class="text-4xl">404 – Not Found</h1>
1153
- }
1154
- ```
1155
-
1156
- ## Usage within a full app
1157
-
1158
- ```ts
1159
- import { serve, Router, ai, graphql } from 'weifuwu'
1160
-
1161
- const app = new Router()
1162
- app.use('/', await tsx({ dir: './pages/' }))
1163
- app.use('/chat', ai(async (req) => ({ model: openai('gpt-4o'), messages: (await req.json()).messages })))
1164
- app.use('/graphql', graphql(() => ({ schema: `type Query { hello: String }`, resolvers: { Query: { hello: () => 'world' } } })))
1165
- app.ws('/chat', { message(ws, _, data) { ws.send(data) } })
1166
-
1167
- serve(app.handler(), { websocket: app.websocketHandler() })
1168
- ```
1169
-
1170
- ```bash
1171
- node app.ts # development (auto-reload + live refresh)
1172
- NODE_ENV=production node app.ts # production
1173
- ```
1174
-
1175
- No build step, no configuration file — just Node.js.
1176
-
1177
- ## Graceful shutdown
1178
-
1179
- ```ts
1180
- import { serve } from 'weifuwu'
1181
- import type { Server } from 'weifuwu'
1182
-
1183
- const ac = new AbortController()
1184
- let server: Server
1185
-
1186
- process.on('SIGTERM', () => {
1187
- ac.abort()
1188
- server.stop()
1189
- })
1190
-
1191
- server = serve((req, ctx) => new Response('Hello'), {
1192
- port: 3000,
1193
- signal: ac.signal,
1194
- })
1195
- await server.ready
1196
- ```
1197
-
1198
- ### Using with WebSocket
1199
-
1200
- ```ts
1201
- const app = new Router().ws('/chat', { … })
1202
- const server = serve(app.handler(), {
1203
- port: 3000,
1204
- signal: ac.signal,
1205
- websocket: app.websocketHandler(),
1206
- })
1207
- ```
1208
-
1209
- ## Error handling
1210
-
1211
- ```ts
1212
- const app = new Router()
1213
- .onError((err, req, ctx) =>
1214
- Response.json({ error: err.message }, { status: 500 }),
1215
- )
1216
- .get('/crash', () => { throw new Error('boom') })
1217
- ```
1218
-
1219
- ## Deploy
1220
-
1221
- See [deploy.md](./deploy.md) for complete documentation — VPS setup, subdomain routing, blue-green zero-downtime, WebSocket bridge, Git webhook, auto SSL, and management API.
1222
-
1223
- Quick start on a fresh VPS:
1224
-
1225
- ```bash
1226
- # 1. Install Node.js
1227
- curl -fsSL https://deb.nodesource.com/setup_24.x | bash -
1228
- apt-get install -y nodejs git
1229
-
1230
- # 2. Create deploy project
1231
- mkdir -p /opt/deploy && cd /opt/deploy
1232
- npm init -y && npm install weifuwu
1233
-
1234
- # 3. Write deploy.ts
1235
- cat > deploy.ts << 'EOF'
1236
- import { deploy, defineConfig } from 'weifuwu'
1237
- await deploy(defineConfig({
1238
- domain: 'example.com',
1239
- deployToken: process.env.DEPLOY_TOKEN,
1240
- apps: {
1241
- blog: {
1242
- repo: 'https://github.com/me/my-blog.git',
1243
- subdomain: 'blog',
1244
- entry: 'app.ts',
1245
- port: 3001,
1246
- },
1247
- },
1248
- }))
1249
- EOF
1250
-
1251
- # 4. Run
1252
- DEPLOY_TOKEN='my-secret' node deploy.ts
1253
- ```
1254
-
1255
- ## API
1256
-
1257
- ### `serve(handler, options?)`
1258
-
1259
- | Option | Default | Description |
1260
- |--------|---------|-------------|
1261
- | `port` | `0` | Listen port (`0` = random) |
1262
- | `hostname` | `'0.0.0.0'` | Bind address |
1263
- | `signal` | — | `AbortSignal` for graceful shutdown |
1264
- | `websocket` | — | Upgrade handler from `router.websocketHandler()` |
1265
-
1266
- Returns `{ stop, port, hostname, ready }`.
1267
-
1268
- ### `user(options)`
1269
-
1270
- | Option | Default | Description |
1271
- |--------|---------|-------------|
1272
- | `pg` | — | PostgreSQL client from `postgres()` |
1273
- | `jwtSecret` | — | Secret key for JWT signing |
1274
- | `table` | `'_users'` | Users table name |
1275
- | `expiresIn` | `'24h'` | JWT expiration |
1276
- | `oauth2.server` | `false` | Enable OAuth2 Server |
1277
-
1278
- Returns `UserModule` — `{ router, middleware, migrate, register, login, verify, registerClient, getClient, revokeClient, close }`.
1279
-
1280
- ### `tenant(options)`
1281
-
1282
- | Option | Default | Description |
1283
- |--------|---------|-------------|
1284
- | `pg` | — | PostgreSQL client from `postgres()` |
1285
- | `usersTable` | — | Users table name (matching the `table` option passed to `user()`) |
1286
-
1287
- Returns `TenantModule` — `{ migrate, middleware, router, graphql, close }`.
1288
-
1289
- ### `agent(options)`
1290
-
1291
- | Option | Default | Description |
1292
- |--------|---------|-------------|
1293
- | `pg` | — | PostgreSQL client from `postgres()` |
1294
- | `model` | env `OPENAI_MODEL` → Ollama | `LanguageModel` from ai SDK |
1295
- | `embeddingModel` | env `OPENAI_EMBEDDING_MODEL` → Ollama | `EmbeddingModel` for knowledge RAG |
1296
- | `embeddingDimension` | `1024` | Vector dimension for pgvector |
1297
- | `tools` | — | Tools for tool-use agents (ai SDK `Tool` objects) |
1298
-
1299
- Returns `AgentModule` — `{ migrate, router, run, addKnowledge, close }`.
1300
-
1301
- ### `opencode(options)`
1302
-
1303
- | Option | Default | Description |
1304
- |--------|---------|-------------|
1305
- | `pg` | — | PostgreSQL client from `postgres()` |
1306
- | `workspace` | `process.cwd()` | Base directory for `.sessions` |
1307
- | `model` | `'deepseek-v4-flash'` | LLM model name |
1308
- | `baseURL` | env `DEEPSEEK_BASE_URL` | API base URL |
1309
- | `apiKey` | env `DEEPSEEK_API_KEY` | API key |
1310
- | `systemPrompt` | — | Custom system prompt |
1311
- | `skills` | `[]` | Static skill definitions |
1312
- | `permissions` | — | Tool permission config |
1313
-
1314
- Returns `OpencodeModule` — `{ migrate, router, wsHandler, close }`.
1315
-
1316
- ### `messager(options)`
1317
-
1318
- | Option | Default | Description |
1319
- |--------|---------|-------------|
1320
- | `pg` | — | PostgreSQL client from `postgres()` |
1321
- | `agents` | — | `AgentModule` instance (enables agent message routing) |
1322
-
1323
- Returns `MessagerModule` — `{ migrate, router, wsHandler, send, close }`.
1324
-
1325
- ### `tsx(options)`
1326
-
1327
- | Option | Default | Description |
1328
- |--------|---------|-------------|
1329
- | `dir` | — | UI directory path (containing `pages/` and optionally `components/`) |
1330
-
1331
- Returns `Promise<Router>`.
1332
-
1333
- Auto-detected features (no configuration needed):
1334
-
1335
- | Feature | Behavior |
1336
- |---------|----------|
1337
- | **File watching** | Enabled in dev mode. Watches `dir` for changes, recompiles on the fly, sends reload via WebSocket |
1338
- | **WebSocket live reload** | Endpoint at `/__weifuwu/livereload`. Browser auto-refreshes on file changes or server restart |
1339
- | **Tailwind CSS** | Auto-detected when `app.css` exists. Compiled through PostCSS + `@tailwindcss/postcss`. Served at `/__wfw/style.css`, auto-injected into HTML `<head>` |
1340
- | **`@` alias** | Read from `tsconfig.json` / `jsconfig.json` `compilerOptions.paths`. Passed to all esbuild builds |
1341
- | **Process state** | Dev mode keeps the process alive on file changes. DB connections, WebSockets, in-memory caches persist |
1342
-
1343
- To use WebSocket features, pass `router.websocketHandler()` to `serve()`:
1344
-
1345
- ```ts
1346
- serve(app.handler(), { websocket: app.websocketHandler() })
1347
- ```
1348
-
1349
- ### `Router`
1350
-
1351
- | Method | Description |
1352
- |--------|-------------|
1353
- | `get/post/put/delete/patch/head/options/all(path, ...mws, handler)` | Route registration |
1354
- | `use(mw)` / `use(path, mw)` / `use(path, subRouter)` | Middleware / sub-router |
1355
- | `ws(path, ...mws, handler)` | WebSocket route |
1356
- | `onError(handler)` | Global error handler |
1357
- | `handler()` | Returns `(req, ctx) => Response` for `serve()` |
1358
- | `websocketHandler()` | Returns upgrade handler for `serve({ websocket })` |
1359
-
1360
- ### Middleware modules
1361
-
1362
- | Import | Description |
1363
- |--------|-------------|
83
+ ## Documentation
84
+
85
+ | Module | Doc | Description |
86
+ |--------|-----|-------------|
87
+ | **Router** | [docs/router.md](./docs/router.md) | Routes, middleware, WebSocket, error handling |
88
+ | **Middleware** | [docs/middleware.md](./docs/middleware.md) | auth, cors, logger, rateLimit, compress, validate, upload, cookie, static |
89
+ | **PostgreSQL** | [docs/postgres.md](./docs/postgres.md) | Schema builder, CRUD, DDL, transactions, PgModule |
90
+ | **Auth & User** | [docs/user.md](./docs/user.md) | Password, JWT, OAuth2 Server, Social Login cookbook |
91
+ | **React SSR** | [docs/tsx.md](./docs/tsx.md) | pages, layouts, loaders, Tailwind, shadcn/ui |
92
+ | **AI** | [docs/ai.md](./docs/ai.md) | `aiStream()`, `runWorkflow()` |
93
+ | **AI Agent** | [docs/agent.md](./docs/agent.md) | Chat, tool-use, RAG knowledge |
94
+ | **Opencode** | [docs/opencode.md](./docs/opencode.md) | Programming assistant, skills, sessions, permissions |
95
+ | **Messager** | [docs/messager.md](./docs/messager.md) | Real-time chat, channels, WebSocket, agent routing |
96
+ | **GraphQL** | [docs/graphql.md](./docs/graphql.md) | GraphQL endpoint with GraphiQL |
97
+ | **Tenant BaaS** | [docs/tenant.md](./docs/tenant.md) | Dynamic tables, auto REST + GraphQL, row isolation |
98
+ | **Extra** | [docs/extra.md](./docs/extra.md) | Health check, i18n, email, test utilities |
99
+
100
+ ### Infrastructure
101
+
102
+ | Module | Import | What it gives you |
103
+ |--------|--------|-------------------|
104
+ | PostgreSQL | `postgres(options?)` | Connection pool + schema builder + CRUD + transactions |
105
+ | Redis | `redis(options?)` | ioredis client injected as `ctx.redis` |
106
+ | Queue | `queue(options?)` | Redis-backed job queue with cron scheduling |
107
+ | Deploy | `deploy(config)` | Self-hosted PaaS, see [deploy.md](./deploy.md) |
108
+
109
+ ### Mountable modules
110
+
111
+ All use the same pattern — `const m = module(options)` → `app.use('/path', m.router())`:
112
+
113
+ | Module | Purpose | Also provides |
114
+ |--------|---------|---------------|
115
+ | `user(options)` | Auth (password + JWT + OAuth2) | `migrate()`, `middleware()`, `register()`, `login()`, `verify()`, `close()` |
116
+ | `tenant(options)` | Multi-tenant BaaS | `migrate()`, `middleware()`, `graphql()`, `close()` |
117
+ | `agent(options)` | AI agents | `migrate()`, `run()`, `addKnowledge()`, `close()` |
118
+ | `opencode(options)` | Programming assistant | `migrate()`, `wsHandler()`, `close()` |
119
+ | `messager(options)` | Real-time messaging | `migrate()`, `wsHandler()`, `send()`, `close()` |
120
+ | `aiStream(handler)` | AI streaming endpoint | — |
121
+ | `graphql(handler)` | GraphQL endpoint | — |
122
+ | `health(options?)` | Health check | — |
123
+
124
+ ### Middleware (all `(req, ctx, next) => Response`)
125
+
126
+ | Middleware | Description |
127
+ |-----------|-------------|
1364
128
  | `auth(options)` | Bearer token / custom header / verify / proxy |
1365
129
  | `cors(options?)` | CORS with preflight, origin whitelist, credentials |
1366
130
  | `logger(options?)` | Request logging with duration |
1367
131
  | `rateLimit(options?)` | In-memory rate limiting with headers |
1368
132
  | `compress(options?)` | Brotli / Gzip / Deflate compression |
1369
- | `validate(schemas)` | Zod validation middleware |
1370
- | `upload(options?)` | Multipart file upload middleware |
1371
-
1372
- ### Sub-Router modules (mount via `app.use()`)
1373
-
1374
- | Import | Description |
1375
- |--------|-------------|
1376
- | `postgres(options?)` | PostgreSQL connection + DDL schema builder + transactions + module lifecycle |
1377
- | `redis(options?)` | Redis client (ioredis) — injects `ctx.redis` |
1378
- | `queue(options?)` | Redis-backed job queue — immediate, delayed, cron scheduling |
1379
- | `user(options)` | Built-in authentication (password + OAuth2 Server + JWT, middleware) |
1380
- | `tenant(options)` | Multi-tenant BaaS — dynamic tables, REST + GraphQL auto-generation, row-level isolation |
1381
- | `agent(options)` | AI Agent — chat/tool-use/knowledge, Ollama-ready, programmatic API |
1382
- | `messager(options)` | Real-time messaging — channels, WebSocket, agent routing, webhooks |
1383
- | `opencode(options)` | AI programming assistant — chat agents with tools, skills, permissions, isolated workspaces |
1384
- | `graphql(handler)` | GraphQL endpoint (GET/POST + GraphiQL) |
1385
- | `ai(handler)` | AI streaming endpoint (POST) |
1386
- | `runWorkflow(options)` | DAG execution engine as an AI SDK `Tool` — use with `streamText()` |
133
+ | `validate(schemas)` | Zod validation (body, query, params) |
134
+ | `upload(options?)` | Multipart file upload |
135
+ | `i18n(options)` | Internationalization — `ctx.t()`, locale detection |
1387
136
 
1388
- ### Deploy
1389
-
1390
- | Import | Description |
1391
- |--------|-------------|
1392
- | `deploy(config)` | Start the deployment platform — see [deploy.md](./deploy.md) |
1393
- | `defineConfig(config)` | Type-safe config helper with validation — see [deploy.md](./deploy.md) |
1394
-
1395
- ### Utilities
137
+ ### Utility functions
1396
138
 
1397
139
  | Function | Description |
1398
140
  |----------|-------------|
1399
- | `serveStatic(root, options?)` | Static file serving handler |
1400
- | `getCookies(req)` | Parse Cookie header object |
1401
- | `setCookie(res, name, value, options?)` | Set cookie (returns new Response) |
1402
- | `deleteCookie(res, name)` | Delete cookie (returns new Response) |
1403
- | `useTsx()` | Hook returning `{ params, query, user, parsed }` from `TsxContext` |
1404
- | `runWorkflow(options)` | Create a DAG execution AI SDK `Tool` — `{ tools?, model?, maxSteps? }` |
1405
- | `pgTable(name, columns)` | Type-safe table schema definition with DDL + CRUD |
1406
- | `pg.table(name, columns)` | Pre-bound table (no `sql` parameter needed for CRUD) |
1407
- | `serial()`, `uuid()`, `text()`, `integer()`, `boolean()`, `timestamptz()`, `jsonb()`, `textArray()`, `vector()` | Column type builders |
1408
- | `sql(strings, ...)` | SQL expression literal for defaults and SET values (e.g. `sql\`NOW()\``) |
1409
- | `PgModule` | Base class for database-backed modules (provides `sql`, `close()`) |
1410
- | `BoundTable` | Table with pre-bound `sql` — returned by `pg.table()` |
1411
- | `FindOptions` | Query options: `{ orderBy?, limit?, offset? }` for `find()` |
1412
-
1413
- Import `useTsx` and `TsxContext` from `'weifuwu'`.
141
+ | `serveStatic(root, options?)` | Static file serving |
142
+ | `getCookies(req)` / `setCookie(res, ...)` / `deleteCookie(res, ...)` | Cookie helpers |
143
+ | `mailer(options)` | Email sender (SMTP or custom) |
144
+ | `createTestServer(handler)` | Start test server `{ server, url }` |
145
+ | `runWorkflow(options)` | DAG execution engine as AI SDK `Tool` |
146
+ | `pgTable(name, columns)` | Type-safe table schema builder |
147
+ | `pg.table(name, columns)` | Pre-bound table (no `sql` param needed) |
148
+ | `serial()`, `uuid()`, `text()`, ... | Column type builders |
149
+ | `PgModule` | Base class for DB-backed modules |
1414
150
 
1415
151
  ## License
1416
152