weifuwu
v0.25.0
Published
Web-standard HTTP framework for Node.js — (req, ctx) => Response
Readme
name: weifuwu description: Web-standard HTTP framework for Node.js — (req, ctx) => Response
weifuwu
Web-standard HTTP framework for Node.js. (req, ctx) => Response — no framework-specific objects.
Quick Start
import { serve } from 'weifuwu'
serve((req, ctx) => new Response('Hello, World!'), { port: 3000 })import { serve, Router, ssr } from 'weifuwu'
const app = new Router()
app.use('/', ssr({ dir: './ui' }))
serve(app.handler(), { port: 3000, websocket: app.websocketHandler() })npx weifuwu init my-app && cd my-app && npm run devCLI
Typical Full App
import {
serve,
Router,
postgres,
session,
user,
aiProvider,
ssr,
flash,
i18n,
theme,
logger,
rateLimit,
} from 'weifuwu'
const app = new Router()
// 1. Observability (order matters — run early)
app.use(logger())
// 2. UX middleware — single-line auto-registers middleware + routes
app.use(theme())
app.use(i18n({ default: 'zh', dir: './locales' }))
app.use(flash())
// 3. Database
const pg = postgres()
app.use(pg)
// 4. Session & Auth
app.use(session({ store: 'redis', redis: myRedis }))
const auth = user({ pg, jwtSecret: process.env.JWT_SECRET })
await auth.migrate()
app.use(auth) // auto-registers middleware + /register, /login
app.use('/auth', auth) // explicit path mounts for more control
// 5. API protection
app.use('/api', rateLimit({ max: 60, window: 60_000 }))
// 6. AI
app.use(aiProvider()) // ctx.ai
// 7. SSR
app.use('/', ssr({ dir: './ui' }))
// 8. REST API
app.get('/api/ping', () => Response.json({ ok: true }))
app.post('/api/chat', async (req, ctx) => {
const { prompt } = await req.json()
const result = await ctx.ai.generateText({ prompt })
return Response.json(result)
})
// 9. Start
const server = serve(app.handler(), { port: 3000 })npx weifuwu init my-app # Full project (SSR + i18n + theme + WS demo)
npx weifuwu init my-api --minimal # Minimal HTTP project (2 files)
npx weifuwu init my-api --skip-install # Skip npm install
npx weifuwu dev # Start dev server (auto-detect index.ts)
npx weifuwu generate module my-mod # Scaffold middleware module + test
npx weifuwu version # Print versionCore Concepts
serve()
const server = serve(handler, { port: 3000 })
await server.ready| Option | Type | Default | Description |
| ------------------ | ------------------ | ----------- | ------------------------------ |
| port | number | 0 | Listen port |
| hostname | string | '0.0.0.0' | Listen address |
| signal | AbortSignal | — | Shutdown on abort |
| websocket | WsUpgradeHandler | — | WebSocket upgrade handler |
| maxBodySize | number | 10MB | Max body bytes (0 = unlimited) |
| timeout | number | 30_000 | Socket inactivity timeout (ms) |
| keepAliveTimeout | number | 5_000 | Keep-Alive idle timeout (ms) |
| headersTimeout | number | 6_000 | Headers read timeout (ms) |
| shutdown | boolean | true | Auto SIGTERM/SIGINT |
interface Server {
stop: (timeoutMs?: number) => Promise<void> // graceful: waits for in-flight, force-closes after timeoutMs (default 10s)
readonly port: number
readonly hostname: string
ready: Promise<void>
}
const { server, url } = await createTestServer(handler)server.stop() performs a graceful shutdown: stops accepting new connections,
closes idle keep-alive sockets, then waits for in-flight requests to complete.
If they don't finish within timeoutMs (default 10 seconds), remaining connections
are forcibly closed. SIGTERM/SIGINT use the same graceful pattern.
Router
const app = new Router()
app.get('/hello/:name', (req, ctx) => Response.json({ message: `Hello, ${ctx.params.name}!` }))
app.post('/data', async (req, ctx) => {
const body = await req.json()
return Response.json(body, { status: 201 })
})
app.use('/admin', authMW) // path-scoped middleware
app.use('/admin', adminRouter) // sub-router (flattened into parent trie)
app.ws('/echo', {
open(ws, ctx) {
ctx.ws.json({ type: 'connected' })
},
message(ws, ctx, data) {
ctx.ws.json({ echo: data.toString() })
},
})
app.ws('/chat', {
open(ws, ctx) {
ctx.ws.join('room')
},
message(ws, ctx, data) {
ctx.ws.sendRoom('room', JSON.parse(data.toString()))
},
})
app.onError((err, req, ctx) => Response.json({ error: err.message }, { status: 500 }))
// Debug: list all registered routes
console.log(app.routes())
// [ 'GET /hello/:name', 'POST /data', 'WS /echo', 'WS /chat' ]
// Cross-process WebSocket broadcast (Redis)
import { createHub } from 'weifuwu'
app.wsHub(createHub({ redis: redis() }))
const handler = app.handler()
const wsHandler = app.websocketHandler()
serve(handler, { port: 3000, websocket: wsHandler })| Pattern | Example | Match |
| -------- | ------------ | ----------------------------- |
| Static | /about | exact |
| Param | /users/:id | /users/42 → ctx.params.id |
| Wildcard | /static/* | /static/js/app.js |
Query params → ctx.query.
Request lifecycle
Request → serve() → app.handler() → global middleware × N → path middleware × N → route handler → Response
↑
mountPath set by sub-routerserve()receives HTTP requestapp.handler()createsctx = { params, query }and routes to the matching trie node- Global middleware runs in
use()order (e.g.theme(),i18n(),postgres(),cors()) - Path‑scoped middleware runs for matching paths (e.g.
app.use('/admin', authMW)) - Route‑level middleware runs (e.g.
app.get('/admin', validate(...), handler)) - Route handler returns
Response— middleware chain unwinds
Sub-routers (app.use('/admin', adminRouter)) are flattened into the parent trie. The sub-router's global middleware merges with the parent's. ctx.mountPath is set when entering a sub-router, allowing each module to derive its own paths.
Middleware
type Middleware = (req: Request, ctx: Context, next: Handler) => Response | Promise<Response>
app.use(mw) // global
app.use('/admin', mw) // path-scoped
app.get('/admin', mw, handler) // route-levelMiddleware Dependency Checking
Middleware factories can declare what ctx fields they inject and depend on via
__meta. The Router warns at registration time if a dependency is unsatisfied.
// postgres() declares: __meta = { injects: ['sql'], depends: [] }
// session() declares: __meta = { injects: ['session'], depends: [] }
// user() declares: __meta = { injects: ['user'], depends: ['sql', 'session'] }
const app = new Router()
app.use(user()) // ⚠️ Warns: depends on 'sql' and 'session' but they aren't registered
// → "[weifuwu] Middleware at "global" depends on ctx.sql but it hasn't been registered yet."
// → "Register the provider before this middleware: app.use(sql())"
// Correct order:
app.use(postgres())
app.use(session())
app.use(user())To add __meta to your own middleware:
function myMiddleware() {
const mw = async (req, ctx, next) => {
ctx.myField = await setup()
return next(req, ctx)
}
mw.__meta = { injects: ['myField'], depends: ['sql'] }
return mw
}The check is purely advisory — warnings go to console.warn, no errors are thrown. Built-in
middleware (postgres, redis, session, aiProvider, rateLimit) all have __meta pre-attached.
New in v0.25.
Context
The ctx object accumulates properties as it passes through the middleware chain. Below are all documented properties:
| Property | Set by | Type | Description |
| ------------- | -------------------------------- | ------------------------- | ------------------------------------ |
| params | Router | Record<string, string> | URL path parameters |
| query | Router | Record<string, string> | URL query parameters |
| mountPath | Router | string | Current sub-router mount prefix |
| env | loadEnv() | Record<string, string> | Public env vars (WEIFUWU_PUBLIC_*) |
| csrf.token | csrf() | string | CSRF token (namespace) |
| requestId | requestId() | string | Request ID |
| session | session() | Session | Session data object |
| sql | postgres() | Sql<{}> | PostgreSQL tagged-template client |
| redis | redis() | Redis | Redis client |
| ai | aiProvider() | AIProvider | AI model & embedding |
| queue | queue() | Queue | Job queue |
| user | auth() / user().middleware() | { id?: string } | Authenticated user |
| permissions | permissions() | { roles, permissions } | RBAC roles & permissions sets |
| theme | theme() | { value, set } | Current theme + switcher |
| i18n | i18n() | { locale, t, set } | Locale, translation, switcher |
| flash | flash() | { value, set } | Flash message + setter |
| tailwind | tailwindContext() | { css, url } | Compiled Tailwind CSS |
| tenant | tenant() | TenantContext | Current tenant info |
| parsed | validate() / upload() | { body, files } | Validated/parsed request data |
| layoutStack | ssr() internal | LayoutEntry[] | React layout component stack |
| notifier | notifier() | Notifier | Multi-channel notification system |
| loaderData | User middleware | Record<string, unknown> | SSR data passed to client |
| mountPath | Router | string | Sub-router mount path |
| deploy | deploy() | { appName? } | Deploy gateway info |
Type-Safe Context
Middleware-injected properties are automatically typed through chained use() calls:
const app = new Router()
.use(csrf()) // → Router<Context & { csrf: { token: string } }>
.use(requestId()) // → Router<Context & { csrf: ..., requestId }>
.use(postgres()) // → Router<Context & { csrf: ..., requestId, sql }>
app.get('/me', (_req, ctx) => {
ctx.csrf.token // ✅ string (IDE autocomplete)
ctx.requestId // ✅ string
ctx.sql`SELECT 1` // ✅ Sql<{}>
})Each module exports an XxxInjected type (e.g. PostgresInjected, UserInjected) for composing custom context types. Context is an interface — modules augment it via declare module for ambient compatibility.
Module Patterns
All modules follow one of 4 patterns — learn these and you know every module.
| Pattern | How to mount | Example |
| ------- | ---------------------------------------- | ------------------------------------------------------ |
| [α] | app.use(mod()) | compress(), theme(), postgres() |
| [β] | app.use('/path', mod()) | health(), ssr({dir}), graphql(handler), user() |
| [γ] | Import and call directly | mailer(), fts, cron-utils |
| [δ] | import { useXxx } from 'weifuwu/react' | useTheme(), useLocale(), useWebsocket() |
Pattern α — Middleware
app.use(compress()) // basic
const pg = postgres() // with extras: .sql, .table, .migrate(), .close()
app.use(pg)
app.use(rateLimit({ max: 100 })) // with .close()Pattern β — Router
app.use('/health', health()) // with path
app.use('/graphql', graphql(handler))
app.use('/logs', logdb({ pg })) // with .log(), .migrate()
app.use('/auth', user({ pg, jwtSecret })) // with .middleware(), .register()
app.ws('/ws', messager({ pg }).wsHandler())β modules that need separate middleware use .middleware(). Most can auto-register both middleware and routes in one call:
app.use(theme()) // auto: middleware + /__theme/:value
app.use(i18n({ dir: './locales' })) // auto: middleware + /__lang/:locale
app.use(analytics({ pg })) // auto: middleware + /__analytics
app.use(auth) // auto: middleware + /register, /login (user())
// Explicit form when more control is needed:
const a = analytics()
app.use(a.middleware()) // tracking only
app.use('/', a) // dashboard at custom pathPattern γ — Standalone
Modules that don't intercept requests or serve routes. Import and use directly.
import { mailer, cronNext, fts } from 'weifuwu'
const email = mailer({ transport: 'smtp://...', from: '[email protected]' })
await email.send({ to: '[email protected]', subject: 'Hello', text: 'Body' })
const next = cronNext('0 9 * * 1-5') // next weekday at 09:00Pattern δ — Client-side
React hooks that self-register via addInterceptor(). Import to enable.
import { useTheme, useLocale, useWebsocket } from 'weifuwu/react'
function ThemeToggle() {
const { theme, setTheme } = useTheme()
return <button onClick={() => setTheme('dark')}>Dark</button>
}Module Dependency Map
graph TD
serve --> Router
Router --> postgres
Router --> redis
Router --> aiProvider
subgraph "DB-Dependent Modules"
user --> postgres
session --> postgres
session -.-> redis
queue --> postgres
queue -.-> redis
permissions --> postgres
analytics --> postgres
logdb --> postgres
tenant --> postgres
messager --> postgres
messager -.-> redis
agent --> postgres
kb --> postgres
iii --> postgres
iii -.-> redis
end
subgraph "AI-Dependent Modules"
agent --> aiProvider
kb --> aiProvider
aiStream --> aiProvider
opencode --> aiProvider
runWorkflow --> aiProvider
endQuick Module Selection
| What do you want to do? | Module | Pattern |
| -------------------------------- | ----------------------------------------------- | ---------------------- |
| User registration / login | user() | β |
| Simple token/header auth | auth() | α |
| JWT verification | user().middleware() | α |
| Role-based access control | permissions() | α |
| AI chat / generate / stream | ctx.ai.generateText() / ctx.ai.streamText() | α (via aiProvider()) |
| AI agent with knowledge | agent() + knowledgeBase() | β |
| Send email | mailer() | γ |
| File upload | upload() | α |
| Object storage (S3/MinIO) | s3() | α |
| Rate limiting | rateLimit() | α |
| Response caching | cache() | α |
| Periodic / delayed jobs | queue() | α |
| Page view analytics | analytics() | β |
| Structured logging | logdb() | β |
| Real-time chat / messager | messager() | β |
| Full-text search | fts | γ |
| Theme switching | theme() | α |
| i18n / localization | i18n() | α |
| Flash messages | flash() | α |
| Server-Sent Events | createSSEStream() | γ |
| GraphQL endpoint | graphql() | β |
| Webhook receiver | webhook() | β |
| SSR with React | ssr() | β |
| Health check | health() | β |
| SEO (robots.txt, sitemap) | seo() | β |
| Multi-process deploy | deploy() | γ |
| Distributed functions (iii) | iii() | β |
| Multi-tenant BaaS | tenant() | β |
| Client-side routing | useNavigate(), <Link> | δ |
| WebSocket in React | useWebsocket() | δ |
| Compression (brotli/gzip) | compress() | α |
| Security headers (CSP, HSTS) | helmet() | α |
| CORS | cors() | α |
| CSRF protection | csrf() | α |
| Request ID tracing | requestId() | α |
| Environment variables | env() / loadEnv() | α |
| Static file serving | serveStatic() | α |
| Object storage (S3/MinIO) | s3() | α |
| Send email | mailer() | γ |
| Scheduled / cron tasks | cron-utils (cronNext()) | γ |
| Server-Sent Events | createSSEStream() | γ |
| Multi-process deploy | deploy() | γ |
| Distributed functions (iii) | iii() | β |
| Webhook receiver | webhook() | β |
| MCP tool integration | mcpClient() | γ |
| Notifications | notifier() | α |
| API Key management | user({ apiKeys: true }) | β |
| WebSocket testing | testApp().wsReq() | — |
| Social login (OAuth) | user({ oauthLogin }) | β |
| Database migrations | pg.migrate() | — |
Request Tracing & Logging
Every request gets a trace ID via AsyncLocalStorage, injected into responses as X-Trace-Id. W3C traceparent headers are forwarded.
import { currentTraceId } from 'weifuwu'
app.get('/api', (req, ctx) => {
console.log('Handling request', currentTraceId()) // f240a3f3-60e2-...
})Structured logging — logger({ format: 'json' }) outputs JSON to stderr with traceId, timestamp, elapsed_ms:
{
"level": "info",
"message": "request",
"method": "GET",
"path": "/api/users",
"status": 200,
"elapsed_ms": 42,
"traceId": "f240a3f3-...",
"timestamp": "2025-01-15T10:30:00.000Z"
}Default format is 'short' (human-readable). 'combined' includes query strings.
AI Observability
Agent runs are automatically logged to _agent_runs. Dashboard endpoints provide analytics:
GET /agents/:id/runs?days=7 → [{ input, output, tokens_in, tokens_out, elapsed_ms, status, trace_id, ... }]
GET /agents/:id/runs/summary?days=7 → { total, success, error, success_rate, tokens_in, tokens_out, avg_elapsed_ms, p95_elapsed_ms }
GET /opencode/sessions/:id/usage → { message_count, tokens_in, tokens_out, tokens_total }Non-streaming runs log full token data; streaming runs log status: 'stream'.
Agent ↔ Messager Streaming
Agent replies in messager channels now stream token-by-token via WebSocket:
// Backend — automatic when agents are attached to messager
const msg = messager({ pg, agents: agent({ pg, model }) })
app.ws('/ws', msg.wsHandler())
// Agent replies stream to: hub.broadcast({ type: 'agent_stream', data: { token, full } })// Frontend — React hook
import { useAgentStream } from 'weifuwu/react'
const { getAgentText, isAgentStreaming, stream } = useAgentStream({
wsPath: '/ws',
channelId: 1,
})Multi-round conversation context: the last 10 channel messages are automatically injected into agent calls.
Test Utilities
Chainable test helper for HTTP-level testing without starting a server:
import { testApp } from 'weifuwu'
const app = testApp()
app.use(postgres({ connection: TEST_DB }))
app.get('/users/:id', (req, ctx) => Response.json({ id: ctx.params.id, user: ctx.user }))
const res = await app
.getReq('/users/42?name=Alice')
.withUser({ id: 1 })
.header('X-Custom', 'val')
.body({ data: 'test' })
.send()
assert.equal(res.status, 200)
assert.deepEqual(await res.json(), { id: '42', user: { id: 1 } })| Method | Description |
| ------------------------------------------------------------ | ----------------------------------------------------- |
| app.getReq(path) postReq putReq patchReq deleteReq | Start building a request |
| .withUser(u) .withTenant(t) .with(ctx) | Simulate middleware injection |
| .header(k,v) .body(data) .rawBody(str) | Set request properties |
| .send() → TestResponse | Execute and get { status, headers, json(), text() } |
WebSocket testing (new in v0.25) — app.ws() + app.wsReq():
const app = testApp()
app.ws('/echo', {
open(ws) {
ws.send(JSON.stringify({ type: 'connected' }))
},
message(ws, ctx, data) {
ws.send('echo: ' + data.toString())
},
})
// Connect via WebSocket
const conn = await app.wsReq('/echo').connect()
// Wait for the open message
const openMsg = await conn.receiveJson()
assert.equal(openMsg.type, 'connected')
// Send and receive
conn.send('hello')
const reply = await conn.receive()
assert.equal(reply, 'echo: hello')
conn.close()
await app.close() // cleanup server| Method | Description |
| ------------------------------------------ | ------------------------------------------- |
| app.ws(path, handler) | Register a WebSocket handler |
| app.wsReq(path) | Start building a WebSocket connection |
| .timeout(ms) | Set connection timeout (default: 5000) |
| .connect() → TestWSConnection | Connect and return a connection handle |
| conn.send(data) / conn.json(obj) | Send a message |
| conn.receive() / conn.receiveJson<T>() | Wait for the next message |
| conn.expectSilent(ms) | Assert no message arrives within the period |
| conn.close() | Close the connection |
| app.close() | Close all connections and stop the server |
Database test isolation
import { createTestDb, withTestDb } from 'weifuwu'
// Isolated schema — each test gets its own schema, destroyed after
const db = await createTestDb()
await db.sql`CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)`
await db.sql`INSERT INTO users (name) VALUES ('Alice')`
await db.destroy() // DROP SCHEMA ... CASCADE
// Transaction rollback — all changes are rolled back after callback
await withTestDb(async (sql) => {
await sql`INSERT INTO users ...`
// Automatically rolled back
})| Function | Description |
| ---------------------- | --------------------------------------------------------------- |
| createTestDb(opts?) | Create isolated schema, returns { sql, url, schema, destroy } |
| withTestDb(url?, fn) | Run callback in a transaction, auto-rollback |
Uses TEST_DATABASE_URL or DATABASE_URL. Automatically skipped in CI if unset.
Module Reference
Modules are organized alphabetically. Each module shows its pattern badge ([α] Middleware, [β] Router, [γ] Standalone, [δ] Client-side) and category.
Category key: AI, API, Clientδ, Database, DevTools, Networking, Security, SSR, UX
agent [β] [AI]
const provider = aiProvider()
const a = agent({ pg, provider })
await a.migrate()
app.use('/api', a)
await a.addKnowledge(agentId, 'Title', 'some knowledge content')
a.run(agentId, { input: 'summarize the data', stream: true })| Option | Type | Default | Description |
| -------------------- | ------------ | ------------------------- | --------------------------------------------- |
| pg | object | — | PostgreSQL client |
| provider | AIProvider | aiProvider() (from env) | AI provider for model & embedding resolution |
| model | object | — | Explicit AI model (overrides provider) |
| embeddingModel | object | — | Explicit embedding model (overrides provider) |
| embeddingDimension | number | provider.dimension | Embedding vector dimension |
| tools | object[] | — | Custom tool definitions |
| Method | Description |
| ---------------------------------------------- | ------------------------ |
| .run(agentId, { input, stream?, messages? }) | Execute agent with input |
| .addKnowledge(agentId, title, content) | Add knowledge document |
| .migrate() | DB setup |
| .close() | Cleanup |
aiStream [β] [AI]
Creates an AI streaming chat endpoint using the Vercel AI SDK.
const provider = aiProvider()
const chat = await aiStream(async (req) => ({ messages: (await req.json()).messages }), provider)
app.use('/chat', chat)| Param | Type | Description |
| ---------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| handler | (req, ctx) => AIStreamOptions \| Promise<AIStreamOptions> | Returns AI SDK options (model, messages, schema, etc.) |
| provider | AIProvider | Optional. If provided and handler omits model, provider.model() is used as default |
analytics [β] [API]
In-memory or PostgreSQL page view tracking with built-in dashboard.
const a = analytics()
app.use(a.middleware())
app.use('/', a) // GET /__analytics (dashboard), GET /__analytics/data?days=7 (JSON)| Option | Type | Default | Description |
| ---------- | ---------- | --------------------------------------- | --------------------------------- |
| pg | object | — | PostgreSQL client for persistence |
| excluded | string[] | ['/__analytics', '/__wfw', '/static'] | Paths to skip |
// With PostgreSQL
const a = analytics({ pg })
await a.migrate()
app.use(a.middleware())
app.use('/', a) // dashboard routesauth [α] [Security]
app.use(auth({ token: 'sk-123' })) // static token
app.use(auth({ header: 'X-API-Key', token: 'my-key' })) // custom header
app.use(auth({ verify: async (token, req) => ({ sub: 'abc' }) })) // custom verify → sets ctx.user
app.get('/protected', auth({ proxy: 'http://auth:3000/validate' }), handler)
// Session-based auth (must be placed after session() middleware)
app.use(session())
app.use(
auth({
session: true,
resolveUser: async (userId) => {
// load user from DB
const [user] = await sql`SELECT * FROM users WHERE id = ${userId}`
return user ?? null // null → destroy stale session
},
}),
)| Option | Type | Default | Description |
| ------------- | ------------------------------ | ----------------- | -------------------------------------------------------------------------------------------------------- |
| token | string | — | Static token to match |
| header | string | 'Authorization' | Header name |
| verify | (token, req) => object\|null | — | Verify function, return value sets ctx.user |
| proxy | string | — | Auth service URL to proxy requests to |
| session | boolean | false | Enable session-based auth. Checks ctx.session.userId first |
| resolveUser | (userId) => object\|null | — | Load user from userId (called when session: true). Return falsy to reject + auto-destroy stale session |
When session: true, auth checks ctx.session.userId before the
Authorization header. This lets logged-in users authenticate via their
session cookie without sending a token. Falls back to header/token auth
if no session userId is present.
compress [α] [DevTools]
app.use(compress()) // brotli > gzip > deflate (min 1KB)
app.use(compress({ threshold: 2048, level: 4 })) // custom threshold and level| Option | Type | Default | Description |
| ----------- | -------- | ------- | ----------------------------- |
| threshold | number | 1024 | Minimum byte size to compress |
| level | number | 6 | Compression level (zlib) |
cors [α] [DevTools]
app.use(cors()) // allow all
app.use(cors({ origin: ['https://example.com'] })) // whitelist
app.use(cors({ origin: (o) => o.endsWith('.trusted.com') && o }))
app.use(cors({ credentials: true, maxAge: 3600 }))| Option | Type | Default | Description |
| ---------------- | ---------------------------- | -------------------------------------------------------- | ---------------------------------- |
| origin | string\|string[]\|function | '*' | Allowed origins |
| methods | string[] | ['GET','POST','PUT','DELETE','PATCH','HEAD','OPTIONS'] | Allowed methods |
| allowedHeaders | string[] | — | Custom allowed headers |
| exposedHeaders | string[] | — | Response headers exposed to client |
| credentials | boolean | false | Allow cookies/credentials |
| maxAge | number | — | Preflight cache duration (seconds) |
flash [α] [UX]
Cookie-based flash message. Read from request, write via redirect.
app.use(flash())
app.get('/', (req, ctx) => {
const msg = ctx.flash.value // { type: 'success', text: 'Saved!' } or undefined
})
app.post('/save', (req, ctx) => {
return ctx.flash.set({ type: 'success', text: 'Saved!' }, '/articles')
})| Option | Type | Default | Description |
| ------ | -------- | --------- | ----------- |
| name | string | 'flash' | Cookie name |
cache [α] [DevTools]
Response caching middleware with memory and Redis stores. Caches GET/HEAD responses, with tag-based invalidation.
app.use(cache()) // in-memory, 5min TTL
app.use(cache({ ttl: 60_000, store: 'redis', redis: ctx.redis })) // Redis store
app.use(
cache({
ttl: 30_000,
tag: (req, ctx) => (ctx.user ? `user:${ctx.user.id}` : undefined), // per-user invalidation
}),
)
// Programmatic invalidation
const c = cache({ store: 'redis', redis: ctx.redis })
app.use(c)
await c.invalidate('users') // invalidate all entries tagged with 'users'
await c.flush() // clear entire cache| Option | Type | Default | Description |
| -------------- | ----------------------------------- | ------------------ | --------------------------------------------- |
| ttl | number | 300000 (5min) | Cache TTL in ms |
| store | 'memory' \| 'redis' \| CacheStore | 'memory' | Cache store backend |
| redis | Redis | — | Redis client (required when store: 'redis') |
| key | (req) => string | SHA256(method+URL) | Custom cache key |
| tag | (req, ctx) => string \| string[] | — | Tag for grouped invalidation |
| cacheCookies | boolean | false | Cache responses with Set-Cookie |
| cacheStatus | number[] | [200] | Status codes to cache |
| maxBodySize | number | 1048576 (1MB) | Max body bytes to cache |
Cached responses include X-Cache: HIT and Age headers. Requests with Authorization or Cookie headers are never cached. Binary content types (image, audio, video) are skipped.
import { MemoryCache, RedisCache } from 'weifuwu'
const mem = new MemoryCache()
await mem.set(
'key',
{ status: 200, statusText: 'OK', headers: {}, body: '...', createdAt: Date.now(), tags: [] },
300_000,
)
mem.close()csrf [α] [Security]
app.use(csrf())
// ctx.csrf.token — set on GET/HEAD/OPTIONS
// Auto-validates x-csrf-token or x-xsrf-token header on POST/PUT/DELETE/PATCH
// Falls back to body field matching the key name| Option | Default | Description |
| ---------------- | -------------------------- | ----------------------------------------- |
| cookie | '_csrf' | Cookie name |
| header | 'x-csrf-token' | Header name (also accepts x-xsrf-token) |
| key | '_csrf' | Body field fallback |
| excludeMethods | ['GET','HEAD','OPTIONS'] | Skip validation |
deploy [β] [Networking]
Multi-process manager with reverse proxy, health checks, auto-restart, and zero-downtime updates. Works identically locally and in production.
import { deploy, defineConfig } from 'weifuwu'
// Local
await deploy(
defineConfig({
apps: { blog: {}, api: {} },
}),
)
// Production
await deploy(
defineConfig({
domain: 'example.com',
deployToken: process.env.DEPLOY_TOKEN,
apps: { blog: {}, api: {} },
}),
)Auto-derived defaults — each app key derives dir, port, entry, and path:
| Field | Default | Rule |
| ------- | ------------ | -------------------------- |
| dir | App key | blog → './blog' |
| entry | 'index.ts' | Default entry file |
| port | 3001+ | Auto-incremented from 3001 |
| path | '/key' | Only for localhost domain |
Override any field explicitly:
defineConfig({
apps: {
blog: { dir: '../packages/blog', entry: 'server.ts', port: 8080, path: '/blog' },
},
})Routing — match priority: explicit path > app key > defaultApp.
apps: {
api: { path: '/api' }, // example.com/api or localhost:3000/api
blog: {}, // blog.example.com or localhost:3000/blog
}Blue-green — zero-downtime via ports:
apps: {
blog: {
ports: [3001, 3002]
}
}WebSocket — automatically bridged through the gateway.
Process watchdog — auto-restarts with exponential backoff on unexpected exit.
Management API — all endpoints require Authorization: Bearer <deployToken>:
| Endpoint | Method | Description |
| ----------------------------- | ------ | -------------- |
| /_deploy/apps | GET | List apps |
| /_deploy/apps/:name | GET | App details |
| /_deploy/apps/:name/deploy | POST | Restart |
| /_deploy/apps/:name/restart | POST | Restart |
| /_deploy/apps/:name/stop | POST | Stop |
| /_deploy/apps/:name/start | POST | Start |
| /_deploy/apps/:name/logs | GET | SSE log stream |
curl -H "Authorization: Bearer my-token" http://localhost:3000/_deploy/appsRunning — use systemd for production:
[Service]
WorkingDirectory=/opt/deploy
ExecStart=/usr/bin/node /opt/deploy/deploy.ts
Restart=alwaysDeployConfig:
| Option | Default | Description |
| ------------- | ------------- | ------------------------------- |
| domain | 'localhost' | Root domain |
| port | 3000 | Gateway port |
| deployToken | — | Bearer token for management API |
| defaultApp | — | Fallback route |
| apps | — | Record<string, AppConfig> |
AppConfig:
| Field | Default | Description |
| ---------------- | ---------------- | ------------------------------- |
| dir | App key | Directory containing the app |
| port | Auto (3001+) | Internal port |
| entry | 'index.ts' | Entry file |
| path | '/key' (local) | URL path prefix |
| env | — | Environment variables |
| healthEndpoint | / | Health check path |
| buildCommand | — | Build command |
| ports | — | [port, port+1] for blue-green |
env [α] [DevTools]
Environment variable middleware. Injects ctx.env with all WEIFUWU_PUBLIC_* variables (prefix stripped).
Safe to expose to the client.
import { env, loadEnv } from 'weifuwu'
loadEnv() // Load .env into process.env
app.use(env()) // → ctx.env
app.get('/config', (req, ctx) => {
return Response.json({ apiUrl: ctx.env.API_URL })
})Helper utilities:
import { isDev, isProd, isBundled, getPublicEnv } from 'weifuwu'
isDev() // NODE_ENV === 'development'
isProd() // NODE_ENV === 'production'
isBundled() // Running from compiled dist/index.js?
getPublicEnv() // { API_URL: '...' } — no middleware needed| Function | Description |
| ---------------- | ---------------------------------------------------------------- |
| loadEnv(path?) | Load .env file into process.env (does not override existing) |
| env() | Middleware — injects ctx.env with public vars |
| getPublicEnv() | Returns WEIFUWU_PUBLIC_* vars with prefix stripped |
| isDev() | true when NODE_ENV === 'development' |
| isProd() | true when NODE_ENV === 'production' |
| isBundled() | true when running from compiled bundle |
graphql [β] [API]
const handler: GraphQLHandler = () => ({
schema: `type Query { hello: String }`,
resolvers: { Query: { hello: () => 'world' } },
graphiql: true, // GET / returns GraphiQL IDE
maxDepth: 10, // max query nesting (default 10, 0 = disable)
timeout: 30_000, // execution timeout in ms
})
app.use('/graphql', graphql(handler))| Option | Type | Default | Description |
| ----------- | ------------------------- | -------- | ------------------------------ |
| schema | string \| GraphQLSchema | — | SDL string or pre-built schema |
| resolvers | object | — | Resolver map |
| rootValue | any | — | Root value for queries |
| context | (req, ctx) => object | — | Per-request context factory |
| graphiql | boolean | false | Serve GraphiQL IDE at GET / |
| maxDepth | number | 10 | Max query nesting depth |
| timeout | number | 30_000 | Execution timeout (ms) |
health [β] [API]
app.use('/health', health())
// Returns 200 on success, 503 when check throws| Option | Type | Default | Description |
| ------- | --------------------- | ----------- | ---------------------------- |
| path | string | '/health' | Health check endpoint |
| check | () => Promise<void> | — | Async function; throws → 503 |
helmet [α] [Security]
15 security headers: CSP, HSTS, X-Frame-Options, X-Content-Type-Options, etc.
app.use(helmet())
app.use(helmet({ contentSecurityPolicy: "default-src 'self'", xFrameOptions: 'DENY' }))| Option | Default | Description |
| --------------------------- | --------------------------------------- | -------------------------- |
| contentSecurityPolicy | "default-src 'self'" | CSP policy |
| xFrameOptions | 'SAMEORIGIN' | Frame-embedding policy |
| strictTransportSecurity | 'max-age=15552000; includeSubDomains' | HSTS |
| referrerPolicy | 'no-referrer' | Referrer header |
| xContentTypeOptions | 'nosniff' | MIME sniffing protection |
| permissionsPolicy | — | Feature permissions policy |
| crossOriginEmbedderPolicy | — | COEP header |
| crossOriginOpenerPolicy | — | COOP header |
| crossOriginResourcePolicy | — | CORP header |
iii [β] — Worker / Function / Trigger [API]
Distributed function execution with WebSocket workers, triggers, and Redis streams.
import { createWorker } from 'weifuwu'
const engine = iii({ pg, redis })
app.use('/iii', engine)
app.ws('/iii', engine.wsHandler())
const w = createWorker('orders')
w.registerFunction('orders::create', async (payload) =>
db.query('INSERT INTO orders ...', [payload.items]),
)
engine.addWorker(w)
await engine.trigger({ function_id: 'orders::create', payload: { items: ['apple'] } })| Option | Type | Default | Description |
| ----------- | -------- | ------- | --------------------------------------------- |
| pg | object | — | PostgreSQL client for persistent triggers |
| redis | object | — | Redis client for streams |
| streamTTL | number | 3600 | Redis stream key TTL (seconds, 0 = no expiry) |
| Method | Description |
| ---------------------------------------------------------- | ------------------------- |
| .addWorker(w) | Register a worker |
| .removeWorker(w) | Remove a worker |
| .trigger({ function_id, payload, action?, timeout_ms? }) | Invoke a function |
| .listWorkers() | List registered workers |
| .listFunctions() | List registered functions |
| .listTriggers() | List registered triggers |
| .wsHandler() | WebSocket handler |
| .migrate() | DB setup |
| .shutdown() | Clean shutdown |
knowledgeBase [β] — RAG with pgvector [AI]
import { knowledgeBase, aiProvider } from 'weifuwu'
const kb = knowledgeBase({
pg: postgres(),
provider: aiProvider(),
table: 'my_docs',
})
// Create table + HNSW index (safe to call multiple times)
await kb.migrate()
// Ingest a document (auto chunk → embed → store)
await kb.ingest('docs/intro.md', `# Welcome\n\nThis is the introduction...`, {
title: 'Introduction',
metadata: { source: 'docs', author: 'alice' },
})
// Semantic search
const results = await kb.search('how to get started?', { limit: 5 })
// → [{ key, title, content, score: 0.92, metadata }, ...]
// Delete
await kb.delete('docs/outdated.md')
// List all documents
const entries = await kb.list()
// → [{ key, title, chunks: 3 }, ...]
// Use as middleware (injects ctx.kb.search)
app.use(kb.middleware())
app.get('/search', async (req, ctx) => {
const results = await ctx.kb.search(ctx.query.q)
return Response.json(results)
})| Option | Type | Default | Description |
| ----------------- | ---------------- | ------------ | --------------------------------------- |
| pg | PostgresClient | — | Required. PostgreSQL client |
| provider | AIProvider | — | Required. AI provider for embedding |
| table | string | '_kb_docs' | Database table name |
| chunkSize | number | 512 | Max characters per chunk |
| chunkOverlap | number | 64 | Overlap between chunks |
| searchLimit | number | 5 | Default search result count |
| searchThreshold | number | 0 | Minimum similarity (0–1) |
Documents are split on paragraph boundaries (\n\n). Re-ingesting the same key
replaces old chunks. Provider's embed() is used automatically.
The HNSW index enables fast approximate nearest-neighbor search (cosine distance).
logdb [β] [API]
PostgreSQL structured event logging with monthly partitioning.
const logger = logdb({ pg })
await logger.migrate()
app.use('/logs', logger)
await logger.clean(12) // drop partitions older than 12 months
await logger.log({ level: 'info', source: 'app', message: 'hello', metadata: { userId: 1 } })| Option | Type | Default | Description |
| ------- | -------- | ---------------- | ----------------- |
| pg | object | — | PostgreSQL client |
| table | string | '_log_entries' | Table name |
| Method | Path | Description |
| ------ | ------ | ---------------------------------------------------------------- |
| POST | / | Create log entry |
| GET | / | Query (?level=, ?source=, ?after=, ?before=, ?meta.*=) |
| GET | /:id | Get single entry |
logger [α] [DevTools]
app.use(logger()) // GET /hello 200 5ms
app.use(logger({ format: 'combined' })) // with query params| Option | Type | Default | Description |
| -------- | --------------------------------- | --------- | ------------------------------------------------------------- |
| format | 'short' \| 'combined' \| 'json' | 'short' | Log format: path only, path + query params, or JSON to stderr |
mailer [γ] [Networking]
const mail = mailer({
from: '[email protected]',
transport: 'smtp://user:[email protected]:587',
})
await mail.send({
to: '[email protected]',
subject: 'Hello',
text: 'Body',
html: '<p>Body</p>',
cc: '[email protected]',
})| Option | Type | Default | Description |
| ----------- | ---------------- | ------- | ------------------------------------------------ |
| transport | string\|object | — | Nodemailer transport config or connection string |
| from | string | — | Default sender address |
| send | function | — | Custom send function (alternative to transport) |
mcpClient [γ] — MCP Server integration [AI]
Model Context Protocol client. Spawns MCP server subprocesses and exposes their tools as AI SDK-compatible tool objects.
import { mcpClient, agent, aiProvider } from 'weifuwu'
const fsMcp = mcpClient({
command: 'npx',
args: ['@modelcontextprotocol/server-filesystem', '/workspace'],
})
const tools = await fsMcp.getTools()
const a = agent({ pg, provider: aiProvider(), tools })
await a.run(agentId, { input: 'read package.json' })
// Later, refresh tools if the server provides new ones
await fsMcp.refresh()
// Or call a tool directly
const result = await fsMcp.callTool('echo', { text: 'hello' })
await fsMcp.close() // shutdown the MCP server process| Option | Type | Default | Description |
| ----------------- | ---------- | ------- | ------------------------------------------------------- |
| command | string | — | Required. Command to spawn (e.g. 'npx', 'node') |
| args | string[] | [] | Arguments passed to the command |
| env | object | — | Extra environment variables |
| timeout | number | 15000 | Handshake/response timeout (ms) |
| maxResponseSize | number | 10MB | Max tool response body size |
| Method | Description |
| ------------ | ------------------------------------------------------------------------- |
| getTools() | Fetch tool definitions, returns Record<string, Tool>-compatible objects |
| refresh() | Re-fetch tool definitions from the server |
| callTool() | Call a tool by name directly |
| close() | Shutdown the MCP server process |
Tool schemas (JSON Schema) are automatically converted to Zod schemas for AI SDK compatibility. Responses are concatenated from text content items, with size limiting.
oauthLogin (via user()) — Social login (OAuth 2.0 client) [Security]
Social login is built into the user() module via the oauthLogin option — no separate import needed.
app.use(session()) // required — stores OAuth state
const u = user({
pg,
jwtSecret: process.env.JWT_SECRET!,
oauthLogin: {
redirectUrl: '/dashboard',
providers: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
},
github: {
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
},
},
},
})
await u.migrate()
app.use(u) // POST /register, POST /login, GET /auth/:provider, GET /auth/:provider/callbackFlow: User clicks "Login with Google" → redirected to Google → back to app → user created/linked in database → JWT signed → session created → redirected to redirectUrl with ?token= (or JSON response for API clients).
Supports custom providers via authUrl, tokenUrl, userUrl, and parseUser:
const u = user({
pg,
jwtSecret: process.env.JWT_SECRET!,
oauthLogin: {
providers: {
discord: {
clientId: process.env.DISCORD_CLIENT_ID,
clientSecret: process.env.DISCORD_CLIENT_SECRET,
authUrl: 'https://discord.com/api/oauth2/authorize',
tokenUrl: 'https://discord.com/api/oauth2/token',
userUrl: 'https://discord.com/api/users/@me',
parseUser: (data) => ({
id: data.id,
email: data.email ?? '',
name: data.global_name ?? data.username,
avatarUrl: data.avatar
? `https://cdn.discordapp.com/avatars/${data.id}/${data.avatar}.png`
: '',
}),
},
},
},
})| Option (oauthLogin) | Type | Default | Description |
| ------------------- | ------------------------------------- | ------- | ------------------------------------------------------------------- |
| providers | Record<string, OAuthProviderConfig> | — | Required. Provider configs (Google/GitHub built-in, any custom) |
| redirectUrl | string | '/' | Post-login redirect destination |
Built-in providers (Google, GitHub) have preset URLs — you only need to provide clientId and clientSecret. The module auto-creates a _auth_providers table on first request.
messager [β] [Networking]
Real-time chat with channels, WebSocket, agent routing.
const msg = messager({ pg, agents, redis: redis() })
await msg.migrate()
app.use('/api', msg)
app.ws('/ws', msg.wsHandler())
await msg.send(channelId, 'System message', { sender_type: 'system', sender_id: 'bot' })| Option | Type | Default | Description |
| ---------------- | ------------- | ------- | ------------------------ |
| pg | object | — | PostgreSQL client |
| agents | AgentModule | — | Agent module for routing |
| webhookTimeout | number | — | Webhook timeout |
| redis | object | — | Redis client |
| Method | Description |
| -------------------------------- | --------------------------------------------------- |
| .wsHandler() | WebSocket handler (channels, typing, read receipts) |
| .send(channel, content, opts?) | Send message to channel |
| .close() | Cleanup |
notifier [α] [UX]
Multi-channel notification system with inbox (DB persistent), email, and WebSocket push. Per-user channel preferences.
import { notifier, mailer } from 'weifuwu'
const mail = mailer({ from: '[email protected]', transport: '...' })
const n = notifier({ sql: pg.sql, mailer: mail })
await n.migrate()
app.use(n) // injects ctx.notifier
// Send a notification (routes through user's channel preferences)
await ctx.notifier.send(
{ userId: 42, email: '[email protected]' },
{ title: 'Welcome!', body: 'Thanks for joining', type: 'onboarding' },
)
// Broadcast to all users with inbox enabled
await ctx.notifier.broadcast({
title: 'System maintenance tonight',
body: 'The system will be down from 2-4 AM',
})
// Check unread count
const count = await ctx.notifier.unreadCount(userId)
// List notifications (newest first)
const notifications = await ctx.notifier.list(userId, { limit: 10 })
// Mark as read
await ctx.notifier.markRead(userId, [notifId])
await ctx.notifier.markRead(userId) // mark ALL as read
// User preferences
await ctx.notifier.setPreferences(userId, { channels: ['inbox', 'email'] })
const prefs = await ctx.notifier.getPreferences(userId)
// → { channels: ['inbox', 'email'] }| Option | Type | Default | Description |
| ---------- | ----------- | ------------------ | ------------------------------- |
| sql | SqlClient | — | Required. PostgreSQL client |
| mailer | Mailer | — | Mailer for email channel |
| hub | Hub | — | Pub/sub hub for WebSocket push |
| table | string | '_notifications' | Notifications table name |
| pageSize | number | 50 | Default page size for list() |
| Method | Description |
| -------------------------------- | --------------------------------------------- |
| .send(to, message) | Send notification (routes by user preference) |
| .broadcast(message) | Send to all users with inbox enabled |
| .unreadCount(userId) | Count unread notifications |
| .count(userId, unreadOnly?) | Total or unread count |
| .markRead(userId, ids?) | Mark notification(s) as read |
| .list(userId, opts?) | List notifications (paginated) |
| .getPreferences(userId) | Get user's channel preferences |
| .setPreferences(userId, prefs) | Set user's channel preferences |
| .migrate() | Create tables |
| .clean(days) | Delete notifications older than N days |
Channel routing: Each user has channel preferences (default: ['inbox']). When
sending, the notification is delivered to each enabled channel. Email requires
mailer to be configured. WebSocket requires hub (e.g. from messager.wsHandler()).
opencode [β] [AI]
AI programming assistant.
const oc = await opencode({
pg,
model: openai('gpt-4o'),
workspace: '/home/user/project',
permissions: { bash: { allow: true }, write: { allow: false } },
})
await oc.migrate()
app.use('/opencode', oc)
app.ws('/opencode', oc.wsHandler())| Option | Type | Default | Description |
| -------------- | ---------- | ------- | ------------------------------------------------------ |
| pg | object | — | PostgreSQL client |
| model | string | — | AI model name (e.g. 'gpt-4o', 'deepseek-v4-flash') |
| baseURL | string | — | OpenAI-compatible API base URL |
| apiKey | string | — | API key for the model |
| workspace | string | — | Project directory |
| systemPrompt | string | — | Custom system prompt |
| skills | object[] | — | Custom skill definitions |
| permissions | object | — | Tool permission rules |
postgres [α] [Database]
Type-safe PostgreSQL client with schema builder, CRUD, migrations, soft delete, and JSONB/vector support.
const pg = postgres() // reads DATABASE_URL
app.use(pg) // injects ctx.sql| Option | Type | Default | Description |
| ------------------ | --------------------------- | ------------------ | --------------------------------------- |
| connection | string | DATABASE_URL env | PostgreSQL connection string |
| max | number | 10 | Max pool connections |
| ssl | boolean\|object | — | SSL options |
| idle_timeout | number | 30 | Idle timeout (seconds) |
| connect_timeout | number | 30 | Connection timeout |
| statementTimeout | number | 30_000 | Per-statement timeout (ms, 0 = disable) |
| onQuery | (query, ms, rows) => void | — | Query logging callback |
// Raw SQL via tagged template
await pg.sql`SELECT * FROM users WHERE email = ${email}`
// Define a table — one API, sql pre-bound
import { serial, text, boolean, timestamps } from 'weifuwu'
const users = pg.table('_users', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
email: text('email').unique().notNull(),
active: boolean('active').default(true),
...timestamps(),
})
await users.create() // DDL — no need to pass sql
await users.createIndex('email')
// CRUD — sql already bound
await users.insert({ name: 'Alice' })
const { count, data } = await users.readMany(
{ role: 'admin' },
{ orderBy: { name: 'asc' }, limit: 10 },
)
await users.upsert({ email: '[email protected]' }, 'email')
// Reuse schema without redefining fields
import { pgTable } from 'weifuwu'
const usersSchema = pgTable('_users', { id: serial('id'), name: text('name') }) // define once
const users = pg.table(usersSchema) // bind — no field duplication
// Transactions — with auto-retry on deadlock/serialization failure
await pg.transaction(
async (sql) => {
const txUsers = users.withSql(sql)
return txUsers.insert({ name: 'Bo