后端与接口
需要部署长运行、就近数据库的 serverless…
适用于在 Neon 分支上部署长运行 Node.js HTTP 函数、让计算靠近 Postgres,并处理流式 Agent、WebSocket/SSE、Webhook、cron、MCP 等请求/响应工作负载。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 38/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 59/100 ×0.15
适用场景
需要部署长运行、就近数据库的 serve特定
解决思路
- 若未安装父 `neon` skill,先从 https://neon.com/docs/ai/skills/neon/SKILL.md 获取,或用 `neon skills -s neon -y` 安装;先阅读父 skill 了解 Neon 概览、入门与最佳实践。
- 确认 Neon Functions 目前仅在 `aws-us-east-2`、`aws-us-east-1`、`aws-eu-central-1`、`aws-ap-southeast-1` 可用,并确认用户 Neon 项目位于这些区域之一。
- 在 `neon.ts` 中通过 `@neon/config/v1` 的 `defineConfig` 声明 `functions`,以 slug 为 key;slug 必须匹配 `^[a-z0-9]{1,20}$`,是函数永久身份,首次部署后不能修改;用 `name` 设置展示标签,用 `source` 设置相对 `neon.ts` 的入口文件。
- 函数入口是默认导出带 `fetch(request)` 方法并返回 `Response` 的对象(Workers/WinterTC 兼容);`export default app` 的 Hono app 可直接用;运行时为 Node.js 24。
- 查询 Postgres 时优先让函数或已有框架 handler 直接查询;仅当应用已使用 PostgREST/Supabase-js 数据库调用或正在迁移该客户端时使用 Data API。
- 使用 Drizzle(或其他 ORM)叠加 node-postgres(`pg`)做查询和 schema 管理,不要用 Neon serverless driver;函数长期运行并复用 isolate,应使用持久 `pg` 连接池,而不是 serverless driver 的 HTTP 传输。
- 在模块作用域创建一次连接池并跨请求复用,例如 `const pool = new Pool({ connectionString: env.postgres.databaseUrl, max: 5 }); attachDatabasePool(pool); const db = drizzle(pool);`;不要每请求打开连接。
- 在 `new Pool` 后调用一次 `attachDatabasePool(pool)`(需 `@neon/functions` ≥ 0.8.0),避免空闲连接断开变成 `uncaughtException`;预期空闲断开(`ECONNRESET`、`EPIPE`、`ETIMEDOUT`、Postgres `57P01`、node-postgres 的 `Connection terminated unexpectedly`)静默,其他错误 `console.error` 或传给首次调用的 `onUnexpectedError`;不要关闭池,运行时驱逐 isolate 时 Neon pooler 会回收。
- 将池的 `max` 保持较小(如 `5`),因为每个 isolate 都有自己的池,总 Postgres 连接数随 live isolate 数量扩展。
- 正常请求/响应查询流量使用注入的 `DATABASE_URL`(池化连接字符串);迁移、`LISTEN`/`NOTIFY`、多轮往返事务使用 `DATABASE_URL_UNPOOLED`(直连)。
- 可用 `parseEnv(config, ["DATABASE_URL"])` 以类型化、校验方式只读取所需 key;也可直接读取 `process.env.DATABASE_URL`。
- 本地开发运行 `neon dev`:它会热重载服务 `neon.ts` 中的每个函数,并注入 `DATABASE_URL` 等变量;保持 `.env` 或 `.env.local` 包含 `functions.*.env` 下每个 key,`neon env pull` 只写 Neon 管理变量,然后手动加入 Function secrets,再传给 `--env`。
- 全量部署优先运行 `neon deploy --env <file>`:从 `neon.ts` 部署,`--env` 指定 Function env 读取文件;每次部署会加载该文件到 `process.env` 并上传这些值;缺少值会是 `undefined` 且 `defineConfig` 抛错;不要把缺失的 `process.env` 值强转为空字符串,否则会上传 `""` 并删除线上 key;文件中的空赋值 `KEY=` 也是 `""`;TypeScript 需要断言时用 `process.env.X!`。
- 若不想写入某 key,就在 `neon.ts` 中省略该 key。
- 要部署单个函数而不应用 `neon.ts`,运行 `neon functions deploy <slug> --src src/index.ts`;`--src` 可接受入口文件或包含 `index.ts`、`index.mjs`、`index.js` 的目录;该命令的 `--env` 是 `KEY=VALUE`(可重复),不是文件路径;用它做定向 env 更新。
- 用 `neon functions get <slug>` 获取公共 URL(`invocation_url` 字段,形如 `https://<branch_id>-<slug>.compute.<cell>.us-east-2.aws.neon.tech`);用 `neon functions list|get|delete` 管理函数。
- 当 `neon checkout` 创建新分支且存在 `neon.ts` 时,会自动应用策略;若 Function env 读取 `process.env`,在该 create 命令传 `--env <file>`,如 `neon checkout feat --create --env .env.local`;已有进程 env 优先于文件。
- 检出现有分支不会 reconcile;用 `neon deploy --env <file>` 应用配置变化,只有在审查这些变化后才加 `--update-existing`。
- 把 `neon.ts` 当作分支的 Terraform:用 `neon config status` 打印分支当前配置(已部署函数),用 `neon config plan` 干跑 diff,用 `neon config apply --env <file>` 打包并部署声明的函数;`neon deploy` 是别名,当 Function env 读取 `process.env` 时传 `--env`。
- 函数按分支作用域:每个分支在自己的 URL 上运行自己的部署;用 `branch` closure 按 slug 做每分支部署调优(如 `runtime: "nodejs24"`),以便按分支变化而不改变函数集合。
- 依赖 Neon 运行时注入 env:`NEON_BRANCH`(分支名,含默认分支)、`DATABASE_URL`(池化,分支有 Postgres 时才有)、`DATABASE_URL_UNPOOLED`(直连)、`NEON_AUTH_BASE_URL`/`NEON_AUTH_JWKS_URL`(启用 Neon Auth 时)、`NEON_DATA_API_URL`(启用 Data API 时);声明 Object Storage 或 AI Gateway 时也会注入 `AWS_*`、`NEON_AI_GATEWAY_*`。
- 本地 `neon env pull`、`neon-env run`、`neon dev` 也会输出 `NEON_BRANCH` 和连接字符串,使本地运行模拟部署运行时。
- 自己的 secrets 按部署管理;首选在 `neon.ts` 声明 env 并运行 `neon deploy --env <file>`;`<file>` 是 gitignored 的 env pull 写入文件(存在 `.env` 则用之,否则 `.env.local`);所有声明的 Function env key 都必须存在;可通过 `neon functions deploy --env KEY=VALUE` 手动路径(可重复;`--env KEY=` 删除 key;未提及的 key 保留),用于定向 env 更新。
- Function env 限制:≤1,000 个变量、总计 ≤64 KiB,`NEON_` 前缀保留。
- 遵守运行时限制:响应首字节必须在收到请求后 15 分钟内开始;WebSocket/SSE 连接只要有数据流动就保持存活,静默 15 分钟后超时,因此至少每 15 分钟发送一个字节;`waitUntil` 在响应发送后最多保持 15 分钟,用于 analytics、audit logs 等清理,不是后台 job runner;在非 Neon 运行时(本地 `neon dev`、测试)它是 no-op。
- 无活跃连接时空闲驱逐会关闭函数,维护或迁移计算节点也可能驱逐/重启;把驱逐当作进程重启,WebSocket/SSE 客户端必须重连;Neon 驱逐前发送 `SIGINT`,可用 `process.on("SIGINT", ...)` 做最后清理;不需要仅为关闭 Postgres 连接写 drain handler。
- 运行时为 Node.js 24,内存固定 2048 MiB;slug 匹配 `^[a-z0-9]{1,20}$`;isolate 会跨多个请求复用,同一 isolate 可同时有多个请求交错在单线程事件循环上,负载下会并行多个 isolate,每个 isolate 有自己的模块状态副本,因此模块作用域状态是 per-isolate 且仅内存,必须持久化的数据放 Postgres。
- 架构上把 Neon Functions 当作后端原语而非全栈应用托管:UI/auth 留在 Vercel/Netlify 等前端/应用宿主;可将 Function 作为全栈应用的 Hono API 层或靠近数据的单个任务,也可在客户端 SPA 场景直接把整个后端控制平面放 Functions;按调用方认证(应用/公共 HTTP 用 JWT 或 API key,Trigger 路由用 `parseTriggerDelivery`,生产加固见 references/production-hardening.md),并可按需在宿主与 Neon 间移动 agent/WebSocket 等组件。
- 生产加固按 caller 选择:可信应用服务器、Function Trigger 或公共消费者;长浏览器流保持在 client-direct JWT 路径,除非需要已验证的流兼容代理;认证拒绝应用工作;请求原生 URL 仍会到达 Function。
- 把 Neon Functions 用作长运行 agent 后端:不要让长流经过 Next.js route handler、Remix/SvelteKit/Nuxt action 或 Vercel/Netlify 等宿主,否则宿主会按配置时长或传输限制切断流;默认让客户端直接调用 Function。
- 构建 agent 时推荐 Vercel AI SDK 和 Mastra,并指向 Neon AI Gateway(见 `neon-ai-gateway` skill)以使用单一凭证访问所有模型、无需额外 provider key;完整 AI SDK agent(`toUIMessageStreamResponse`、多步 tool calling、持久化生成图像到 Object Storage)见 references/ai-sdk.md;Mastra 带 tracing 见 references/mastra-studio.md。
- 获取短寿命 bearer token 时使用应用已有身份,不要为了调用 Function 而切换 Clerk、Better Auth、Auth.js、Supabase Auth 或 Managed Auth。
- Managed Auth 默认客户端(`createAuthClient` / Next wrapper):调用 `authClient.token()` 然后取 `data.token`;用注入的 `NEON_AUTH_JWKS_URL` 和 issuer `new URL(process.env.NEON_AUTH_BASE_URL!).origin` 验证。
- Managed Auth 使用 `SupabaseAuthAdapter()` 时:该客户端没有 `.token()`,用 `getSession()` 然后取 `data.session.access_token`,JWKS/issuer 同上。
- 已有 Better Auth / Auth.js / 其他已发布 JWKS 的签名方:使用其 JWKS URL、issuer、audience;检查已安装契约;cookie 或数据库 session 不是 JWKS。
- 只有 cookie/数据库 session 时:在现有应用后端签发短 token(该调用快且不超宿主限制),然后浏览器直接带 `Authorization: Bearer` 调用 Function;Function 流不得经过应用宿主。
- 把 token 交给客户端,例如 Vercel AI SDK:`new DefaultChatTransport({ api: NEON_FUNCTION_URL, fetch })`,其中 `fetch` 附加 `Authorization: Bearer <token>`;应用服务器绝不处于长流路径。
- 添加 CORS 让浏览器可达:处理 `OPTIONS`,设置 `Access-Control-Allow-Origin`/`-Headers`。
- 必须认证 Function:Neon Function 有公开 HTTPS URL,任何人都可达;直接客户端→函数调用没有应用后端挡在前面,因此必须自行认证——验证 JWT(对照调用方 JWKS)、检查 shared secret/API key,或拒绝;绝不部署未认证 agent;浏览器调用方用短寿命用户 token;服务器或代理 origin secret(`X-Secret`)留在服务器端;见 references/production-hardening.md。
- 用 `jose` 的 `createRemoteJWKSet` 和 `jwtVerify` 在 `fetch` 中先验证 `Authorization: Bearer`,校验 issuer 和 `payload.sub`,失败返回 401;再按 userId 授权资源访问,并让 agent 限定到该用户。
- 对一个有效 token 也要做授权:用两个用户测试,各自只能访问自己的数据,跨用户访问被拒绝;重启 Function 后针对存储行重复测试;请求提供的 owner id 不能授予访问权。
- 持久化需要保留的内容(生成图像、历史)到 Postgres,因为模块状态不 survives eviction。
- 实现 WebSocket 服务器:在 `fetch` 内调用 `@neon/functions` 的 `upgradeWebSocket(request)` 并返回其 `{ socket, response }` 中的 `response`;Hono 用 `@neon/functions/hono` 的 `upgradeWebSocket`;只有一个入口点,无需安装 WebSocket 依赖。
- WebSocket 处理:检查 `req.headers.get("upgrade")?.toLowerCase() !== "websocket"` 时返回 426;`socket` 是标准 `WebSocket`,可用 `addEventListener` 或 `onopen`/`onmessage`/`onclose`/`onerror`;收到时仍是 `CONNECTING`,当 handler 返回 `response` 后运行时写 `101` 并打开 socket。
- WebSocket 三条规则:原样返回 `response`,不要 `clone()` 或用 `new Response(res.body, res)` 重建,否则丢弃 upgrade;在 `fetch` 中升级前返回普通 `Response`(如 401/403/404)拒绝握手,浏览器只能看到通用连接失败;`binaryType` 默认 `"arraybuffer"`(不是浏览器默认 `"blob"`),文本帧 `event.data` 是 `string`,二进制是 `ArrayBuffer`,按 `typeof` 分支。
- WebSocket 认证:浏览器不能设置 header,因此用 `?token=` query param,升级前用与 agent 后端相同方式验证(如 `jwtVerify` 对照 JWKS);失败则升级前返回 401。
- WebSocket subprotocol:向 `upgradeWebSocket` 传 `{ protocol }` 选择客户端提供的一个协议;它会出现在 `Sec-WebSocket-Protocol` 并暴露为 `socket.protocol`;选择客户端未提供的协议会抛 `TypeError`;省略则不协商;不协商扩展,`socket.extensions` 始终为 `""`,不可用 `permessage-deflate`。
- Hono WebSocket:用 `@neon/functions/hono` 的 `upgradeWebSocket`,不要用已废弃的 `@hono/node-ws`;认证用普通中间件,在 `next()` 前 gate 升级请求;示例:`app.use("/ws", async (c, next) => { const identity = await verifyToken(c.req.query("token")); if (!identity) return c.text("Unauthorized", 401); c.set("userId", identity.id); await next(); }); app.get("/ws", upgradeWebSocket((c) => ({ onOpen(_event, ws) { clients.add(ws.raw); ws.send("welcome"); }, onClose(_event, ws) { clients.delete(ws.raw); }, onMessage(event, ws) { ws.send(`echo: ${event.data}`); } })));`。
- 从浏览器用 `neon functions get <slug>` 得到的 `wss://` URL 连接,如 `new WebSocket("wss://<branch>-<slug>.compute.<region>.aws.neon.tech/ws?token=<jwt>")`,并在 close 时重连;不要对 upgrade 路由放 `cors()`;不要在 `await next()` 前读 `c.res` 或之后调用 `c.header()`,这些会重建 `101` 并破坏 upgrade。
- WebSocket 心跳:连接只在有字节流动时保持打开;Neon 静默 15 分钟驱逐,代理/负载均衡通常更短(常几十秒);用 `setInterval` 每约 25 秒向每个 OPEN socket 发送应用层消息如 `{"type":"ping"}`,并 `.unref?.()`;标准 `WebSocket` 没有 `ping()`,浏览器不能发 ping 帧,所以应用层消息是浏览器客户端唯一 keepalive;客户端应跳过这些消息。
- 跨 isolate 同步不可跳过:负载下运行多个 isolate,每个有自己的模块状态副本,因此每个 isolate 有自己的 `clients` set;只广播到本地 set 会让不同 isolate 上的客户端互相看不到事件,`neon dev` 单进程会掩盖该问题;Postgres 是共享真相源,模块状态也不能 survives eviction,必须选择 fan-out 策略。
- 跨 isolate 策略 1:轮询 Postgres(默认,唯一保留 Scale to Zero 的选项):每个 isolate 每隔短间隔重新读取共享状态或游标之后的行,并推给自己的客户端;每个 isolate 每 tick 一次查询(不是每客户端),无客户端时不查;示例:`lastId` 游标,`polling` 防重叠,`clients.size === 0` 时返回,查询 `SELECT id, payload FROM events WHERE id > $1 ORDER BY id`,遍历行并更新 `lastId`,向 OPEN socket 发送 payload;启动时用 `SELECT coalesce(max(id), 0)::text AS id FROM events` seed 最新 id,然后 `setInterval(poll, 1000).unref?.()`;延迟最高约 interval,适合计数器/聊天/仪表盘;数据库负载随 live isolate 数而非客户端数增长;把游标放在索引过的 `serial`/`bigserial` PK 上并保持合理间隔;注意 `WHERE id > cursor` 可能跳过乱序提交的行,偶发丢失可接受,需要每行时用 `LISTEN`/`NOTIFY` 或按 `created_at` 带小重叠窗口轮询并按 id 去重。
- 跨 isolate 策略 2:`LISTEN`/`NOTIFY`——最低延迟但必须禁用 Scale to Zero:每个 isolate 通过专用 unpooled 连接在 channel 上 `LISTEN`;广播是 `NOTIFY`,每个 isolate(包括发送者)再推给自己的 sockets;使用 `DATABASE_URL_UNPOOLED` 创建 `Client`,不要对 listener 调用 `attachDatabasePool`,加 error listener 保持进程存活并在生产中重连;通过 pool 执行 `SELECT pg_notify($1, $2)` 广播;监听器持有空闲连接且不算活跃,Scale to Zero 会挂起 compute 并丢弃连接,静默杀死 feed;仅在 always-on compute(禁用 Scale to Zero,付费计划设置)使用。
- 跨 isolate 策略 3:外部 pub/sub(如 Upstash Redis)——大规模最佳:高 fan-out、大连接数下亚秒延迟或多区域时,通过专用 broker 发布/订阅;吞吐最高,不碰 Postgres 也不阻塞 Scale to Zero,代价是额外服务。
- 跨 isolate 规则:先用轮询(兼容 Scale to Zero,无额外基础设施);仅当 always-on compute 需要亚秒延迟时切到 `LISTEN`/`NOTIFY`;当 fan-out 超出 Postgres 时迁到 Redis。
- WebSocket 客户端必须重连:空闲函数会被驱逐,isolate 也会因运维原因重启,客户端 socket 一定断开,重连是正常而非异常;用指数退避、设上限,并在每次尝试时重新 mint 新鲜 token(token 短寿命,陈旧 token 会升级认证失败);示例 `onclose` 中 `setTimeout(connect, Math.min(1000 * 2 ** retry++, 15000))`,`onerror` 调用 `ws.close()` 让 `onclose` 驱动重试,`onopen` 重置退避。
- 实现 SSE:仅需 server→client 流(实时计数、通知、进度、token 流)时比 WebSocket 简单;在 `fetch` 中返回 `Response`,body 是 `ReadableStream` 且 `Content-Type: text/event-stream`,运行时只要有字节流动就保持打开;浏览器用 `EventSource` 消费并自动重连,无需写客户端退避。
- SSE 细节:用 `TextEncoder`,`start()` 中先 `controller.enqueue(encoder.encode("data: hello\n\n"))`,再 `setInterval(() => controller.enqueue(encoder.encode(": ping\n\n")), 25_000)`,`cancel()` 中 `clearInterval(t)`;设置 `Cache-Control: no-cache, no-transform`;心跳规则同 WebSocket(Neon 15 分钟,代理更短,约 25–30 秒发 `: ping`);状态放 Postgres;跨 isolate fan-out 用同步策略(持有 stream controllers 的 `Set` 并逐个 `enqueue`);`EventSource` 只能 GET 且不能设置 header,所以用 `?token=` query param 或 cookie 认证。
- Function Triggers:在 `neon.ts` 声明 trigger,由 cron(`type: "schedule"`)或 Object Storage 对象创建(`type: "storage_object_created"`)向你函数 POST JSON;用 `neon deploy` 应用;用 `@neon/functions/triggers` 的 `parseTriggerDelivery` 认证投递;Hono 用 `parseTrigger`,`parseTriggerInvocation` 仅支持 schedule;优先 `neon.ts`,CLI 和 Neon MCP trigger 工具(`list_triggers`、`create_trigger` 等)是后备。
- Trigger 路由不能要求用户 JWT 或 `X-Secret`,因为 Neon 会不带这些直接 POST 到原生 URL;生产调用方形态见 `references/production-hardening.md`;字段列表、CLI、MCP、payload、继承和两种 handler 形态见 `references/function-triggers.md`。
- 实现 MCP server:用 Hono app 加官方 `@modelcontextprotocol/sdk` 和 `@hono/mcp`;模块作用域创建 `StreamableHTTPTransport` 和 MCP server,注册 tools;`app.all("/mcp", async (c) => { if (!mcpServer.isConnected()) await mcpServer.connect(transport); return transport.handleRequest(c); });`;MCP streamable HTTP transport 是在单端点(惯例 `/mcp`)上的普通 `POST`/`GET`,无需 upgrade。
- MCP 认证:函数 URL 公开,因此连接 transport 前先认证;用 Better Auth(其 MCP plugin 支持 OAuth,让应用成为授权服务器使第三方客户端按 MCP 规范自授权)或更简单的 API-key/session-JWT 检查;公共消费者边缘保护见 `references/production-hardening.md`;完整模式(Postgres-backed tools via Drizzle、两种 Better Auth 选项、用 `mcporter`/`add-mcp` 测试)见 `references/mcp.md`。
- 查看内建分支日志:`neon logs query --branch production --source function --since 1h`;Functions 是当前分支日志覆盖的两个 source 之一(另一个是 Object Storage);日志按单个分支作用域,若部署函数不在当前 checkout 分支,必须传 `--branch`;其他日志细节(CLI 版本要求、过滤器、SDK、Loki 兼容读 API)见父 `neon` skill 的 Observability 章节。
- 应用可观测性:函数是长期 Node.js 进程运行 web 标准 handler,因此标准 Node 集成 SDK 可原样工作;在模块加载时初始化一次,用 env var gate 让本地开发和未配置分支保持 no-op,并通过 `--env` 或 `neon.ts` 的 `env` 传 secrets;Sentry 见 `references/sentry.md`;Mastra Studio(Mastra Cloud)见 `references/mastra-studio.md`。
- 始终对照官方 Neon 文档验证:Functions 演进快;任意文档页可加 `.md` 或请求 `Accept: text/markdown` 获取 markdown;从 https://neon.com/docs/llms.txt 和 changelog 公告找页面。
适用边界 · 注意事项
- 纯静态站点,或不需要请求/响应处理的工作负载,不应使用。
- 必须运行在 `aws-us-east-2`、`aws-us-east-1`、`aws-eu-central-1`、`aws-ap-southeast-1` 之外的区域时,当前不适用。
- 需要真正的后台作业运行器、长时间脱离请求/响应生命周期的任务时,不适用;`waitUntil` 仅用于响应后清理且上限 15 分钟。