Neon Functions
Databases & backend39.4K installs
What it does
Long-running, serverless Node.js HTTP functions deployed onto your Neon branch, with DATABASE_URL injected automatically and compute that runs next to your data. Use when a user wants to host an API, an AI agent with long streaming responses, a WebSocket or server-sent-events…
Install
npx skills add https://github.com/neondatabase/agent-skills --skill neon-functionsWebsite
www.skills.sh/neondatabase/agent-skills/neon-functionsDetails
From the skill’s SKILL.md or README, by its authors. View the original
FIRST: Use the parent neon skill for a Neon overview, getting started with Neon, Neon development best practices, and more.
If the neon skill is not installed, fetch it from https://neon.com/docs/ai/skills/neon/SKILL.md or install it with:
neon skills -s neon -y
Neon Functions
Currently available in aws-us-east-2, aws-us-east-1, aws-eu-central-1, and aws-ap-southeast-1.
Neon Functions are long-running Node.js HTTP handlers deployed onto a Neon branch. Each function gets a public HTTPS URL, runs in the same region as your database, and — if the branch has Postgres — gets DATABASE_URL injected automatically. You deploy and manage them through the same Neon CLI, neon.ts, and API you already use.
Use this skill to help the user define, run locally, deploy, and manage functions next to their database. Deliver a deployed function with its invocation URL, a working local neon dev loop, or a precise answer from the official Neon docs.
When to Use
Reach for Neon Functions when the workload is a request/response handler that benefits from staying alive and staying close to the data:
- Long-running request/response flows that outlast lambda-style limits. Agents that make several LLM calls and tool invocations per request, or image/video generation, routinely blow past the ~10–60s execution caps and short streaming windows of traditional serverless functions. Neon Functions are long-running: the handler just needs to _start_ responding within 15 minutes, and an open stream stays alive as long as bytes keep flowing. That's enough headroom for real agent workloads.
- Stateful streaming without bolting on Redis. Because a function stays alive across a request, it can host an SSE endpoint or a WebSocket server and hold the connection open in-process — no external state store (Redis, etc.) needed just to keep a stream coherent. Module-scope state (a
pgpool, an in-memory counter) persists across requests on the same isolate. - Compute that must sit next to Postgres. The function runs in the same region as the branch's database, so there are no cross-region round trips on every query.
DATABASE_URLis injected for you. - A backend that branches with your data. Each branch runs its own version of the function at its own URL, against its own isolated database (and storage, and gateway) state. Preview deployments, CI, and dev environments each get a self-contained backend — deploying to a child never affects the parent.
- Query Postgres from the Function (or an existing framework handler). Prefer that over the Data API. Use the Data API when the application already uses PostgREST or Supabase-js database calls, or is migrating that client.
- Webhooks, bots, and post-response work. Webhook handlers that fan out into multiple DB writes, Discord/WebSocket bots, and fire-and-forget follow-ups via
waitUntil(analytics, audit logs) all fit. - Recurring HTTP work. A Function Trigger POSTs to the function on a cron (
type: "schedule") or when an object is created in Object Storage (type: "storage_object_created"). Samefetchhandler, same 15-minute time-to-first-byte limit. See Function Triggers.
If the workload is a pure static site, or something that must run outside the supported regions (aws-us-east-2, aws-us-east-1, aws-eu-central-1, and aws-ap-southeast-1) today, this isn't the right tool yet (see Timeouts and Runtime Limits and Availability).
What It Does
- Long-running & serverless — Built for WebSocket servers (see WebSocket Servers), SSE endpoints (see Server-Sent Events (SSE)), long agent HTTP streams, and APIs. Still scales to zero when idle.
- Web-standard handler — A function is any default export with a
fetch(request)method returning aResponse(Workers/WinterTC-compatible). A Hono app exports exactly that shape, soexport default appjust works. Runs on Node.js 24, so all Node APIs are available. - Close to your database — Runs in the branch's region;
DATABASE_URLinjected automatically when the branch has Postgres. - Branchable — Each branch runs its own function version at its own URL against its own isolated state.
- Same CLI/API — Deploy and manage via
neon,neon.ts, or the Neon API. - Function Triggers — Neon POSTs to the function on a cron or an object-storage upload. See Function Triggers.
Availability
Check this precondition before setting anything up: Neon Functions is currently available in aws-us-east-2, aws-us-east-1, aws-eu-central-1, and aws-ap-southeast-1. Confirm the user's Neon project is in one of these regions.
Architecture: Where Functions Fit
Neon (Functions included) is backend primitives, not full-stack app hosting. Host your app on a frontend hosting provider like Vercel (or Netlify, Cloudflare, etc.), or serve a small static page from a public_read bucket behind a CDN (see the neon-object-storage skill for caching); Functions are the long-running, stateful slice of your backend that lives next to your data. They compose with that platform in two ways:
- Add a Function to a full-stack app. Your Next.js / TanStack Start app on Vercel (or Netlify) owns UI, auth (Managed Auth, Better Auth, Clerk, or another IdP), and talks directly to Lakebase Postgres and Object Storage. Add a Function as a Hono API layer for the web app and other clients, or for one job next to the data: Object Storage uploads, AI agents, Discord bots, WebSocket or SSE servers. (See Functions as an Agent Backend for the client-direct pattern.)
- Run the whole backend control plane on Functions. Especially when the frontend is client-only — TanStack Router, React Router in client mode, and similar SPAs hosted on Vercel or Netlify — the client calls Functions directly. Build REST APIs and request/response agents, host MCP servers, and run anything stateful or that belongs close to Postgres and Object Storage.
Either way, authenticate by caller: JWT or API key for app and public HTTP (see the WARNING under Functions as an Agent Backend); parseTriggerDelivery for Function Trigger routes; production hardening in Production hardening. Because a Function is just your backend, you can move pieces between your host and Neon — relocate an agent or a stateful WebSocket server onto a Function when it needs more runtime, and back if needed.
Prefer a Function, or an existing framework handler, that queries Postgres. Use Data API when the application already uses PostgREST/Supabase-js database calls or is migrating that client.
Production hardening
Before exposing production routes, read Production hardening.
Pick by caller: trusted app server, Function Trigger, or public consumer. Keep long browser streams on the client-direct JWT path unless a verified streaming-compatible proxy is required. Authentication rejects application work; requests to the native URL still reach the Function.
Setup
Functions are declared in neon.ts (see the neon skill for the branch-first workflow and neon.ts basics). Add @neon/config and declare functions under functions, keyed by 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
},
},
});
The slug is the function's permanent identity (it appears in the invocation URL and CLI commands) and can't be changed after the first deploy. Use name for a human-readable label.
A minimal function — a Hono app that queries the branch's Postgres via the injected DATABASE_URL:
// 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;
Create the pg pool at module scope (reused across requests on the same isolate) and keep max small (e.g. 5), since each isolate keeps its own pool. Call attachDatabasePool(pool) so an idle disconnect is not an uncaughtException — see Connecting to Postgres.
parseEnv(config) requires _every_ variable the config implies. A function that only talks to Postgres over the pooled URL can scope it to just that key — parseEnv then validates and returns only what you asked for (the keys autocomplete from your 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);
Develop Locally and Deploy
| Command | What it does |
|---|---|
neon dev | serves every function in neon.ts with hot reload; injects DATABASE_URL & friends |
neon deploy --env | preferred full deploy from neon.ts; --env is the file Function env is read from |
Keep .env or .env.local up to date with every key under functions.*.env. neon env pull writes Neon-managed vars only; add Function secrets to that file, then pass it as --env. neon deploy --env loads that file into process.env each time, then uploads the declared values. Declare each secret as { value: process.env.X, required: true } (see Your own secrets below): an unset required value fails neon deploy and neon dev before anything changes, and never fails an import of neon.ts. Never coerce a missing value to "" (that uploads "" and deletes the live key); an empty assignment (KEY=) counts as unset.
To deploy a single function without applying neon.ts: neon functions deploy --src src/index.ts (--src takes either the entry file or a directory containing index.ts, index.mjs, or index.js). That command's --env is KEY=VALUE (repeatable), not a file path. Use it for a targeted env update. Retrieve the public URL with neon functions get (the invocation_url field, of the form https://-.compute..us-east-2.aws.neon.tech). Manage with neon functions list|get|delete.
When neon checkout _creates_ a new branch and a neon.ts is present, it applies the policy automatically. Pass --env on that create so Function env that reads process.env resolves (neon checkout feat --create --env .env.local). Existing process env wins over the file. Checking out an existing branch never reconciles it — apply config changes with neon deploy --env (add --update-existing only after reviewing those changes).
Native binaries and custom builds
The default deploy bundles source into one index.mjs, which cannot hold a compiled binary. Ship one as a separate archive file:
- npm package with a Node addon (
sharp):externalPackages: ["sharp"]. The deploy installs its linux-arm64 glibc build and copies the files in. - Your own build output (
.node,.so,.wasm, data files):bundler: "none"with a prebuilt directory, or an inlinebundler: async (fn) => ({ "index.mjs": …, "bin/tool": … }). - A standalone executable you spawn: ship it as above, then copy it to
/runandchmod 755it at startup. Files in/opt/functionarrive as mode644on a read-only filesystem, and/tmpisnoexec.
Runtime target, pros and cons, working snippets, and size limits: references/native-binaries.md.
Neon Infrastructure as Code (neon.ts)
The functions block from Setup is part of neon.ts, Neon's infrastructure-as-code file — one TypeScript file declares every function (its source, display name, and env) alongside any other branch services, in version control (see the neon skill for the full reference). Treat it like Terraform for your branch:
| Command | What it does |
|---|---|
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 | bundle + deploy the declared functions (neon deploy is an alias; pass --env when Function env reads process.env) |
Functions are branch-scoped: each branch runs its own deployment at its own URL. When a neon.ts is present, neon checkout applies the policy as it _creates_ a branch. Pass --env on that create when Function env reads process.env. Checking out an _existing_ branch doesn't redeploy — run neon deploy --env to apply changes.
Per-branch deploy tuning (e.g. runtime) lives in the branch closure, keyed by slug, so it can vary by branch without changing which functions exist:
export default defineConfig({
functions: { todos: { name: "todo api", source: "src/index.ts" } },
branch: (branch) => ({
functions: { todos: { runtime: "nodejs24" } },
}),
});
Environment Variables
Neon injects branch-scoped connection strings and service URLs at runtime — you don't declare these or pass them at deploy time:
| Variable | Notes |
|---|---|
NEON_BRANCH | The branch name (e.g. main, preview/foo). Injected on every branch, including the default. |
NEON_BRANCH_ID | The branch id (br-…). Injected on every branch. |
NEON_FUNCTION_HOST_SUFFIX | The hostname suffix every function on the branch shares (.compute.c-2.us-east-2.aws.neon.tech). See How to get the function URL. |
DATABASE_URL | Pooled connection string. Use for most queries. Present only if the branch has Postgres. |
DATABASE_URL_UNPOOLED | Direct connection. Use for migrations, LISTEN/NOTIFY, multi-round-trip transactions. |
NEON_AUTH_BASE_URL | Present when Neon Auth is enabled on the branch. |
NEON_AUTH_JWKS_URL | Present when Neon Auth is enabled on the branch. JWKS for verifying Managed Auth JWTs. |
NEON_DATA_API_URL | Present when the Data API is enabled on the branch. |
Object storage (AWS_*) and AI Gateway (NEON_AI_GATEWAY_*) vars are also injected when those services are declared — see the neon-object-storage and neon-ai-gateway skills.
neon env pull / neon-env run / neon dev emit NEON_BRANCH, NEON_BRANCH_ID, NEON_FUNCTION_HOST_SUFFIX (and the connection strings) into your local dev environment too, so local runs mirror the deployed runtime.
Your own secrets are per-deployment. Preferred path: declare them in neon.ts and run neon deploy --env . ` is the gitignored file env pull already writes (.env if that file exists, otherwise .env.local). Env pull writes Neon-managed vars only; add Function secrets to that file. Declare each one with both value and required (needs @neon/config 1.13.0 and neon 9.2.0 or later, and @neon/env 2.1.0 or later where the function calls parseEnv; older parseEnv` treats every declared key as required):
functions: {
todos: {
name: "todo api",
source: "src/index.ts",
env: {
RESEND_API_KEY: { value: process.env.RESEND_API_KEY, required: true },
SENTRY_DSN: { value: process.env.SENTRY_DSN, required: "default-branch" },
LOG_LEVEL: "info",
},
},
}
required | Unset deploying the default branch | Unset deploying another branch | Unset in neon dev | parseEnv(config, slug).function |
|---|---|---|---|---|
true | fails before any change | fails before any change | fails before any function starts | string |
"default-branch" | fails before any change | removed from the deployment | runs without it | string | undefined |
false | removed from the deployment | removed from the deployment | runs without it | string | undefined |
An undefined or empty value counts as unset; put a fallback in value (process.env.X ?? "1"). "default-branch" is the branch Neon marks as default, not a branch name. Add onUndefined: "skip" to a required: false or "default-branch" variable to keep its deployed value instead of removing it, for a value set with neon functions deploy --env. Keys neon.ts does not declare are left alone.
Prefer a declaration over a string such as process.env.X!. A string is validated wherever neon.ts is imported, so a function that calls parseEnv(config, "") fails to load when another function's secret is missing from its runtime. A declaration is checked only when a command needs its value.
neon functions deploy --env KEY=VALUE is the manual path (repeatable; --env KEY= deletes a key; unmentioned keys carry over). Use it for a targeted env update, not a full neon.ts apply.
Load Function secrets into the same file env pull wrote, then neon deploy --env . Pull the branch's Neon-managed vars onto disk for local dev with neon env pull (link/checkout do this automatically; pass --no-env-pull to skip and use neon-env run -- for runtime injection). Limits: ≤1,000 vars, ≤64 KiB total, and the NEON_ prefix is reserved.
How to get the function URL
A function answers at https://-, for example https://br-cool-lab-a1b2c3d4-todos.compute.c-2.us-east-2.aws.neon.tech. Each branch has its own URL.
From a terminal: neon functions get prints it as invocation_url. neon deploy prints it for each function it deploys.
From code (another function, a script, or an app after neon env pull): call functionBaseUrl(slug) from @neon/env (2.0.0 or later). It returns the base URL with no trailing slash:
import { functionBaseUrl } from "@neon/env";
const res = await fetch(`${functionBaseUrl("todos")}/api/todos`);
| Where the code runs | functionBaseUrl("todos") returns |
|---|---|
| A deployed function on the branch | https://-todos.compute.… (runtime env) |
An app or script after neon env pull | the same URL, from the pulled env |
| `neon |
More Databases & backend skills
See allDatabases & backend
Guides Microsoft Entra ID app registration, OAuth 2.0 authentication, and MSAL integration. USE FOR: create app registration, register Azure AD app, configure OAuth, set up authentication, add API permissions, generate service principal, MSAL example, console app auth, Entra ID…
649.5K installsnpx skills add https://github.com/microsoft/azure-skills --skill entra-app-registrationDatabases & backend
Postgres best practices maintained by Supabase, for Postgres running anywhere. Load this skill BEFORE writing or changing anything that lives in a Postgres database: creating or altering tables and columns (including choosing column types), schema design, migrations and…
478.7K installsnpx skills add https://github.com/supabase/agent-skills --skill supabase-postgres-best-practicesDatabases & backend
Prisma Client API reference covering model queries, filters, operators, and client methods. Use when writing database queries, using CRUD operations, filtering data, or configuring Prisma Client. Triggers on "prisma query", "findMany", "create", "update", "delete",…
418.2K installsnpx skills add https://github.com/prisma/skills --skill prisma-client-apiDatabases & backend
Deprecated compatibility name for prisma-orm-setup. Use only when a user or existing integration explicitly invokes prisma-database-setup; otherwise select prisma-orm-setup.
416.4K installsnpx skills add https://github.com/prisma/skills --skill prisma-database-setup
Skills are shared by members and not reviewed by us. Read a skill’s code before you install it.