mcpField
N
neon-functions

neon-functions

github.com/neondatabase云与 DevOps10729.2k 次安装1 天前更新
代码仓库

关于 neon-functions

neon-functions 是面向开发者的 Neon Functions 技能,指导在 Neon 数据库分支旁定义、测试和部署长时间运行的 Node.js HTTP 函数。它涵盖本地开发、分支隔离及函数管理。

主要功能

  1. 01定义和部署 Node.js HTTP 函数
  2. 02使用 neon dev 本地热重载
  3. 03自动注入数据库连接变量
  4. 04函数随数据库分支隔离
  5. 05支持定时与对象存储触发

适用场景

  1. 01部署连接 Postgres 的 API
  2. 02运行 SSE 或 WebSocket 服务
  3. 03处理 Webhook 和机器人请求
  4. 04设置定时或对象创建任务

MCP 相关

文档指出 Neon Functions 可用于托管 MCP 服务。

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/neondatabase/agent-skills --skill neon-functions

安装后在 Claude Code 中输入/neon-functions确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
ai-sdk.md
function-triggers.md
mastra-studio.md
mcp.md
native-binaries.md
production-hardening.md
sentry.md
sse.md

详细介绍

首先:使用父级 neon 技能了解 Neon 概览、Neon 入门、Neon 开发最佳实践等内容。

如果尚未安装 neon 技能,请从 https://neon.com/docs/ai/skills/neon/SKILL.md 获取,或使用以下命令安装:

neon skills -s neon -y

Neon Functions

目前可在 aws-us-east-2、aws-us-east-1、aws-eu-central-1 和 aws-ap-southeast-1 使用。

Neon Functions 是部署到 Neon 分支上的长时间运行的 Node.js HTTP 处理程序。每个函数都有一个公开的 HTTPS URL,在与数据库相同的区域运行;如果分支中有 Postgres,还会自动注入 DATABASE_URL。你可以通过一直在使用的同一套 Neon CLI、neon.ts 和 API 来部署和管理它们。

使用此技能,帮助用户在数据库旁定义、在本地运行、部署和管理函数。交付已部署函数及其调用 URL、可正常工作的本地 neon dev 开发循环,或基于 Neon 官方文档给出准确答复。

适用场景

当工作负载属于请求/响应处理程序,并且受益于持续运行且靠近数据时,可考虑使用 Neon Functions:

  • 运行时间超过 Lambda 类限制的长时间运行请求/响应流程。 每个请求需要多次调用 LLM 和工具的 Agent,或图像/视频生成任务,经常会超出传统无服务器函数约 10–60 秒的执行上限和较短的流式传输窗口。Neon Functions 可长时间运行:处理程序只需在 15 分钟内开始响应,且只要数据持续传输,打开的流就会一直保持活动状态。这为实际的 Agent 工作负载提供了充足的余量。
  • 无需额外接入 Redis 的有状态流式传输。 由于函数会在整个请求期间保持活动状态,因此可以托管 SSE 端点或 WebSocket 服务,并在进程内保持连接打开——无需仅为保持流状态一致而使用外部状态存储(如 Redis)。模块级状态(pg 连接池、内存计数器)会在同一隔离实例上的多个请求之间保留。
  • 必须靠近 Postgres 的计算任务。 函数与分支的数据库在同一区域运行,因此每次查询都无需跨区域往返。系统会自动为你注入 DATABASE_URL。
  • 随数据一起分支的后端。 每个分支都会在自己的 URL 上运行自己的函数版本,并使用隔离的数据库(以及存储和网关)状态。预览部署、CI 和开发环境各自拥有独立完整的后端——部署到子分支不会影响父分支。
  • 从函数(或现有框架处理程序)查询 Postgres。 优先采用这种方式,而不是使用 Data API。当应用已经使用 PostgREST 或 Supabase-js 进行数据库调用,或正在迁移这类客户端时,再使用 Data API。
  • Webhook、机器人和响应后的工作。 将任务分发到多个数据库写入操作的 Webhook 处理程序、Discord/WebSocket 机器人,以及通过 waitUntil 执行的即发即弃后续任务(分析、审计日志)都适用。
  • 定期执行 HTTP 任务。 Function Trigger 会按 Cron 向函数发送 POST(type: "schedule"),或在 Object Storage 中创建对象时发送 POST(type: "storage_object_created")。使用相同的 fetch 处理程序,并遵循相同的 15 分钟首字节时间限制。请参阅函数触发器。

如果工作负载是纯静态网站,或目前必须在受支持区域(aws-us-east-2、aws-us-east-1、aws-eu-central-1 和 aws-ap-southeast-1)以外运行,则目前不适合使用此工具(请参阅超时和运行时限制和可用性)。

它的功能

  • 长期运行与无服务器 — 专为 WebSocket 服务(参见 WebSocket 服务)、SSE 端点(参见 服务器发送事件(SSE))、长时间运行的 Agent HTTP 流和 API 构建。空闲时仍会缩至零。
  • 符合 Web 标准的处理程序 — 函数是任何带有 fetch(request) 方法并返回 Response 的默认导出(兼容 Workers/WinterTC)。Hono 应用导出的形式正好符合这一要求,因此 export default app 可以直接使用。运行于 Node.js 24,因此所有 Node API 均可用。
  • 靠近数据库 — 在分支所在的区域运行;如果分支使用 Postgres,DATABASE_URL 会自动注入。
  • 可分支 — 每个分支都在自己的 URL 上运行独立版本的函数,并使用各自隔离的状态。
  • 相同的 CLI/API — 通过 neon、neon.ts 或 Neon API 进行部署和管理。
  • 函数触发器 — Neon 会在定时任务触发或对象存储上传时向函数发送 POST 请求。参见 函数触发器。

可用性

开始设置前,请先确认这一前提:Neon Functions 目前仅在 aws-us-east-2、aws-us-east-1、aws-eu-central-1 和 aws-ap-southeast-1 区域可用。请确认用户的 Neon 项目位于其中一个区域。

架构:函数的定位

Neon(包括 Functions)提供的是后端基础组件,而非全栈应用托管。请将应用托管在 Vercel(或 Netlify 等其他前端/应用托管平台);Functions 是位于数据旁边、长期运行且有状态的后端部分。它们可以通过以下两种方式与该平台配合:

  • 为全栈应用添加函数。 部署在 Vercel(或 Netlify)上的 Next.js / TanStack Start 应用负责 UI、身份验证(Managed Auth、Better Auth、Clerk 或其他 IdP),并直接与 Lakebase Postgres 和 Object Storage 通信。添加一个函数作为 Hono API 层,供 Web 应用和其他客户端使用,或用于靠近数据运行的单项任务:Object Storage 上传、AI Agent、Discord 机器人、WebSocket 或 SSE 服务。(客户端直连模式请参见 将函数用作 Agent 后端。)
  • 在 Functions 上运行整个后端控制平面。 尤其是当前端为纯客户端应用时——例如托管在 Vercel 或 Netlify 上的 TanStack Router、客户端模式下的 React Router 及类似 SPA——客户端会直接调用函数。构建 REST API 和请求/响应式 Agent、托管 MCP 服务,并运行任何有状态或需要靠近 Postgres 和 Object Storage 的服务。

无论采用哪种方式,都应根据调用方进行身份验证:面向应用和公开 HTTP 的请求使用 JWT 或 API 密钥(参见 将函数用作 Agent 后端下的 WARNING);函数触发器路由使用 parseTriggerDelivery;生产环境加固请参见生产环境加固。由于函数就是你的后端,你可以在托管平台和 Neon 之间迁移组件——当 Agent 或有状态 WebSocket 服务需要更长的运行时间时,将其迁移到函数上;有需要时也可以迁回。

优先使用函数或现有框架处理程序查询 Postgres。如果应用已使用 PostgREST/Supabase-js 数据库调用,或正在迁移该客户端,则使用 Data API。

生产环境加固

向外部开放生产环境路由前,请阅读生产环境加固。

根据调用方选择方案:受信任的应用服务器、函数触发器或公开使用者。除非确实需要经过验证且兼容流式传输的代理,否则应让浏览器长连接流使用客户端直连 JWT 方案。身份验证会拒绝应用请求;但发往原生 URL 的请求仍会到达函数。

设置

在 neon.ts 中声明函数(有关分支优先工作流和 neon.ts 基础知识,请参阅 neon skill)。添加 @neon/config,并在 functions 下按 slug 声明函数:

// neon.ts
import { defineConfig } from "@neon/config/v1";

export default defineConfig({
  functions: {
    todos: {
      // slug: ^[a-z0-9]{1,20}$ — lowercase letters/digits, no hyphens
      name: "todo api", // display label only
      source: "src/index.ts", // entry file, relative to neon.ts
    },
  },
});

slug 是函数的永久标识(会显示在调用 URL 和 CLI 命令中),首次部署后无法更改。使用 name 设置便于人阅读的标签。

一个最简函数——通过注入的 DATABASE_URL 查询分支 Postgres 的 Hono 应用:

// src/index.ts
import { Hono } from "hono";
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import { parseEnv } from "@neon/env";
import { attachDatabasePool } from "@neon/functions";
import config from "../neon";
import { todos } from "./db/schema";

const env = parseEnv(config);
const pool = new Pool({ connectionString: env.postgres.databaseUrl, max: 5 });
attachDatabasePool(pool);
const db = drizzle(pool);

const app = new Hono();
app.get("/", (c) => c.text("Neon + Hono + Drizzle"));
app.post("/todos", async (c) => {
  const { text } = await c.req.json<{ text: string }>();
  const [row] = await db.insert(todos).values({ text }).returning();
  return c.json(row, 201);
});
app.get("/todos", async (c) => c.json(await db.select().from(todos)));

export default app;

在模块作用域创建 pg 连接池(同一隔离环境中的请求会复用该连接池),并将 max 设得较小(例如 5),因为每个隔离环境都有自己的连接池。调用 attachDatabasePool(pool),避免空闲断开连接导致 uncaughtException——请参阅连接到 Postgres。

parseEnv(config) 要求提供配置所隐含的_所有_变量。只通过池化 URL 连接 Postgres 的函数,可以将其范围限定为仅包含该键——此时 parseEnv 只会验证并返回你指定的内容(键会根据 neon.ts 中的配置自动补全):

const { postgres } = parseEnv(config, ["DATABASE_URL"]); // not the unpooled URL, auth, etc.
const pool = new Pool({ connectionString: postgres.databaseUrl, max: 5 });
attachDatabasePool(pool);

本地开发与部署

neon dev      # serves every function in neon.ts with hot reload; injects DATABASE_URL & friends
neon deploy --env <file>   # preferred full deploy from neon.ts; --env is the file Function env is read from

确保 .env 或 .env.local 中包含 functions.*.env 下的所有键。neon env pull 只会写入 Neon 管理的变量;将 Function 密钥添加到该文件,然后通过 --env 传入。neon deploy --env <file> 每次都会将该文件加载到 process.env,然后上传这些值。缺失的值为 undefined,defineConfig 会抛出错误。如果不想写入某个键,请从 neon.ts 中省略它。切勿将缺失的 process.env 值强制转换为空字符串(这会上传 "" 并删除线上键)。空赋值(KEY=)同样会变成 ""。TypeScript 需要断言时,请使用 process.env.X!。

若要在不应用 neon.ts 的情况下部署单个函数:neon functions deploy <slug> --src src/index.ts(--src 接受入口文件,或包含 index.ts、index.mjs 或 index.js 的目录)。该命令的 --env 参数是 KEY=VALUE(可重复指定),而不是文件路径。可用此方式有针对性地更新环境变量。使用 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 时,会自动应用策略。创建分支时传入 --env <file>,以解析读取 process.env 的 Function 环境变量(neon checkout feat --create --env .env.local)。现有进程环境变量优先于文件中的值。切换到已有分支不会进行协调——使用 neon deploy --env <file> 应用配置更改(仅在检查这些更改后才添加 --update-existing)。

原生二进制文件与自定义构建

默认部署会将 source 打包到一个 index.mjs 中,无法容纳编译后的二进制文件。请将二进制文件作为单独的归档文件随包发布:

  • 带有 Node 插件的 npm 包(sharp):externalPackages: ["sharp"]。部署会安装其 linux-arm64 glibc 版本并复制文件。
  • 自行构建的输出(.node、.so、.wasm、数据文件):使用带预构建目录的 bundler: "none",或内联 bundler: async (fn) => ({ "index.mjs": …, "bin/tool": … })。
  • 需要通过子进程启动的独立可执行文件:按上述方式随包发布,然后在启动时将其复制到 /run 并执行 chmod 755。/opt/function 中的文件在只读文件系统上会以 644 模式提供,而 /tmp 带有 noexec 标记。

运行时目标、优缺点、可用代码片段和大小限制:请参阅 references/native-binaries.md。

Neon 基础设施即代码(neon.ts)

设置中的 functions 块属于 neon.ts,也就是 Neon 的基础设施即代码文件——一个 TypeScript 文件即可在版本控制中声明每个函数(其 source、显示名称 name 和 env),以及任何其他分支服务(完整参考请参阅 neon 技能)。请将它视为分支的 Terraform:

neon config status   # print the branch's live config (deployed functions)
neon config plan     # dry-run diff of what apply would change
neon config apply --env <file>  # bundle + deploy the declared functions  (neon deploy is an alias; pass --env when Function env reads process.env)

函数按分支范围隔离:每个分支都在自己的 URL 上运行自己的部署。存在 neon.ts 时,neon checkout 会在_创建_分支时应用策略。如果函数环境变量读取 process.env,请在创建分支时传入 --env <file>。检出_现有_分支不会重新部署——运行 neon deploy --env <file> 以应用更改。

每个分支的部署调优设置(例如 runtime)位于 branch 闭包中,并以 slug 为键,因此可以按分支变化,而不必更改现有函数:

export default defineConfig({
  functions: { todos: { name: "todo api", source: "src/index.ts" } },
  branch: (branch) => ({
    functions: { todos: { runtime: "nodejs24" } },
  }),
});

环境变量

Neon 会在运行时注入按分支范围隔离的连接字符串和服务 URL——无需声明这些变量,也无需在部署时传入:

变量说明
NEON_BRANCH分支名称(例如 main、preview/foo)。注入到每个分支中,包括默认分支。
DATABASE_URL池化连接字符串。适用于大多数查询。仅当分支使用 Postgres 时才会提供。
DATABASE_URL_UNPOOLED直接连接。适用于迁移、LISTEN/NOTIFY、多次往返的事务。
NEON_AUTH_BASE_URL分支启用 Neon Auth 时提供。
NEON_AUTH_JWKS_URL分支启用 Neon Auth 时提供。用于验证 Managed Auth JWT 的 JWKS。
NEON_DATA_API_URL分支启用 Data API 时提供。

声明对象存储(AWS_*)和 AI Gateway(NEON_AI_GATEWAY_*)服务时,也会注入相应变量——请参阅 neon-object-storage 和 neon-ai-gateway 技能。

neon env pull / neon-env run / neon dev 也会将 NEON_BRANCH(以及连接字符串)写入本地开发环境,因此本地运行与已部署的运行时保持一致。

你自己的密钥按部署设置。推荐做法:在 neon.ts 中声明,并运行 neon deploy --env <file>。<file> 是 env pull 已写入的、被 Git 忽略的文件(如果 .env 文件存在则使用它,否则使用 .env.local)。Env pull 只会写入 Neon 管理的变量;请将函数密钥添加到该文件中。所有已声明的函数环境变量键都必须存在。如果不想写入某个键,请将其从 neon.ts 中省略。undefined 表示你要求写入该键,但值缺失(defineConfig 会抛出异常)。绝不要将缺失的 process.env 值强制转换为空字符串:这会上传 "" 并删除线上键。在文件中进行空赋值(KEY=)也会变成 ""。如果 TypeScript 需要断言,请使用 process.env.X!,并确保文件中有该值:

functions: {
  todos: {
    name: "todo api",
    source: "src/index.ts",
    env: { RESEND_API_KEY: process.env.RESEND_API_KEY! },
  },
}

neon functions deploy --env KEY=VALUE 是手动设置方式(可重复使用;--env KEY= 会删除某个键;未提及的键会保留)。请将其用于有针对性的环境变量更新,而不是完整应用 neon.ts。

将函数密钥加载到 env pull 写入的同一文件中,然后运行 neon deploy --env <file>。使用 neon env pull 将分支的 Neon 管理变量拉取到磁盘,以供本地开发使用(link/checkout 会自动执行此操作;传入 --no-env-pull 可跳过,并使用 neon-env run -- <cmd> 注入运行时环境变量)。限制:最多 1,000 个变量,总计不超过 64 KiB,且 NEON_ 前缀为保留前缀。

连接到 Postgres

如果分支启用了 Postgres,Neon 会在运行时注入连接字符串——你无需声明它们、在部署时传入或硬编码任何内容。你会用到以下两个:

  • DATABASE_URL — 池化连接字符串(通过 Neon 的连接池路由)。用于常规请求/响应查询流量。保持不带前缀,因为每个 Postgres ORM(Drizzle、Prisma、Knex,等等)默认都会读取 DATABASE_URL。
  • DATABASE_URL_UNPOOLED — 指向同一数据库的直连连接字符串。用于迁移、LISTEN/NOTIFY 和长时间运行的多语句事务。

**使用 Drizzle(或其他 ORM)搭配 node-postgres(pg)**执行查询和管理架构,而不是使用 Neon 的无服务器驱动。函数会长时间运行,并在多个请求间复用同一个 isolate,因此持久化的 pg 连接池更合适;无服务器驱动的 HTTP 传输是为完全隔离的 lambda 风格运行时设计的。

在模块作用域仅创建一次连接池,并在请求间复用——不要为每个请求单独建立连接:

import { attachDatabasePool } from "@neon/functions";
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";

const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 5 });
attachDatabasePool(pool);
const db = drizzle(pool);

node-postgres 会在连接池上以 error 事件的形式报告空闲客户端故障。如果没有监听器,这会导致 uncaughtException,Node 也会退出该 isolate。在 new Pool 后调用一次 attachDatabasePool(pool)。要求 @neon/functions ≥ 0.8.0。预期的空闲断开连接(ECONNRESET、EPIPE、ETIMEDOUT、Postgres 57P01、node-postgres 的 Connection terminated unexpectedly)会被静默处理。其他情况会调用 console.error;如果你在首次调用时传入 onUnexpectedError,则会调用它。首次调用优先;后续调用即使传入 onUnexpectedError 也会被忽略,并发出警告。此操作不会关闭连接池。

建议使用连接池,因为一个 isolate 会在多个请求间复用(而且同一个 isolate 上可能同时有多个请求正在处理——请参阅超时和运行时限制)。模块作用域的连接池会在冷启动时打开一次,之后由该 isolate 处理的每个请求共享,因此你可以分摊连接建立开销,而不必为每个请求付费,并避免负载过高时耗尽 Postgres 连接。

将 max 设得较小(例如 5):每个 isolate 都有自己的连接池,因此 Postgres 的总连接数会随存活 isolate 的数量增长。无需在关闭时关闭连接池——运行时淘汰 isolate 时会发送 SIGINT/SIGTERM,Neon 的连接池会为你回收这些连接,因此显式清理处理程序是多余的。

直接读取 process.env.DATABASE_URL 在任何环境中都可行。设置中的函数使用 @neon/env 的 parseEnv(config) 以类型安全且经过验证的方式读取相同的值——两种方式都可以。

超时和运行时限制

函数可以长期运行,但仍然是无服务器的——它们是请求/响应运行时,而不是后台任务运行器。硬性限制如下:

  • 首字节时间:15 分钟。 你的处理程序必须在收到请求后的 15 分钟内_开始_返回响应。大多数处理程序只需几秒即可完成;设置 15 分钟上限,是为了给图像/视频生成等 Agent 工作负载留出足够时间。
  • 心跳:15 分钟。 只要有数据流动,打开的 WebSocket/SSE 连接就会保持活动状态。只有连接静默时才会触发超时——每 15 分钟至少发送一个字节,即可让安静的数据流保持活动状态。
  • waitUntil:15 分钟。 使用 @neon/functions 中的 waitUntil 注册的工作,会在响应发送后继续保持调用活动,最长 15 分钟——适用于分析数据写入和审计日志等清理工作,而不是后台任务运行器。在 Neon 运行时之外(本地运行 neon dev、测试),它不起作用:Promise 仍会运行,但不会被跟踪。
  • 空闲回收。 没有活动连接时,Neon 会关闭函数;出于运维原因也可能回收并重启函数,例如维护或将函数迁移到其他计算节点(活动函数可能会先运行数小时)。请将回收视为进程重启——WebSocket/SSE 客户端必须重新连接。Neon 会在回收前发送 SIGINT,因此可以使用 process.on("SIGINT", ...) 处理程序,检测函数即将被回收并执行最后的清理工作。你无需仅为关闭 Postgres 连接而添加处理程序——Neon 的连接池会自行回收这些连接。
  • 运行时: Node.js 24,内存固定为 2048 MiB。Slug 必须匹配 ^[a-z0-9]{1,20}$。同一个 isolate 会被多个请求重复使用——多个请求可以同时在同一个 isolate 中处理中(在 Node.js 单线程事件循环上交错执行);在负载较高时,运行时会并行运行多个 isolate,每个 isolate 都有各自的模块状态副本。因此,保存在模块作用域中的状态是每个 isolate 独有的(由该 isolate 处理的所有请求共享),且只存在于内存中——必须在回收后保留的数据应持久化到 Postgres。这种复用机制正是你应在模块作用域中创建一次连接池,而不是为每个请求创建连接池的原因(参见连接到 Postgres)。

将函数用作 Agent 后端(Next.js 和类似框架)

Neon Function 非常适合作为 AI Agent 的运行环境,正是因为它不会像 Lambda 风格的无服务器服务那样超时(时限为 15 分钟,参见超时和运行时限制)。如果通过托管在 Vercel、Netlify 等平台上的 Next.js 路由处理程序、Remix/SvelteKit/Nuxt action 或类似组件代理数据流,一旦超过相应平台配置的运行时长或传输限制,数据流就会中断,即使 Function 仍会继续运行。默认情况下,请使用下方的客户端直连 JWT 路径。有关使用兼容流式传输的代理(HTTP 触发的 Cloudflare Worker,验证数据流后使用)的例外情况,请参阅生产环境加固。

构建 Agent 本身。 推荐使用 Vercel AI SDK 和 Mastra 构建 Agent——将任一方案连接到 Neon AI Gateway(参见 neon-ai-gateway 技能),即可用一组凭证访问所有模型,无需额外的提供商密钥。有关以 Function 形式运行的完整 AI SDK Agent(使用 toUIMessageStreamResponse 进行流式传输、在 Postgres 旁进行多步工具调用,并将生成的图像持久化到 Object Storage),请参阅 references/ai-sdk.md;有关包含内置跟踪功能的对应 Mastra 方案,请参阅 references/mastra-studio.md。

解决方法:从客户端直接调用函数。 不要通过应用服务器转发长时间运行的请求。

Browser ──(Authorization: Bearer <JWT>)──▶  Neon Function (agent)   ✅ no host timeout
Browser ──▶ your app backend ──▶ Neon Function                       ❌ host cuts the stream
  • 从应用已经使用的身份提供方获取短期 bearer token。不要为了调用 Function 而切换到 Clerk、Better Auth、Auth.js、Supabase Auth 或 Managed Auth。
    • Managed Auth,默认客户端(createAuthClient / Next 封装):调用 authClient.token(),然后获取 data.token。使用注入的 NEON_AUTH_JWKS_URL 和签发者 new URL(process.env.NEON_AUTH_BASE_URL!).origin 进行验证。
    • 使用 SupabaseAuthAdapter() 的 Managed Auth:该客户端没有 .token()。调用 getSession(),然后获取 data.session.access_token。使用与上述相同的 JWKS 和签发者。
    • 已有的 Better Auth / Auth.js / 其他已发布 JWKS 的签发方:使用其 JWKS URL、签发者和受众。检查已安装的契约;Cookie 或数据库会话不是 JWKS。
    • 仅有 Cookie / 数据库会话:在现有应用后端签发短期 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)。

警告

Neon Function 具有公开的 HTTPS URL——任何人都能访问。 客户端直接调用 Function,意味着前面没有应用后端来控制访问,因此你必须自行对 Function 进行身份验证。 根据调用方的 JWKS 验证 JWT,检查共享密钥 / API 密钥,或拒绝请求。切勿部署未经身份验证的 Agent。浏览器调用方使用短期用户 token。服务器或代理来源的密钥(X-Secret)必须留在服务器端;参见生产环境加固。

// src/index.ts — verify the caller before doing any work
import { createRemoteJWKSet, jwtVerify } from "jose";

const jwks = createRemoteJWKSet(new URL(process.env.NEON_AUTH_JWKS_URL!));
const issuer = new URL(process.env.NEON_AUTH_BASE_URL!).origin;

export default {
  async fetch(request: Request) {
    if (request.method === "OPTIONS")
      return new Response(null, { status: 204, headers: cors(request) });

    const auth = request.headers.get("authorization");
    if (!auth?.toLowerCase().startsWith("bearer ")) {
      return new Response("Unauthorized", {
        status: 401,
        headers: cors(request),
      });
    }
    let userId: string;
    try {
      const { payload } = await jwtVerify(auth.slice(7), jwks, { issuer });
      if (!payload.sub) {
        return new Response("Unauthorized", {
          status: 401,
          headers: cors(request),
        });
      }
      userId = payload.sub;
    } catch {
      return new Response("Unauthorized", {
        status: 401,
        headers: cors(request),
      });
    }
    // Authorize resource access by userId, then run the agent scoped to that user.
    // ... return result.toUIMessageStreamResponse({ headers: cors(request) })
  },
};

该代码片段用于 Managed Auth 验证。默认客户端通过 .token()(data.token)签发 bearer token;使用 SupabaseAuthAdapter() 时,则调用 getSession(),然后获取 data.session.access_token。对于其他身份提供方,通过 Function env 传入该应用的 JWKS URL 和签发者(参见环境变量),并仅在 token 契约要求时包含 audience。https://neon.com/docs/compute/functions/authentication.md

有效 token 并不意味着有权读取其他用户的行。使用两个用户进行测试:每个用户都能访问自己的数据,但访问其他用户的数据会被拒绝。将 Function 重启并针对已存储的行再次测试。请求中提供的 owner id 不能授予访问权限。

需要保留的内容(例如生成的图像、历史记录)请存储在 Postgres 中——模块状态在驱逐后不会保留。

WebSocket 服务

WebSocket 服务是典型的 Functions 工作负载:长时间运行的处理程序会在进程内保持连接打开,无需外部状态存储即可确保数据流连贯。只要有字节流动,连接就会保持活动状态(15 分钟心跳,见超时)。

在 fetch 中升级。 调用 @neon/functions 中的 upgradeWebSocket(request),并返回它提供的响应。Hono 应用通过 @neon/functions/hono 使用相同的基本功能(见下方的 Hono)。只有一个入口点,无需安装 WebSocket 依赖:

import { upgradeWebSocket } from "@neon/functions";

export default {
  async fetch(req: Request): Promise<Response> {
    if (req.headers.get("upgrade")?.toLowerCase() !== "websocket") {
      return new Response("expected a websocket upgrade", { status: 426 });
    }

    const { socket, response } = upgradeWebSocket(req);
    socket.addEventListener("message", (event) => socket.send(event.data));
    return response;
  },
};

socket 是标准的 WebSocket,因此 addEventListener 和 onopen/onmessage/onclose/onerror 属性都可以使用。获取它时,它仍处于 CONNECTING 状态——运行时会在处理程序返回 response 后才写入 101,此时套接字才会打开。

以下三条规则很重要:

  • 原样返回 response。 不能将 101 构造为普通的 Response(fetch 规范将构造的响应限制为 200–599),因此运行时会返回一个携带待处理升级的对象。clone() 或像改写响应的中间件那样用 new Response(res.body, res) 重建响应,都会丢弃升级并导致请求失败。
  • 通过返回普通的 Response 拒绝握手。 在升级之前,从 fetch 返回 401、403 或 404,即可限制套接字访问。浏览器客户端无法获知握手被拒绝的原因;它只会看到通用的连接失败,而不会看到你的状态码或响应正文。拒绝连接以阻止客户端进入,但需要客户端了解的详细信息应通过单独的经过身份验证的请求发送。
  • binaryType 默认为 "arraybuffer",而不是浏览器中的 "blob"。文本帧的 event.data 是 string,二进制帧的 event.data 是 ArrayBuffer,因此应根据 typeof 进行分支处理。

使用身份验证。 浏览器无法为 WebSocket 设置标头,因此请使用 ?token= 查询参数进行身份验证(以与 Agent 后端相同的方式,通过 JWKS 使用 jwtVerify 验证),并在升级前拒绝请求:

// src/index.ts
import { upgradeWebSocket } from "@neon/functions";

const clients = new Set<WebSocket>();

export default {
  async fetch(request: Request): Promise<Response> {
    if (request.headers.get("upgrade")?.toLowerCase() !== "websocket") {
      return new Response("WebSocket endpoint — connect with ?token=<jwt>");
    }

    const url = new URL(request.url);
    const identity = await verifyToken(url.searchParams.get("token"));
    if (!identity) return new Response("unauthorized", { status: 401 });

    const { socket, response } = upgradeWebSocket(request);
    clients.add(socket);
    socket.addEventListener("close", () => clients.delete(socket));
    socket.addEventListener("message", (event) => {
      if (typeof event.data !== "string") return;
      persist(identity.id, event.data); // fan out to every isolate — see below
    });
    return response;
  },
};

子协议。 传入 { protocol },从客户端提供的协议中选择一个;该协议会在 Sec-WebSocket-Protocol 中回显,并可通过 socket.protocol 访问。选择客户端未提供的协议会抛出 TypeError。省略此参数则不会协商协议。也不会协商扩展——socket.extensions 始终为 "",且不支持 permessage-deflate。

Hono。 使用 @neon/functions/hono 中的 upgradeWebSocket——它与 Hono 自带的 WebSocket 辅助函数使用相同的基本功能,无需 ws 依赖,也不使用已弃用的 @hono/node-ws。身份验证使用普通中间件;在调用 next() 之前限制升级请求:

// src/index.ts
import { Hono } from "hono";
import { upgradeWebSocket } from "@neon/functions/hono";

const clients = new Set<WebSocket>();

const app = new Hono<{ Variables: { userId: string } }>();

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}`);
    },
  })),
);

export default app;

使用函数的 wss:// URL 从浏览器连接(通过 neon functions get <slug> 获取),例如 new WebSocket("wss://<branch>-<slug>.compute.<region>.aws.neon.tech/ws?token=<jwt>")。连接关闭时应重新连接——隔离环境可能被回收,空闲连接也可能在 15 分钟后终止。

不要在升级路由上使用 cors(),也不要在 await next() 之前读取 c.res,或在之后调用 c.header()——这两种做法都会重建 101 并导致升级失败。完整的中间件表请参见 @neon/functions README。

心跳(保持套接字连接)

只有在字节持续传输时,连接才会保持打开状态:Neon 会在静默流持续 15 分钟后将其驱逐(超时和运行时限制),而中间代理/负载均衡器通常严格得多(通常只有几十秒)。不要指望应用程序足够活跃——请由服务端定期发送保活消息,避免套接字进入静默状态。

标准的 WebSocket 接口没有 ping(),因此应发送一条客户端会过滤掉的应用层消息:

const HEARTBEAT_MS = 25_000; // comfortably under proxy idle timeouts

const beat = setInterval(() => {
  for (const socket of clients) {
    if (socket.readyState === socket.OPEN) socket.send('{"type":"ping"}');
  }
}, HEARTBEAT_MS);
beat.unref?.();

客户端在处理消息时会跳过这些消息。这里没有协议层的捷径:upgradeWebSocket 返回的标准 WebSocket 没有 ping(),而且浏览器无法通过 JavaScript 发送 ping 帧,因此应用层消息是浏览器客户端唯一可用的保活方式。(Node ws 客户端可以发送 ping 帧,服务端会自动回复 pong,但浏览器不行。)

跨 isolate 扇出(不要跳过)

在负载下,运行时会并行运行多个 isolate,每个 isolate 都有自己的一份模块状态副本——因此每个 isolate 都有自己的 clients 集合。只向本地集合广播,意味着 isolate A 上的客户端永远看不到 isolate B 上产生的事件,消息流会悄无声息地分裂。这个问题很容易被忽略:neon dev 只运行单个进程(一个 isolate),因此进程内广播在本地看起来始终正常,但在生产环境中,由于并发连接分散在多个 isolate 上,就会出问题。

如果数据是数据库行(实时仪表板、信息流、基于存储消息的聊天),请使用 Neon Realtime(neon-realtime 技能),不要自行实现扇出。使用 Realtime 并不会妨碍数据库缩容至零。下面的策略适用于并非来自数据库行的推送(在线状态、正在输入状态、大模型流),以及 Realtime 不可用时的备用方案,例如尚未认领的 Claimable 项目。

模块状态无论如何都无法在驱逐后保留,因此 Postgres 是共享的事实来源。选择一种扇出策略。以下每个代码片段中,pool 是一个池化的 pg 客户端,clients 是此 isolate 中包含活动连接的 Set。

1. 轮询 Postgres——默认方案,并且可以保留缩容至零。 每个 isolate 按较短的时间间隔重新读取共享状态(或游标之后的行),并将更改推送给自己的客户端。每个时间间隔每个 isolate 只执行一次查询(而非每个客户端一次),且 isolate 没有客户端时不会执行查询——因此空闲计算仍会挂起。

let lastId = "0"; // bigint id, so a string
let polling = false;

async function poll() {
  if (polling || clients.size === 0) return; // guard overlap; no clients → no query → compute can scale to zero
  polling = true;
  try {
    const { rows } = await pool.query(
      "SELECT id, payload FROM events WHERE id > $1 ORDER BY id",
      [lastId],
    );
    for (const { id, payload } of rows) {
      lastId = id;
      for (const socket of clients) {
        if (socket.readyState === socket.OPEN) socket.send(payload);
      }
    }
  } catch (err) {
    console.error("[poll]", err);
  } finally {
    polling = false;
  }
}

// Seed from the latest id so a fresh isolate sends only new rows, not the whole table, then poll.
pool
  .query("SELECT coalesce(max(id), 0)::text AS id FROM events")
  .then((seed) => {
    lastId = seed.rows[0].id;
  })
  .catch((err) => console.error("[seed]", err))
  .finally(() => setInterval(poll, 1000).unref?.());
  • 延迟: 最多为轮询间隔(约 1 秒)。
  • 扩展: 数据库负载随活动 isolate 数量增长,而不是随客户端数量增长。请确保游标使用已建立索引的 serial/bigserial 主键,并将轮询间隔设为合理值。
  • 缩容至零: ✅ 保留——没有客户端连接时轮询会停止,因此计算会按正常计时器挂起。
  • 顺序: WHERE id > cursor 可能会漏掉提交顺序错乱的行:一个分配了较小 id 的事务在分配了较大 id 的事务提交后才提交,这样它就已经落在游标之后,轮询将永远不会返回它。对于广播信息流,偶尔丢失通常可以接受;如果需要确保获取每一行,请使用 LISTEN/NOTIFY,或按 created_at 轮询并设置一个较小的重叠窗口,再按 id 去重。

2. LISTEN/NOTIFY——延迟最低,但需要禁用缩容至零。 每个 isolate 通过专用的非池化连接在某个通道上执行 LISTEN;广播通过 NOTIFY 实现,因此每个 isolate(包括发送方所在的 isolate)都会重新向自己的套接字推送。近乎即时——但监听器会保持一个空闲连接,该连接不会计为活动连接,因此缩容至零会挂起计算并断开连接,导致信息流无声中断。仅在始终在线的计算环境中使用(已禁用缩容至零——付费计划设置)。

import { attachDatabasePool } from "@neon/functions";
import { Pool, Client } from "pg";

const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 5 });
attachDatabasePool(pool);
const CHANNEL = "chat_events";

// One dedicated DIRECT connection per isolate, just to receive events.
// Use DATABASE_URL_UNPOOLED — LISTEN needs a real session, not a pooled one.
// Don't call attachDatabasePool here: it would silence the idle drop that killed the feed.
// The error listener keeps the process alive; reconnect the client on error in production (omitted here).
const listener = new Client({
  connectionString: process.env.DATABASE_URL_UNPOOLED,
});
listener.on("error", (err) => {
  console.error(err);
});
listener.connect().then(() => listener.query(`LISTEN ${CHANNEL}`));
listener.on("notification", (msg) => {
  if (!msg.payload) return;
  for (const socket of clients) {
    if (socket.readyState === socket.OPEN) socket.send(msg.payload);
  }
});

// Broadcast by NOTIFYing through the pool — every isolate's listener fires.
function broadcast(event: unknown) {
  return pool.query("SELECT pg_notify($1, $2)", [
    CHANNEL,
    JSON.stringify(event),
  ]);
}

3. 外部发布/订阅(例如 Upstash Redis)——适合大规模场景。 对于高扇出、大量连接下的亚秒级延迟或多区域场景,请通过专用代理进行发布/订阅。吞吐量最高,且不会访问 Postgres 或阻止缩容至零,但需要额外运行一个服务。

经验法则: 对于数据库行,请使用 Neon Realtime。其他情况从轮询开始(支持缩容至零,无需额外基础设施);只有需要亚秒级延迟时,才在始终在线的计算环境中切换到 LISTEN/NOTIFY;当扇出超出 Postgres 的承载能力时,再迁移到 Redis。

客户端必须重新连接

空闲函数会被驱逐(隔离区也会因运行需要而重启),因此客户端的套接字一定会断开连接——应将重新连接视为正常情况,而非异常情况。使用有上限的指数退避策略重新连接,并且每次尝试都重新签发令牌(令牌有效期较短,过期令牌会无法通过 upgrade 身份验证检查):

let closed = false,
  retry = 0,
  timer: ReturnType<typeof setTimeout>;

async function connect() {
  if (closed) return;
  const token = await getToken(); // re-mint each attempt; short-lived
  const ws = new WebSocket(`${WS_URL}?token=${encodeURIComponent(token)}`);
  ws.onopen = () => {
    retry = 0; // reset backoff on success
  };
  ws.onmessage = (e) => {
    /* apply the event */
  };
  ws.onclose = () => {
    if (!closed)
      timer = setTimeout(connect, Math.min(1000 * 2 ** retry++, 15000));
  };
  ws.onerror = () => ws.close(); // let onclose drive the retry
}
connect();

综合来看,fetch 中的 upgradeWebSocket、通过 ?token= 进行的 JWT 身份验证、跨隔离区扇出以及客户端退避策略,共同构成了一个完整的单函数 WebSocket 后端,适用于不由数据库行触发的推送(在线状态、输入状态、LLM 流),或无法使用 Realtime 的情况。对于基于数据库行的持久化聊天、信息流和其他实时 UI,请使用 Neon Realtime(neon-realtime 技能)。

Server-Sent Events(SSE)

如果只需要服务端 → 客户端流式传输(实时计数器、通知、进度、令牌流),SSE 比 WebSocket 更简单,而且无需升级连接:普通的 fetch 处理程序只需返回一个响应,其主体为 ReadableStream,并设置 Content-Type: text/event-stream,运行时就会在数据持续传输期间保持连接打开。浏览器使用 EventSource 接收事件,它会自动重新连接,因此无需编写客户端退避逻辑。

// src/index.ts — minimal SSE endpoint
const encoder = new TextEncoder();
export default {
  fetch: () => {
    let t: ReturnType<typeof setInterval>;
    return new Response(
      new ReadableStream<Uint8Array>({
        start(controller) {
          controller.enqueue(encoder.encode("data: hello\n\n"));
          t = setInterval(
            () => controller.enqueue(encoder.encode(": ping\n\n")),
            25_000,
          );
        },
        cancel() {
          clearInterval(t); // fires when the client disconnects
        },
      }),
      {
        headers: {
          "Content-Type": "text/event-stream",
          "Cache-Control": "no-cache, no-transform",
        },
      },
    );
  },
};

WebSocket 的规则同样适用。**心跳:**只有持续传输字节,流才会保持打开——Neon 的超时时间为 15 分钟(超时和运行时限制),但代理通常限制严格得多,因此每隔约 25–30 秒发送一条 : ping\n\n 注释(如上所示),以防止空闲流被断开。将状态保存在 Postgres 中,并使用一种扇出策略跨隔离区扇出(持有一个流控制器 Set,并对每个控制器调用 enqueue)。EventSource 只支持 GET,且无法设置标头,因此请像 WebSocket 情况一样,使用 ?token= 查询参数或 Cookie 进行身份验证。references/sse.md 提供了完整模式,包括 Hono 变体、跨隔离区扇出、线路格式、客户端和注意事项。

函数触发器

函数触发器会通过 POST 向你的函数发送 JSON,触发条件可以是定时任务(schedule),也可以是在对象存储中创建对象(storage_object_created)。在 neon.ts 中声明,使用 neon deploy 应用,并通过 parseTriggerDelivery(@neon/functions/triggers)验证投递身份。parseTrigger(Hono)和 parseTriggerInvocation 仍仅支持定时任务。优先使用 neon.ts;CLI 和 Neon MCP 触发器工具(list_triggers、create_trigger、……)可作为备用方案。

触发器路由不得要求用户 JWT 或 X-Secret;Neon 会直接向原生 URL 发送 POST 请求,不会携带这些信息。生产环境的调用方格式:references/production-hardening.md。完整字段列表、CLI、MCP、载荷、继承机制和两种处理程序格式:references/function-triggers.md。

MCP 服务

MCP 服务是 Functions 的天然工作负载:一个长期运行的 HTTP 处理器,向 AI 客户端(Cursor、Claude、ChatGPT、Agent)提供工具,这些工具可在计算资源旁直接读取和写入分支的 Postgres。MCP 的流式 HTTP 传输是在单个端点(通常为 /mcp)上执行普通的 POST/GET,因此可以映射到函数的 fetch 处理器,无需 upgrade 方法或额外协议。

最简单的托管方式是使用 Hono 应用,配合官方 @modelcontextprotocol/sdk 和 @hono/mcp,后者可将传输桥接到路由。构建服务、注册工具,并在模块作用域中创建一次传输,然后将每个 /mcp 请求交给它:

const transport = new StreamableHTTPTransport();
app.all("/mcp", async (c) => {
  if (!mcpServer.isConnected()) await mcpServer.connect(transport);
  return transport.handleRequest(c);
});

由于函数的 URL 是公开的,在连接传输前必须先进行身份验证——Better Auth 同时支持 OAuth(其 MCP 插件会将你的应用设为授权服务,使第三方客户端能够根据 MCP 规范自行授权)以及更简单的 API 密钥 / 会话 JWT 检查,供你自己的调用方使用。面向公众用户的边缘防护措施:references/production-hardening.md。references/mcp.md 提供了完整模式——使用 Drizzle 和 Postgres 支持的工具构建服务、两种 Better Auth 身份验证方案,以及使用 mcporter / add-mcp 进行测试。

集成与可观测性

内置分支日志
neon logs query --branch production --source function --since 1h

目前,Functions 与对象存储是分支日志涵盖的两个来源。日志按单个分支划分范围,因此,如果已部署的函数不在当前检出的分支上,请传入 --branch。日志的其他所有信息——所需的 CLI 版本、筛选器、SDK 和兼容 Loki 的读取 API——都在上级 neon 技能的可观测性部分。

应用程序检测

函数是一个长期运行的 Node.js 进程,运行符合 Web 标准的请求 / 响应处理器,因此标准的 Node 集成 SDK 无需修改即可使用。在模块加载时初始化一次,并通过环境变量控制是否启用,这样本地开发和未配置的分支就不会执行任何操作;通过 --env 或 neon.ts 的 env 传递密钥。

  • Sentry — 监控 HTTP 框架、函数运行时以及 Agent 自身捕获 / 回退失败中的错误(这是 Functions 面向的长期运行场景):参见 references/sentry.md。
  • Mastra Studio (Mastra Cloud) — 在函数上运行 Mastra Agent,并将其跟踪数据发送到 Studio 项目进行可观测性监控:参见 references/mastra-studio.md。

Neon 文档

Neon 文档是权威信息来源,而 Functions 正在快速演进,因此请始终对照官方文档进行确认。在 URL 后追加 .md,或请求 Accept: text/markdown,即可将任意文档页面获取为 Markdown。请从文档索引(https://neon.com/docs/llms.txt)和变更日志公告中查找合适的页面。

延伸阅读

评论

支持 Markdown