npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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 dev

CLI

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 version

Core 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/42ctx.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-router
  1. serve() receives HTTP request
  2. app.handler() creates ctx = { params, query } and routes to the matching trie node
  3. Global middleware runs in use() order (e.g. theme(), i18n(), postgres(), cors())
  4. Path‑scoped middleware runs for matching paths (e.g. app.use('/admin', authMW))
  5. Route‑level middleware runs (e.g. app.get('/admin', validate(...), handler))
  6. 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-level

Middleware 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 path

Pattern γ — 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:00

Pattern δ — 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
    end

Quick 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 logginglogger({ 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 routes

auth [α] [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/apps

Running — use systemd for production:

[Service]
WorkingDirectory=/opt/deploy
ExecStart=/usr/bin/node /opt/deploy/deploy.ts
Restart=always

DeployConfig:

| 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/callback

Flow: 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