vybekiit 0.7.3 → 0.7.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +16607 -7527
- package/dist/global-skills/aws-cdk/SKILL.md +2 -1
- package/dist/global-skills/aws-cloudformation/SKILL.md +2 -1
- package/dist/global-skills/aws-serverless/SKILL.md +45 -44
- package/dist/global-skills/aws-serverless/assets/powertools-handler.py +2 -1
- package/dist/global-skills/aws-serverless/references/api-gateway.md +50 -470
- package/dist/global-skills/aws-serverless/references/architecture.md +47 -186
- package/dist/global-skills/aws-serverless/references/concurrency.md +44 -158
- package/dist/global-skills/aws-serverless/references/deployment.md +1 -1
- package/dist/global-skills/aws-serverless/references/event-sources.md +72 -391
- package/dist/global-skills/aws-serverless/references/lambda.md +69 -428
- package/dist/global-skills/aws-serverless/references/orchestration.md +65 -384
- package/dist/global-skills/aws-serverless/references/production.md +78 -415
- package/dist/global-skills/aws-serverless/references/troubleshooting.md +92 -619
- package/dist/global-skills/claimable-postgres/SKILL.md +60 -49
- package/dist/global-skills/design-my-data/SKILL.md +2 -0
- package/dist/global-skills/eas-app-stores/SKILL.md +8 -0
- package/dist/global-skills/eas-app-stores/references/play-store.md +2 -0
- package/dist/global-skills/eas-hosting/SKILL.md +8 -0
- package/dist/global-skills/eas-observe/SKILL.md +30 -8
- package/dist/global-skills/eas-observe/agents/openai.yaml +1 -1
- package/dist/global-skills/eas-observe/references/metrics.md +54 -3
- package/dist/global-skills/eas-observe/references/queries.md +78 -17
- package/dist/global-skills/eas-observe/references/setup.md +164 -5
- package/dist/global-skills/eas-observe/references/third-party.md +136 -0
- package/dist/global-skills/eas-simulator/SKILL.md +63 -11
- package/dist/global-skills/eas-simulator/references/controllers.md +21 -2
- package/dist/global-skills/eas-simulator/references/run-your-app.md +16 -11
- package/dist/global-skills/eas-simulator/references/troubleshooting.md +8 -5
- package/dist/global-skills/eas-update-insights/SKILL.md +8 -0
- package/dist/global-skills/eas-workflows/SKILL.md +15 -10
- package/dist/global-skills/eas-workflows/scripts/package.json +2 -7
- package/dist/global-skills/email-best-practices/.github/workflows/sync-skills.yml +30 -0
- package/dist/global-skills/email-best-practices/README.md +63 -0
- package/dist/global-skills/email-best-practices/references/accessibility.md +189 -0
- package/dist/global-skills/email-best-practices/references/compliance.md +125 -0
- package/dist/global-skills/email-best-practices/references/deliverability.md +121 -0
- package/dist/global-skills/email-best-practices/references/email-capture.md +129 -0
- package/dist/global-skills/email-best-practices/references/email-types.md +173 -0
- package/dist/global-skills/email-best-practices/references/list-management.md +157 -0
- package/dist/global-skills/email-best-practices/references/marketing-emails.md +115 -0
- package/dist/global-skills/email-best-practices/references/sending-reliability.md +155 -0
- package/dist/global-skills/email-best-practices/references/transactional-email-catalog.md +418 -0
- package/dist/global-skills/email-best-practices/references/transactional-emails.md +92 -0
- package/dist/global-skills/email-best-practices/references/webhooks-events.md +167 -0
- package/dist/global-skills/email-best-practices/tests/README.md +35 -0
- package/dist/global-skills/email-best-practices/tests/scenarios/01-spam-deliverability.md +46 -0
- package/dist/global-skills/email-best-practices/tests/scenarios/02-multi-region-compliance.md +48 -0
- package/dist/global-skills/email-best-practices/tests/scenarios/03-retry-idempotency.md +36 -0
- package/dist/global-skills/email-best-practices/tests/scenarios/04-webhook-bounce-handling.md +52 -0
- package/dist/global-skills/email-best-practices/tests/scenarios/05-new-saas-email-plan.md +51 -0
- package/dist/global-skills/expo-animation/LICENSE +21 -0
- package/dist/global-skills/expo-animation/RECIPES.md +385 -0
- package/dist/global-skills/expo-animation/SKILL.md +267 -0
- package/dist/global-skills/expo-animation/agents/openai.yaml +4 -0
- package/dist/global-skills/expo-app-clip/SKILL.md +8 -0
- package/dist/global-skills/expo-brownfield/SKILL.md +8 -0
- package/dist/global-skills/expo-brownfield/references/brownfield-isolated.md +53 -4
- package/dist/global-skills/expo-data-fetching/SKILL.md +8 -0
- package/dist/global-skills/expo-design-system/SKILL.md +356 -0
- package/dist/global-skills/expo-design-system/agents/openai.yaml +4 -0
- package/dist/global-skills/expo-design-system/references/audit.md +185 -0
- package/dist/global-skills/expo-dev-client/SKILL.md +8 -0
- package/dist/global-skills/expo-dom/SKILL.md +8 -0
- package/dist/global-skills/expo-examples/SKILL.md +8 -0
- package/dist/global-skills/expo-migrate-module/SKILL.md +113 -0
- package/dist/global-skills/expo-migrate-module/agents/openai.yaml +4 -0
- package/dist/global-skills/expo-migrate-module/references/compatibility.md +73 -0
- package/dist/global-skills/expo-migrate-module/references/example.md +212 -0
- package/dist/global-skills/expo-migrate-module/references/migration-map.md +306 -0
- package/dist/global-skills/expo-module/SKILL.md +11 -1
- package/dist/global-skills/expo-native-ui/SKILL.md +8 -0
- package/dist/global-skills/expo-project-structure/SKILL.md +114 -0
- package/dist/global-skills/expo-project-structure/agents/openai.yaml +4 -0
- package/dist/global-skills/expo-router/SKILL.md +8 -0
- package/dist/global-skills/expo-skill-feedback/SKILL.md +66 -33
- package/dist/global-skills/expo-skill-feedback/agents/openai.yaml +2 -2
- package/dist/global-skills/expo-tailwind-setup/SKILL.md +8 -0
- package/dist/global-skills/expo-ui/SKILL.md +8 -0
- package/dist/global-skills/expo-upgrade/SKILL.md +15 -1
- package/dist/global-skills/expo-web-to-native/SKILL.md +8 -0
- package/dist/global-skills/extension-to-functions-codebase/SKILL.md +129 -0
- package/dist/global-skills/extension-to-functions-codebase/references/configuration-migration.md +161 -0
- package/dist/global-skills/extension-to-functions-codebase/references/destructuring-shim.md +122 -0
- package/dist/global-skills/extension-to-functions-codebase/references/signature-mapping.md +84 -0
- package/dist/global-skills/firebase-ai-logic-basics/SKILL.md +61 -19
- package/dist/global-skills/firebase-ai-logic-basics/references/flutter_setup.md +6 -3
- package/dist/global-skills/firebase-app-hosting-basics/SKILL.md +5 -1
- package/dist/global-skills/firebase-auth-basics/SKILL.md +4 -2
- package/dist/global-skills/firebase-basics/SKILL.md +5 -5
- package/dist/global-skills/firebase-crashlytics/SKILL.md +2 -0
- package/dist/global-skills/firebase-data-connect/SKILL.md +2 -0
- package/dist/global-skills/firebase-firestore/SKILL.md +3 -6
- package/dist/global-skills/firebase-hosting-basics/SKILL.md +4 -1
- package/dist/global-skills/firebase-remote-config-basics/SKILL.md +5 -1
- package/dist/global-skills/firebase-security-rules-auditor/SKILL.md +4 -1
- package/dist/global-skills/harden/SKILL.md +2 -1
- package/dist/global-skills/instrument-feature-flags/SKILL.md +1 -0
- package/dist/global-skills/instrument-feature-flags/references/COMMANDMENTS.md +5 -0
- package/dist/global-skills/instrument-feature-flags/references/adding-feature-flag-code.md +6 -4
- package/dist/global-skills/instrument-feature-flags/references/android.md +6 -4
- package/dist/global-skills/instrument-feature-flags/references/api.md +6 -4
- package/dist/global-skills/instrument-feature-flags/references/best-practices.md +6 -4
- package/dist/global-skills/instrument-feature-flags/references/django.md +38 -5
- package/dist/global-skills/instrument-feature-flags/references/dotnet.md +10 -4
- package/dist/global-skills/instrument-feature-flags/references/elixir.md +6 -4
- package/dist/global-skills/instrument-feature-flags/references/flask.md +17 -5
- package/dist/global-skills/instrument-feature-flags/references/flutter.md +7 -5
- package/dist/global-skills/instrument-feature-flags/references/go.md +6 -4
- package/dist/global-skills/instrument-feature-flags/references/ios.md +6 -4
- package/dist/global-skills/instrument-feature-flags/references/java.md +6 -4
- package/dist/global-skills/instrument-feature-flags/references/laravel.md +6 -4
- package/dist/global-skills/instrument-feature-flags/references/next-js.md +74 -8
- package/dist/global-skills/instrument-feature-flags/references/nodejs.md +6 -4
- package/dist/global-skills/instrument-feature-flags/references/php.md +6 -4
- package/dist/global-skills/instrument-feature-flags/references/python.md +6 -4
- package/dist/global-skills/instrument-feature-flags/references/react-native.md +6 -4
- package/dist/global-skills/instrument-feature-flags/references/react.md +6 -4
- package/dist/global-skills/instrument-feature-flags/references/ruby-on-rails.md +6 -4
- package/dist/global-skills/instrument-feature-flags/references/ruby.md +6 -4
- package/dist/global-skills/instrument-feature-flags/references/rust.md +6 -4
- package/dist/global-skills/instrument-feature-flags/references/usage.md +41 -4
- package/dist/global-skills/instrument-feature-flags/references/web.md +7 -5
- package/dist/global-skills/instrument-product-analytics/SKILL.md +2 -1
- package/dist/global-skills/instrument-product-analytics/references/COMMANDMENTS.md +5 -0
- package/dist/global-skills/instrument-product-analytics/references/EXAMPLE-astro-hybrid.md +12 -14
- package/dist/global-skills/instrument-product-analytics/references/EXAMPLE-astro-ssr.md +25 -21
- package/dist/global-skills/instrument-product-analytics/references/EXAMPLE-django.md +75 -55
- package/dist/global-skills/instrument-product-analytics/references/EXAMPLE-fastapi.md +3 -6
- package/dist/global-skills/instrument-product-analytics/references/EXAMPLE-flask.md +26 -16
- package/dist/global-skills/instrument-product-analytics/references/EXAMPLE-nuxt-3-6.md +8 -5
- package/dist/global-skills/instrument-product-analytics/references/EXAMPLE-nuxt-4.md +4 -4
- package/dist/global-skills/instrument-product-analytics/references/EXAMPLE-react-react-router-7-framework.md +3 -3
- package/dist/global-skills/instrument-product-analytics/references/EXAMPLE-ruby-on-rails.md +8 -4
- package/dist/global-skills/instrument-product-analytics/references/EXAMPLE-tanstack-start.md +20 -8
- package/dist/global-skills/instrument-product-analytics/references/android.md +46 -4
- package/dist/global-skills/instrument-product-analytics/references/angular.md +41 -5
- package/dist/global-skills/instrument-product-analytics/references/astro.md +41 -5
- package/dist/global-skills/instrument-product-analytics/references/configuration.md +7 -4
- package/dist/global-skills/instrument-product-analytics/references/django.md +38 -5
- package/dist/global-skills/instrument-product-analytics/references/dotnet.md +10 -4
- package/dist/global-skills/instrument-product-analytics/references/elixir.md +6 -4
- package/dist/global-skills/instrument-product-analytics/references/flask.md +17 -5
- package/dist/global-skills/instrument-product-analytics/references/flutter.md +49 -4
- package/dist/global-skills/instrument-product-analytics/references/go.md +6 -4
- package/dist/global-skills/instrument-product-analytics/references/identify-users.md +38 -7
- package/dist/global-skills/instrument-product-analytics/references/ios.md +14 -4
- package/dist/global-skills/instrument-product-analytics/references/laravel.md +6 -4
- package/dist/global-skills/instrument-product-analytics/references/next-js.md +74 -8
- package/dist/global-skills/instrument-product-analytics/references/nuxt-js-3-6.md +39 -7
- package/dist/global-skills/instrument-product-analytics/references/nuxt-js.md +40 -4
- package/dist/global-skills/instrument-product-analytics/references/php.md +6 -4
- package/dist/global-skills/instrument-product-analytics/references/posthog-python.md +21 -17
- package/dist/global-skills/instrument-product-analytics/references/python.md +15 -5
- package/dist/global-skills/instrument-product-analytics/references/react-native.md +41 -7
- package/dist/global-skills/instrument-product-analytics/references/react-router-v6.md +18 -4
- package/dist/global-skills/instrument-product-analytics/references/react-router-v7-data-mode.md +18 -4
- package/dist/global-skills/instrument-product-analytics/references/react-router-v7-declarative-mode.md +18 -4
- package/dist/global-skills/instrument-product-analytics/references/react-router-v7-framework-mode.md +18 -4
- package/dist/global-skills/instrument-product-analytics/references/ruby-on-rails.md +6 -4
- package/dist/global-skills/instrument-product-analytics/references/ruby.md +6 -4
- package/dist/global-skills/instrument-product-analytics/references/svelte.md +42 -6
- package/dist/global-skills/instrument-product-analytics/references/tanstack-start.md +31 -7
- package/dist/global-skills/instrument-product-analytics/references/usage.md +41 -4
- package/dist/global-skills/instrument-product-analytics/references/vue-js.md +42 -6
- package/dist/global-skills/manifest.json +6 -1
- package/dist/global-skills/neon/SKILL.md +235 -121
- package/dist/global-skills/neon-ai-gateway/SKILL.md +95 -16
- package/dist/global-skills/neon-functions/SKILL.md +242 -152
- package/dist/global-skills/neon-functions/references/ai-sdk.md +9 -3
- package/dist/global-skills/neon-functions/references/mastra-studio.md +1 -1
- package/dist/global-skills/neon-functions/references/mcp.md +13 -4
- package/dist/global-skills/neon-functions/references/sentry.md +165 -52
- package/dist/global-skills/neon-functions/references/sse.md +23 -6
- package/dist/global-skills/neon-object-storage/SKILL.md +40 -16
- package/dist/global-skills/neon-postgres/SKILL.md +63 -262
- package/dist/global-skills/neon-postgres-branches/SKILL.md +61 -50
- package/dist/global-skills/neon-postgres-egress-optimizer/SKILL.md +22 -3
- package/dist/global-skills/resend/SKILL.md +6 -6
- package/dist/global-skills/resend/references/automations.md +2 -2
- package/dist/global-skills/resend/references/broadcasts.md +11 -3
- package/dist/global-skills/resend/references/domains.md +25 -3
- package/dist/global-skills/resend/references/installation.md +7 -7
- package/dist/global-skills/setup-payments/SKILL.md +4 -5
- package/dist/global-skills/stripe-best-practices/SKILL.md +32 -7
- package/dist/global-skills/stripe-best-practices/references/billing.md +11 -1
- package/dist/global-skills/stripe-best-practices/references/payments.md +23 -2
- package/dist/global-skills/stripe-best-practices/references/security.md +21 -11
- package/dist/global-skills/stripe-best-practices/references/tax.md +116 -11
- package/dist/global-skills/supabase/CHANGELOG.md +25 -0
- package/dist/global-skills/supabase/SKILL.md +5 -1
- package/dist/global-skills/supabase-postgres-best-practices/CHANGELOG.md +30 -0
- package/dist/global-skills/supabase-postgres-best-practices/SKILL.md +1 -1
- package/dist/global-skills/use-kit-mcp/SKILL.md +5 -5
- package/dist/global-skills/use-railway/SKILL.md +4 -4
- package/dist/global-skills/use-railway/references/iac.md +206 -57
- package/dist/global-skills/use-railway/scripts/railway-api.sh +7 -2
- package/dist/global-skills/xcode-project-setup/SKILL.md +2 -0
- package/package.json +12 -9
- package/LICENSE.md +0 -47
- package/dist/global-skills/eas-workflows/scripts/validate.js +0 -84
- package/dist/global-skills/expo-skill-feedback/scripts/skill-feedback.cjs +0 -118
- package/dist/global-skills/neon-functions/references/hono-websockets.md +0 -145
|
@@ -5,17 +5,31 @@ description: >-
|
|
|
5
5
|
branch, with DATABASE_URL injected automatically and compute that runs next
|
|
6
6
|
to your data. Use when a user wants to host an API, an AI agent with long
|
|
7
7
|
streaming responses, a WebSocket or server-sent-events (SSE) server, a
|
|
8
|
-
webhook handler, a Discord bot, or any request/response
|
|
9
|
-
timing out on short, lambda-style serverless functions —
|
|
10
|
-
branch with their database. Triggers include "serverless
|
|
11
|
-
an API", "long-running function", "streaming agent",
|
|
12
|
-
"WebSocket server", "webhook handler", "
|
|
13
|
-
"function that won't time out",
|
|
8
|
+
webhook handler, a Discord bot, an MCP server, or any request/response
|
|
9
|
+
workload that risks timing out on short, lambda-style serverless functions —
|
|
10
|
+
and wants it to branch with their database. Triggers include "serverless
|
|
11
|
+
function", "deploy an API", "long-running function", "streaming agent",
|
|
12
|
+
"SSE server", "WebSocket server", "webhook handler", "MCP server",
|
|
13
|
+
"run code next to my database", "function that won't time out",
|
|
14
|
+
"function logs", "Neon Functions", and "Neon Compute".
|
|
15
|
+
metadata:
|
|
16
|
+
parent: neon
|
|
17
|
+
source: https://github.com/neondatabase/agent-skills/tree/main/skills/neon-functions
|
|
14
18
|
---
|
|
15
19
|
|
|
20
|
+
**FIRST**: Use the parent `neon` skill for a Neon overview, getting started with Neon, Neon development best practices, and more.
|
|
21
|
+
|
|
22
|
+
If the `neon` skill is not installed, fetch it from https://neon.com/docs/ai/skills/neon/SKILL.md or install it with:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx skills add neondatabase/agent-skills --skill neon
|
|
26
|
+
```
|
|
27
|
+
|
|
16
28
|
# Neon Functions
|
|
17
29
|
|
|
18
|
-
This is a
|
|
30
|
+
This is a public beta feature and only available in `us-east-2`.
|
|
31
|
+
|
|
32
|
+
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.
|
|
19
33
|
|
|
20
34
|
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.
|
|
21
35
|
|
|
@@ -29,24 +43,28 @@ Reach for Neon Functions when the workload is a request/response handler that be
|
|
|
29
43
|
- **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.
|
|
30
44
|
- **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.
|
|
31
45
|
|
|
32
|
-
If the workload is a pure static site, a cron/background job that needs its own lifecycle and cancellation, or something that must run outside `us-east-2` today, this isn't the right tool yet (see Timeouts and
|
|
46
|
+
If the workload is a pure static site, a cron/background job that needs its own lifecycle and cancellation, or something that must run outside `us-east-2` today, this isn't the right tool yet (see [Timeouts and Runtime Limits](#timeouts-and-runtime-limits) and [Availability](#availability)).
|
|
33
47
|
|
|
34
48
|
## What It Does
|
|
35
49
|
|
|
36
|
-
- **Long-running & serverless** — Built for WebSocket servers (see [WebSocket
|
|
50
|
+
- **Long-running & serverless** — Built for WebSocket servers (see [WebSocket Servers](#websocket-servers)), SSE endpoints (see [Server-Sent Events (SSE)](#server-sent-events-sse)), long agent HTTP streams, and APIs. Still scales to zero when idle.
|
|
37
51
|
- **Web-standard handler** — A function is any default export with a `fetch(request)` method returning a `Response` (Workers/WinterTC-compatible). A Hono app exports exactly that shape, so `export default app` just works. Runs on Node.js 24, so all Node APIs are available.
|
|
38
52
|
- **Close to your database** — Runs in the branch's region; `DATABASE_URL` injected automatically when the branch has Postgres.
|
|
39
53
|
- **Branchable** — Each branch runs its own function version at its own URL against its own isolated state.
|
|
40
54
|
- **Same CLI/API** — Deploy and manage via `neon`, `neon.ts`, or the Neon API.
|
|
41
55
|
|
|
42
|
-
##
|
|
56
|
+
## Availability
|
|
57
|
+
|
|
58
|
+
Check this precondition before setting anything up: Neon Functions is a public beta feature available only on new projects in the `us-east-2` region. Confirm the user's Neon project is a new project in `us-east-2`; it can't be enabled on existing projects. Functions usage isn't billed during the public beta.
|
|
59
|
+
|
|
60
|
+
## Architecture: Where Functions Fit
|
|
43
61
|
|
|
44
62
|
Neon (Functions included) is **backend primitives, not full-stack app hosting**. Host your app on **Vercel** (or Netlify, or another frontend/app host); Functions are the long-running, stateful slice of your backend that lives next to your data. They compose with that platform in two ways:
|
|
45
63
|
|
|
46
|
-
- **Add a Function to a full-stack app.** Your Next.js / TanStack Start app on Vercel (or Netlify) owns UI, auth (e.g. Neon Auth), and talks directly to
|
|
64
|
+
- **Add a Function to a full-stack app.** Your Next.js / TanStack Start app on Vercel (or Netlify) owns UI, auth (e.g. Neon Auth), and talks directly to Lakebase Postgres and Object Storage. When one workload outgrows the host's short serverless limits — a WebSocket or SSE server, or a long-running agent that would time out — move just that piece onto a Neon Function. (See [Functions as an Agent Backend](#functions-as-an-agent-backend-nextjs-and-similar-frameworks) for the client-direct pattern.)
|
|
47
65
|
- **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.
|
|
48
66
|
|
|
49
|
-
Either way, secure a Function like any standalone REST API: verify a JWT or API key at the top of the handler (see the WARNING under [Functions as an
|
|
67
|
+
Either way, secure a Function like any standalone REST API: verify a JWT or API key at the top of the handler (see the WARNING under [Functions as an Agent Backend](#functions-as-an-agent-backend-nextjs-and-similar-frameworks)). 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.
|
|
50
68
|
|
|
51
69
|
## Setup
|
|
52
70
|
|
|
@@ -79,11 +97,13 @@ import { Hono } from "hono";
|
|
|
79
97
|
import { drizzle } from "drizzle-orm/node-postgres";
|
|
80
98
|
import { Pool } from "pg";
|
|
81
99
|
import { parseEnv } from "@neon/env";
|
|
100
|
+
import { attachDatabasePool } from "@neon/functions";
|
|
82
101
|
import config from "../neon";
|
|
83
102
|
import { todos } from "./db/schema";
|
|
84
103
|
|
|
85
104
|
const env = parseEnv(config);
|
|
86
105
|
const pool = new Pool({ connectionString: env.postgres.databaseUrl, max: 5 });
|
|
106
|
+
attachDatabasePool(pool);
|
|
87
107
|
const db = drizzle(pool);
|
|
88
108
|
|
|
89
109
|
const app = new Hono();
|
|
@@ -98,23 +118,24 @@ app.get("/todos", async (c) => c.json(await db.select().from(todos)));
|
|
|
98
118
|
export default app;
|
|
99
119
|
```
|
|
100
120
|
|
|
101
|
-
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.
|
|
121
|
+
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](#connecting-to-postgres).
|
|
102
122
|
|
|
103
123
|
`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`):
|
|
104
124
|
|
|
105
125
|
```typescript
|
|
106
126
|
const { postgres } = parseEnv(config, ["DATABASE_URL"]); // not the unpooled URL, auth, etc.
|
|
107
127
|
const pool = new Pool({ connectionString: postgres.databaseUrl, max: 5 });
|
|
128
|
+
attachDatabasePool(pool);
|
|
108
129
|
```
|
|
109
130
|
|
|
110
|
-
## Develop
|
|
131
|
+
## Develop Locally and Deploy
|
|
111
132
|
|
|
112
133
|
```bash
|
|
113
134
|
neon dev # serves every function in neon.ts with hot reload; injects DATABASE_URL & friends
|
|
114
135
|
neon deploy # bundles with esbuild, uploads, and applies neon.ts to the linked branch
|
|
115
136
|
```
|
|
116
137
|
|
|
117
|
-
To deploy a single function without `neon.ts`: `neon functions deploy <slug> --
|
|
138
|
+
To deploy a single function without `neon.ts`: `neon functions deploy <slug> --src src/index.ts` (`--src` takes either the entry file or a directory containing `index.ts`, `index.mjs`, or `index.js`). Retrieve the public URL with `neon functions get <slug>` (the `invocation_url` field, of the form `https://<branch_id>-<slug>.compute.c-1.us-east-2.aws.neon.tech`). Manage with `neon functions list|get|delete`.
|
|
118
139
|
|
|
119
140
|
When `neon checkout` _creates_ a new branch and a `neon.ts` is present, it applies the policy automatically — deploying the function to the fresh branch. Checking out an existing branch does not re-deploy; run `neon deploy` explicitly.
|
|
120
141
|
|
|
@@ -143,17 +164,17 @@ export default defineConfig({
|
|
|
143
164
|
});
|
|
144
165
|
```
|
|
145
166
|
|
|
146
|
-
## Environment
|
|
167
|
+
## Environment Variables
|
|
147
168
|
|
|
148
169
|
Neon injects branch-scoped connection strings and service URLs at runtime — you don't declare these or pass them at deploy time:
|
|
149
170
|
|
|
150
|
-
| Variable | Notes
|
|
151
|
-
| ----------------------- |
|
|
171
|
+
| Variable | Notes |
|
|
172
|
+
| ----------------------- | -------------------------------------------------------------------------------------------------- |
|
|
152
173
|
| `NEON_BRANCH` | The branch **name** (e.g. `main`, `preview/foo`). Injected on every branch, including the default. |
|
|
153
|
-
| `DATABASE_URL` | Pooled connection string. Use for most queries. Present only if the branch has Postgres.
|
|
154
|
-
| `DATABASE_URL_UNPOOLED` | Direct connection. Use for migrations, `LISTEN`/`NOTIFY`, multi-round-trip transactions.
|
|
155
|
-
| `NEON_AUTH_BASE_URL` | Present when Neon Auth is enabled on the branch.
|
|
156
|
-
| `NEON_DATA_API_URL` | Present when the Data API is enabled on the branch.
|
|
174
|
+
| `DATABASE_URL` | Pooled connection string. Use for most queries. Present only if the branch has Postgres. |
|
|
175
|
+
| `DATABASE_URL_UNPOOLED` | Direct connection. Use for migrations, `LISTEN`/`NOTIFY`, multi-round-trip transactions. |
|
|
176
|
+
| `NEON_AUTH_BASE_URL` | Present when Neon Auth is enabled on the branch. |
|
|
177
|
+
| `NEON_DATA_API_URL` | Present when the Data API is enabled on the branch. |
|
|
157
178
|
|
|
158
179
|
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.
|
|
159
180
|
|
|
@@ -185,114 +206,202 @@ When the branch has Postgres, Neon **injects the connection strings at runtime**
|
|
|
185
206
|
Create the connection pool **once at module scope** and reuse it across requests — don't open a connection per request:
|
|
186
207
|
|
|
187
208
|
```typescript
|
|
209
|
+
import { attachDatabasePool } from "@neon/functions";
|
|
188
210
|
import { drizzle } from "drizzle-orm/node-postgres";
|
|
189
211
|
import { Pool } from "pg";
|
|
190
212
|
|
|
191
|
-
// Created once per isolate; reused by every request that isolate handles.
|
|
192
213
|
const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 5 });
|
|
214
|
+
attachDatabasePool(pool);
|
|
193
215
|
const db = drizzle(pool);
|
|
194
216
|
```
|
|
195
217
|
|
|
196
|
-
|
|
218
|
+
node-postgres emits idle-client failures as `error` on the pool. With no listener that is an `uncaughtException` and Node exits the isolate. Call `attachDatabasePool(pool)` once after `new Pool`. Requires `@neon/functions` ≥ 0.8.0. Expected idle disconnects (`ECONNRESET`, `EPIPE`, `ETIMEDOUT`, Postgres `57P01`, node-postgres's `Connection terminated unexpectedly`) are silent. Anything else is `console.error`, or `onUnexpectedError` if you pass it on the first call. The first call wins; a later call that passes `onUnexpectedError` is ignored and warns. This does not close the pool.
|
|
219
|
+
|
|
220
|
+
**Pooling is recommended because an isolate is reused across many requests** (and several requests can be in flight on the same isolate at once — see [Timeouts and Runtime Limits](#timeouts-and-runtime-limits)). A module-scope pool is opened once on cold start and then shared by every subsequent request that isolate serves, so you amortize connection setup instead of paying it on every request and you avoid exhausting Postgres connections under load.
|
|
197
221
|
|
|
198
222
|
Keep `max` small (e.g. `5`): each isolate keeps its own pool, so total connections to Postgres scale with the number of live isolates. You don't need to close the pool on shutdown — when the runtime evicts an isolate it sends `SIGINT`/`SIGTERM`, and Neon's pooler reclaims those connections for you, so an explicit drain handler is redundant.
|
|
199
223
|
|
|
200
224
|
> Reading `process.env.DATABASE_URL` directly works everywhere. The function in [Setup](#setup) instead uses `@neon/env`'s `parseEnv(config)` to read the same value in a typed, validated way — either is fine.
|
|
201
225
|
|
|
202
|
-
##
|
|
226
|
+
## Timeouts and Runtime Limits
|
|
227
|
+
|
|
228
|
+
Functions are long-running but **still serverless** — they are a request/response runtime, not a background job runner. The hard limits:
|
|
229
|
+
|
|
230
|
+
- **Time to first byte: 15 minutes.** Your handler must _begin_ returning a response within 15 minutes of receiving a request. Most handlers finish in seconds; the 15-minute ceiling exists so agent workloads like image/video generation have room.
|
|
231
|
+
- **Heartbeat: 15 minutes.** Open WebSocket/SSE connections stay alive as long as data flows. The timeout only fires when a connection goes silent — send at least one byte every 15 minutes to keep a quiet stream alive.
|
|
232
|
+
- **`waitUntil`: 15 minutes.** Work registered with `waitUntil` keeps the invocation alive after the response is sent, up to 15 minutes — for cleanup like analytics writes and audit logs, **not** a background job runner. (`waitUntil` from `@neon/functions` is currently a stub during the preview.)
|
|
233
|
+
- **Idle eviction.** With no active connections Neon shuts the function down; it may also evict/restart for operational reasons — e.g. maintenance, or moving the function to a different compute node (active functions can run for hours first). Treat eviction like a process restart — WebSocket/SSE clients must reconnect. Neon sends `SIGINT` before evicting, so a `process.on("SIGINT", ...)` handler lets you detect that the function is about to be evicted and run any last-minute cleanup. You don't need one just to close Postgres connections — Neon's pooler reclaims those on its own.
|
|
234
|
+
- **Runtime:** Node.js 24, memory fixed at 2048 MiB during the preview. Slugs must match `^[a-z0-9]{1,20}$`. **An isolate is reused across many requests** — multiple requests can be in flight on the same isolate at once (interleaved on Node's single-threaded event loop), and under load the runtime runs several isolates in parallel, each with its own copy of module state. State held in module scope is therefore per-isolate (shared by every request that isolate handles) and in-memory only — persist anything that must survive eviction in Postgres. This reuse is exactly why you create a connection pool once at module scope rather than per request (see [Connecting to Postgres](#connecting-to-postgres)).
|
|
235
|
+
|
|
236
|
+
## Functions as an Agent Backend (Next.js and Similar Frameworks)
|
|
237
|
+
|
|
238
|
+
A Neon Function is a great home for an AI agent precisely because it **doesn't time out** the way lambda-style serverless does (15-minute budget, see [Timeouts and Runtime Limits](#timeouts-and-runtime-limits)). But that advantage disappears the moment you **proxy the agent stream through your web app's backend** — a Next.js route handler, Remix/SvelteKit/Nuxt action, etc. hosted on Vercel, Netlify, Cloudflare, and the like. Those platforms cap serverless/edge execution at short windows (often ~10–60s, sometimes up to ~300s), so a long agent or image/video generation stream gets cut off mid-response even though the Neon Function would happily keep going.
|
|
239
|
+
|
|
240
|
+
**Building the agent itself.** The [Vercel AI SDK](https://ai-sdk.dev) and [Mastra](https://mastra.ai) are the recommended ways to build the agent — point either at the Neon AI Gateway (see the `neon-ai-gateway` skill) for one credential across every model, with no extra provider keys. For a complete AI SDK agent running as a Function (streaming `toUIMessageStreamResponse`, multi-step tool calling next to Postgres, and persisting generated images to Object Storage), see [references/ai-sdk.md](https://neon.com/docs/ai/skills/neon-functions/references/ai-sdk.md); for the Mastra equivalent with built-in tracing, see [references/mastra-studio.md](https://neon.com/docs/ai/skills/neon-functions/references/mastra-studio.md).
|
|
241
|
+
|
|
242
|
+
**The fix: call the function directly from the client.** Don't route the long request through your app server.
|
|
243
|
+
|
|
244
|
+
```
|
|
245
|
+
Browser ──(Authorization: Bearer <JWT>)──▶ Neon Function (agent) ✅ no host timeout
|
|
246
|
+
Browser ──▶ your app backend ──▶ Neon Function ❌ host cuts the stream
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
- Mint a **short-lived JWT** on your app backend (e.g. better-auth's `jwt` plugin, NextAuth, or your own signer) — that call is fast and well within host limits.
|
|
250
|
+
- Hand the token to the client and have it call the Neon Function **directly** (cross-origin), e.g. with the Vercel AI SDK: `new DefaultChatTransport({ api: NEON_FUNCTION_URL, fetch })` where `fetch` attaches `Authorization: Bearer <token>`. Your app server is never in the path of the long stream.
|
|
251
|
+
- Add **CORS** so the browser can reach it (handle `OPTIONS`, set `Access-Control-Allow-Origin`/`-Headers`).
|
|
252
|
+
|
|
253
|
+
> [!WARNING]
|
|
254
|
+
> A Neon Function has a **public HTTPS URL — it is reachable by anyone.** A direct client→function call means there is no app backend in front of it to gate access, so **you must authenticate the function yourself.** Verify a JWT (e.g. against your app's JWKS), check a shared secret / API key, or validate a session token at the top of the handler and reject anything else. Never deploy an unauthenticated agent.
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
// src/index.ts — verify the caller before doing any work
|
|
258
|
+
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
203
259
|
|
|
204
|
-
|
|
260
|
+
const jwks = createRemoteJWKSet(
|
|
261
|
+
new URL(`${process.env.AUTH_BASE_URL}/api/auth/jwks`),
|
|
262
|
+
);
|
|
205
263
|
|
|
206
|
-
|
|
264
|
+
export default {
|
|
265
|
+
async fetch(request: Request) {
|
|
266
|
+
if (request.method === "OPTIONS")
|
|
267
|
+
return new Response(null, { status: 204, headers: cors(request) });
|
|
268
|
+
|
|
269
|
+
const auth = request.headers.get("authorization");
|
|
270
|
+
if (!auth?.toLowerCase().startsWith("bearer ")) {
|
|
271
|
+
return new Response("Unauthorized", {
|
|
272
|
+
status: 401,
|
|
273
|
+
headers: cors(request),
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
try {
|
|
277
|
+
const { payload } = await jwtVerify(auth.slice(7), jwks, {
|
|
278
|
+
issuer: process.env.AUTH_BASE_URL,
|
|
279
|
+
audience: process.env.AUTH_BASE_URL,
|
|
280
|
+
});
|
|
281
|
+
const userId = payload.sub; // scope the agent to this user
|
|
282
|
+
// ... run the agent, return result.toUIMessageStreamResponse({ headers: cors(request) })
|
|
283
|
+
} catch {
|
|
284
|
+
return new Response("Unauthorized", {
|
|
285
|
+
status: 401,
|
|
286
|
+
headers: cors(request),
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Pass the JWKS/issuer URL to the function via its `env` (see [Environment Variables](#environment-variables)). Persist anything you need to keep (generated images, history) in Postgres — module state doesn't survive eviction.
|
|
294
|
+
|
|
295
|
+
## WebSocket Servers
|
|
296
|
+
|
|
297
|
+
A WebSocket server is the canonical Functions workload: a long-running handler holds connections open in-process, with no external state store needed to keep a stream coherent. The connection stays alive as long as bytes flow (15-minute heartbeat, see [Timeouts](#timeouts-and-runtime-limits)).
|
|
298
|
+
|
|
299
|
+
**Upgrade from inside `fetch`.** Call `upgradeWebSocket(request)` from [`@neon/functions`](https://www.npmjs.com/package/@neon/functions) and return the response it gives you. There is one entrypoint and no WebSocket dependency to install:
|
|
207
300
|
|
|
208
301
|
```typescript
|
|
302
|
+
import { upgradeWebSocket } from "@neon/functions";
|
|
303
|
+
|
|
209
304
|
export default {
|
|
210
|
-
fetch(
|
|
211
|
-
|
|
305
|
+
async fetch(req: Request): Promise<Response> {
|
|
306
|
+
if (req.headers.get("upgrade")?.toLowerCase() !== "websocket") {
|
|
307
|
+
return new Response("expected a websocket upgrade", { status: 426 });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const { socket, response } = upgradeWebSocket(req);
|
|
311
|
+
socket.addEventListener("message", (event) => socket.send(event.data));
|
|
312
|
+
return response;
|
|
313
|
+
},
|
|
212
314
|
};
|
|
213
315
|
```
|
|
214
316
|
|
|
215
|
-
|
|
317
|
+
`socket` is a standard [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket), so `addEventListener` and the `onopen`/`onmessage`/`onclose`/`onerror` properties both work. It is still `CONNECTING` when you get it — the runtime writes the `101` only once your handler returns `response`, and the socket opens then.
|
|
318
|
+
|
|
319
|
+
Three rules that matter:
|
|
320
|
+
|
|
321
|
+
- **Return `response` unchanged.** A `101` can't be built as a plain `Response` (the fetch spec caps constructed responses at 200–599), so the runtime hands back an object carrying the pending upgrade. `clone()`, or rebuilding it with `new Response(res.body, res)` as response-rewriting middleware does, discards the upgrade and fails the request.
|
|
322
|
+
- **Refuse a handshake by returning an ordinary `Response`.** A `401`, `403` or `404` is relayed to the client as-is. That is how you gate a socket.
|
|
323
|
+
- **`binaryType` defaults to `"arraybuffer"`**, not the browser's `"blob"`. `event.data` is a `string` for text frames and an `ArrayBuffer` for binary ones, so branch on `typeof`.
|
|
324
|
+
|
|
325
|
+
**With auth.** Browsers can't set headers on a WebSocket, so authenticate with a `?token=` query param (verify it the same way as the [agent backend](#functions-as-an-agent-backend-nextjs-and-similar-frameworks): `jwtVerify` against your JWKS) and refuse before upgrading:
|
|
216
326
|
|
|
217
327
|
```typescript
|
|
218
328
|
// src/index.ts
|
|
219
|
-
import
|
|
220
|
-
import type { Duplex } from "node:stream";
|
|
221
|
-
import { WebSocketServer, type WebSocket } from "ws";
|
|
329
|
+
import { upgradeWebSocket } from "@neon/functions";
|
|
222
330
|
|
|
223
331
|
const clients = new Set<WebSocket>();
|
|
224
|
-
const wss = new WebSocketServer({ noServer: true });
|
|
225
332
|
|
|
226
333
|
export default {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
// The runtime hands the WebSocket handshake to upgrade().
|
|
231
|
-
async upgrade(req: IncomingMessage, socket: Duplex, head: Buffer) {
|
|
232
|
-
const url = new URL(req.url ?? "/", "http://localhost");
|
|
233
|
-
const identity = await verifyToken(url.searchParams.get("token")); // reject if invalid
|
|
234
|
-
if (!identity) {
|
|
235
|
-
socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
|
|
236
|
-
socket.destroy();
|
|
237
|
-
return;
|
|
334
|
+
async fetch(request: Request): Promise<Response> {
|
|
335
|
+
if (request.headers.get("upgrade")?.toLowerCase() !== "websocket") {
|
|
336
|
+
return new Response("WebSocket endpoint — connect with ?token=<jwt>");
|
|
238
337
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
338
|
+
|
|
339
|
+
const url = new URL(request.url);
|
|
340
|
+
const identity = await verifyToken(url.searchParams.get("token"));
|
|
341
|
+
if (!identity) return new Response("unauthorized", { status: 401 });
|
|
342
|
+
|
|
343
|
+
const { socket, response } = upgradeWebSocket(request);
|
|
344
|
+
clients.add(socket);
|
|
345
|
+
socket.addEventListener("close", () => clients.delete(socket));
|
|
346
|
+
socket.addEventListener("message", (event) => {
|
|
347
|
+
if (typeof event.data !== "string") return;
|
|
348
|
+
persist(identity.id, event.data); // fan out to every isolate — see below
|
|
243
349
|
});
|
|
350
|
+
return response;
|
|
244
351
|
},
|
|
245
352
|
};
|
|
246
353
|
```
|
|
247
354
|
|
|
248
|
-
**
|
|
355
|
+
**Subprotocols.** Pass `{ protocol }` to select one the client offered; it is echoed in `Sec-WebSocket-Protocol` and exposed as `socket.protocol`. Selecting one the client did not offer throws a `TypeError`. Omit it and no protocol is negotiated. No extensions are negotiated either — `socket.extensions` is always `""` and `permessage-deflate` is not available.
|
|
249
356
|
|
|
250
|
-
|
|
357
|
+
**Hono.** Nothing special is needed: `upgradeWebSocket` takes a `Request`, so call it inside a route with `c.req.raw` and return the response. Auth and everything else is ordinary middleware.
|
|
251
358
|
|
|
252
359
|
```typescript
|
|
253
360
|
// src/index.ts
|
|
254
361
|
import { Hono } from "hono";
|
|
255
|
-
import {
|
|
362
|
+
import { upgradeWebSocket } from "@neon/functions";
|
|
256
363
|
|
|
257
364
|
const app = new Hono();
|
|
258
|
-
const { upgradeWebSocket, handler } = createNeonWebSocket(app);
|
|
259
365
|
|
|
260
|
-
app.get(
|
|
261
|
-
"/ws",
|
|
262
|
-
async (c, next) => {
|
|
263
|
-
if (!(await verifyToken(c.req.query("token")))) return c.text("Unauthorized", 401);
|
|
264
|
-
await next();
|
|
265
|
-
},
|
|
266
|
-
upgradeWebSocket(() => ({
|
|
267
|
-
onOpen: (_evt, ws) => ws.send("welcome"),
|
|
268
|
-
onMessage: (evt, ws) => ws.send(`echo: ${evt.data}`),
|
|
269
|
-
onClose: () => console.log("disconnected"),
|
|
270
|
-
})),
|
|
271
|
-
);
|
|
366
|
+
app.get("/", (c) => c.text("ok"));
|
|
272
367
|
|
|
273
|
-
|
|
274
|
-
|
|
368
|
+
app.get("/ws", async (c) => {
|
|
369
|
+
const identity = await verifyToken(c.req.query("token"));
|
|
370
|
+
if (!identity) return c.text("Unauthorized", 401);
|
|
275
371
|
|
|
276
|
-
|
|
372
|
+
const { socket, response } = upgradeWebSocket(c.req.raw);
|
|
373
|
+
socket.addEventListener("open", () => socket.send("welcome"));
|
|
374
|
+
socket.addEventListener("message", (event) =>
|
|
375
|
+
socket.send(`echo: ${event.data}`),
|
|
376
|
+
);
|
|
377
|
+
return response;
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
export default { fetch: (request: Request) => app.fetch(request) };
|
|
381
|
+
```
|
|
277
382
|
|
|
278
383
|
### Heartbeat (keep the socket alive)
|
|
279
384
|
|
|
280
|
-
A connection stays open **only while bytes flow**: Neon evicts a silent stream after 15 minutes ([Timeouts and
|
|
385
|
+
A connection stays open **only while bytes flow**: Neon evicts a silent stream after 15 minutes ([Timeouts and Runtime Limits](#timeouts-and-runtime-limits)), and intermediary proxies / load balancers are usually far stricter (often tens of seconds). Don't rely on the app being chatty enough — send a periodic keepalive from the server so the socket never goes quiet.
|
|
386
|
+
|
|
387
|
+
The standard `WebSocket` interface has no `ping()`, so send an application-level message the client ignores:
|
|
281
388
|
|
|
282
389
|
```typescript
|
|
283
390
|
const HEARTBEAT_MS = 25_000; // comfortably under proxy idle timeouts
|
|
284
391
|
|
|
285
392
|
const beat = setInterval(() => {
|
|
286
|
-
for (const
|
|
393
|
+
for (const socket of clients) {
|
|
394
|
+
if (socket.readyState === socket.OPEN) socket.send('{"type":"ping"}');
|
|
395
|
+
}
|
|
287
396
|
}, HEARTBEAT_MS);
|
|
288
397
|
beat.unref?.();
|
|
289
398
|
```
|
|
290
399
|
|
|
291
|
-
|
|
400
|
+
The client should skip these when handling messages. The server does answer a client-sent ping frame with a pong automatically, so a browser client can drive the heartbeat instead if you'd rather not filter messages.
|
|
292
401
|
|
|
293
402
|
### Keeping clients in sync across isolates (do not skip this)
|
|
294
403
|
|
|
295
|
-
Under load the runtime runs **several isolates in parallel, each with its own copy of module state** — so each isolate has its own `clients` set. Broadcasting only to that local set means a client on isolate A never sees an event produced on isolate B, and the feed silently fractures. It's easy to miss: `neon dev` runs a single process (one isolate), so in-process broadcast always
|
|
404
|
+
Under load the runtime runs **several isolates in parallel, each with its own copy of module state** — so each isolate has its own `clients` set. Broadcasting only to that local set means a client on isolate A never sees an event produced on isolate B, and the feed silently fractures. It's easy to miss: `neon dev` runs a single process (one isolate), so in-process broadcast always _looks_ fine locally but breaks in production, where concurrent connections spread across many isolates.
|
|
296
405
|
|
|
297
406
|
Module state doesn't survive eviction anyway, so **Postgres is the shared source of truth**. Pick a fan-out strategy. In every snippet below, `pool` is a pooled `pg` client and `clients` is this isolate's `Set` of live connections.
|
|
298
407
|
|
|
@@ -308,7 +417,9 @@ const poller = setInterval(async () => {
|
|
|
308
417
|
);
|
|
309
418
|
for (const { id, payload } of rows) {
|
|
310
419
|
lastId = id;
|
|
311
|
-
for (const
|
|
420
|
+
for (const socket of clients) {
|
|
421
|
+
if (socket.readyState === socket.OPEN) socket.send(payload);
|
|
422
|
+
}
|
|
312
423
|
}
|
|
313
424
|
}, 1000);
|
|
314
425
|
poller.unref?.();
|
|
@@ -321,23 +432,37 @@ poller.unref?.();
|
|
|
321
432
|
**2. `LISTEN`/`NOTIFY` — lowest latency, but requires disabling Scale to Zero.** Each isolate `LISTEN`s on a channel over a dedicated **unpooled** connection; broadcasting is `NOTIFY`, so every isolate (including the sender's) re-pushes to its sockets. Near-instant — but the listener holds an idle connection that **does not count as active**, so [Scale to Zero](https://neon.com/docs/introduction/scale-to-zero) suspends the compute and drops it, silently killing the feed. Only use it on an **always-on** compute (Scale to Zero disabled — a paid-plan setting).
|
|
322
433
|
|
|
323
434
|
```typescript
|
|
435
|
+
import { attachDatabasePool } from "@neon/functions";
|
|
324
436
|
import { Pool, Client } from "pg";
|
|
325
437
|
|
|
326
438
|
const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 5 });
|
|
439
|
+
attachDatabasePool(pool);
|
|
327
440
|
const CHANNEL = "chat_events";
|
|
328
441
|
|
|
329
442
|
// One dedicated DIRECT connection per isolate, just to receive events.
|
|
330
443
|
// Use DATABASE_URL_UNPOOLED — LISTEN needs a real session, not a pooled one.
|
|
331
|
-
|
|
444
|
+
// Don't call attachDatabasePool here: it would silence the idle drop that killed the feed.
|
|
445
|
+
// An error listener keeps the isolate alive; the feed stays down until the isolate restarts.
|
|
446
|
+
const listener = new Client({
|
|
447
|
+
connectionString: process.env.DATABASE_URL_UNPOOLED,
|
|
448
|
+
});
|
|
449
|
+
listener.on("error", (err) => {
|
|
450
|
+
console.error(err);
|
|
451
|
+
});
|
|
332
452
|
listener.connect().then(() => listener.query(`LISTEN ${CHANNEL}`));
|
|
333
453
|
listener.on("notification", (msg) => {
|
|
334
454
|
if (!msg.payload) return;
|
|
335
|
-
for (const
|
|
455
|
+
for (const socket of clients) {
|
|
456
|
+
if (socket.readyState === socket.OPEN) socket.send(msg.payload);
|
|
457
|
+
}
|
|
336
458
|
});
|
|
337
459
|
|
|
338
460
|
// Broadcast by NOTIFYing through the pool — every isolate's listener fires.
|
|
339
461
|
function broadcast(event: unknown) {
|
|
340
|
-
return pool.query("SELECT pg_notify($1, $2)", [
|
|
462
|
+
return pool.query("SELECT pg_notify($1, $2)", [
|
|
463
|
+
CHANNEL,
|
|
464
|
+
JSON.stringify(event),
|
|
465
|
+
]);
|
|
341
466
|
}
|
|
342
467
|
```
|
|
343
468
|
|
|
@@ -350,27 +475,34 @@ function broadcast(event: unknown) {
|
|
|
350
475
|
Idle functions are evicted (and isolates restart for operational reasons), so a client's socket **will** drop — treat reconnection as normal, not exceptional. Reconnect with exponential backoff, capped, and **re-mint a fresh token on every attempt** (tokens are short-lived, so a stale one fails the `upgrade` auth check):
|
|
351
476
|
|
|
352
477
|
```typescript
|
|
353
|
-
let closed = false,
|
|
478
|
+
let closed = false,
|
|
479
|
+
retry = 0,
|
|
480
|
+
timer: ReturnType<typeof setTimeout>;
|
|
354
481
|
|
|
355
482
|
async function connect() {
|
|
356
483
|
if (closed) return;
|
|
357
484
|
const token = await getToken(); // re-mint each attempt; short-lived
|
|
358
485
|
const ws = new WebSocket(`${WS_URL}?token=${encodeURIComponent(token)}`);
|
|
359
|
-
ws.onopen = () => {
|
|
360
|
-
|
|
486
|
+
ws.onopen = () => {
|
|
487
|
+
retry = 0; // reset backoff on success
|
|
488
|
+
};
|
|
489
|
+
ws.onmessage = (e) => {
|
|
490
|
+
/* apply the event */
|
|
491
|
+
};
|
|
361
492
|
ws.onclose = () => {
|
|
362
|
-
if (!closed)
|
|
493
|
+
if (!closed)
|
|
494
|
+
timer = setTimeout(connect, Math.min(1000 * 2 ** retry++, 15000));
|
|
363
495
|
};
|
|
364
|
-
ws.onerror = () => ws.close();
|
|
496
|
+
ws.onerror = () => ws.close(); // let onclose drive the retry
|
|
365
497
|
}
|
|
366
498
|
connect();
|
|
367
499
|
```
|
|
368
500
|
|
|
369
|
-
Together —
|
|
501
|
+
Together — `upgradeWebSocket` inside `fetch`, JWT auth over `?token=`, cross-isolate fan-out, and client backoff — these compose into a complete realtime chat backend on a single function.
|
|
370
502
|
|
|
371
|
-
## Server-
|
|
503
|
+
## Server-Sent Events (SSE)
|
|
372
504
|
|
|
373
|
-
When you only need **server → client** streaming (live counters, notifications, progress, token streams), SSE is simpler than a WebSocket and needs no
|
|
505
|
+
When you only need **server → client** streaming (live counters, notifications, progress, token streams), SSE is simpler than a WebSocket and needs no upgrade at all: a plain `fetch` handler returns a `Response` whose body is a `ReadableStream` with `Content-Type: text/event-stream`, and the runtime holds it open as long as bytes flow. The browser consumes it with `EventSource`, which **reconnects on its own** — so there's no client backoff to write.
|
|
374
506
|
|
|
375
507
|
```typescript
|
|
376
508
|
// src/index.ts — minimal SSE endpoint
|
|
@@ -381,20 +513,30 @@ export default {
|
|
|
381
513
|
new ReadableStream<Uint8Array>({
|
|
382
514
|
start(controller) {
|
|
383
515
|
controller.enqueue(encoder.encode("data: hello\n\n"));
|
|
384
|
-
const t = setInterval(
|
|
516
|
+
const t = setInterval(
|
|
517
|
+
() => controller.enqueue(encoder.encode(": ping\n\n")),
|
|
518
|
+
25_000,
|
|
519
|
+
);
|
|
385
520
|
return () => clearInterval(t); // fires when the client disconnects
|
|
386
521
|
},
|
|
387
522
|
}),
|
|
388
|
-
{
|
|
523
|
+
{
|
|
524
|
+
headers: {
|
|
525
|
+
"Content-Type": "text/event-stream",
|
|
526
|
+
"Cache-Control": "no-cache, no-transform",
|
|
527
|
+
},
|
|
528
|
+
},
|
|
389
529
|
),
|
|
390
530
|
};
|
|
391
531
|
```
|
|
392
532
|
|
|
393
|
-
The same rules as WebSockets apply. **Heartbeat:** a stream stays open only while bytes flow — Neon's window is 15 minutes ([Timeouts and
|
|
533
|
+
The same rules as WebSockets apply. **Heartbeat:** a stream stays open only while bytes flow — Neon's window is 15 minutes ([Timeouts and Runtime Limits](#timeouts-and-runtime-limits)) but proxies are usually far stricter, so emit a `: ping\n\n` comment every ~25–30s (shown above) to keep idle streams from being dropped. Keep state in Postgres, and fan out across isolates using one of the [sync strategies](#keeping-clients-in-sync-across-isolates-do-not-skip-this) (hold a `Set` of stream controllers and `enqueue` to each). `EventSource` is GET-only and can't set headers, so authenticate with a `?token=` query param or cookie, exactly like the WebSocket case. [references/sse.md](https://neon.com/docs/ai/skills/neon-functions/references/sse.md) has the full pattern — Hono variant, cross-isolate fan-out, wire format, client, and caveats.
|
|
534
|
+
|
|
535
|
+
## MCP Servers
|
|
394
536
|
|
|
395
|
-
|
|
537
|
+
An [MCP](https://modelcontextprotocol.io) server is a natural Functions workload: a long-running HTTP handler that exposes tools to AI clients (Cursor, Claude, ChatGPT, agents), with those tools reading and writing the branch's Postgres right next to the compute. MCP's **streamable HTTP transport** is a plain `POST`/`GET` on a single endpoint (conventionally `/mcp`), so it maps onto a function's `fetch` handler with no `upgrade` method or extra protocol.
|
|
396
538
|
|
|
397
|
-
|
|
539
|
+
The simplest host is a Hono app using the official [`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol/typescript-sdk) plus [`@hono/mcp`](https://github.com/honojs/middleware/tree/main/packages/mcp), which bridges the transport to a route. Build the server, register its tools, and create the transport once at module scope, then hand every `/mcp` request to it:
|
|
398
540
|
|
|
399
541
|
```typescript
|
|
400
542
|
const transport = new StreamableHTTPTransport();
|
|
@@ -404,82 +546,30 @@ app.all("/mcp", async (c) => {
|
|
|
404
546
|
});
|
|
405
547
|
```
|
|
406
548
|
|
|
407
|
-
Because the function's URL is public, **authenticate before connecting the transport** — [Better Auth](https://better-auth.com) covers both OAuth (its MCP plugin makes your app the authorization server so third-party clients self-authorize per the MCP spec) and a simpler API-key / session-JWT check for your own callers. [references/mcp.md](references/mcp.md) has the full pattern — server with Postgres-backed tools via Drizzle, both Better Auth auth options, and testing with `mcporter` / `add-mcp`.
|
|
408
|
-
|
|
409
|
-
## Integrations and observability
|
|
549
|
+
Because the function's URL is public, **authenticate before connecting the transport** — [Better Auth](https://better-auth.com) covers both OAuth (its MCP plugin makes your app the authorization server so third-party clients self-authorize per the MCP spec) and a simpler API-key / session-JWT check for your own callers. [references/mcp.md](https://neon.com/docs/ai/skills/neon-functions/references/mcp.md) has the full pattern — server with Postgres-backed tools via Drizzle, both Better Auth auth options, and testing with `mcporter` / `add-mcp`.
|
|
410
550
|
|
|
411
|
-
|
|
551
|
+
## Integrations and Observability
|
|
412
552
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
Functions are long-running but **still serverless** — they are a request/response runtime, not a background job runner. The hard limits:
|
|
416
|
-
|
|
417
|
-
- **Time to first byte: 15 minutes.** Your handler must _begin_ returning a response within 15 minutes of receiving a request. Most handlers finish in seconds; the 15-minute ceiling exists so agent workloads like image/video generation have room.
|
|
418
|
-
- **Heartbeat: 15 minutes.** Open WebSocket/SSE connections stay alive as long as data flows. The timeout only fires when a connection goes silent — send at least one byte every 15 minutes to keep a quiet stream alive.
|
|
419
|
-
- **`waitUntil`: 15 minutes.** Work registered with `waitUntil` keeps the invocation alive after the response is sent, up to 15 minutes — for cleanup like analytics writes and audit logs, **not** a background job runner. (`waitUntil` from `@neon/functions` is currently a stub during the preview.)
|
|
420
|
-
- **Idle eviction.** With no active connections the platform shuts the function down; it may also evict/restart for operational reasons — e.g. maintenance, or moving the function to a different compute node (active functions can run for hours first). Treat eviction like a process restart — WebSocket/SSE clients must reconnect. The platform sends `SIGINT` before evicting, so a `process.on("SIGINT", ...)` handler lets you detect that the function is about to be evicted and run any last-minute cleanup. You don't need one just to close Postgres connections — Neon's pooler reclaims those on its own.
|
|
553
|
+
### Built-in branch logs
|
|
421
554
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
## Functions as an agent backend (Next.js and similar frameworks)
|
|
425
|
-
|
|
426
|
-
A Neon Function is a great home for an AI agent precisely because it **doesn't time out** the way lambda-style serverless does (15-minute budget, see above). But that advantage disappears the moment you **proxy the agent stream through your web app's backend** — a Next.js route handler, Remix/SvelteKit/Nuxt action, etc. hosted on Vercel, Netlify, Cloudflare, and the like. Those platforms cap serverless/edge execution at short windows (often ~10–60s, sometimes up to ~300s), so a long agent or image/video generation stream gets cut off mid-response even though the Neon Function would happily keep going.
|
|
427
|
-
|
|
428
|
-
**Building the agent itself.** The [Vercel AI SDK](https://ai-sdk.dev) and [Mastra](https://mastra.ai) are the recommended ways to build the agent — point either at the Neon AI Gateway (see the `neon-ai-gateway` skill) for one credential across every model, with no extra provider keys. For a complete AI SDK agent running as a Function (streaming `toUIMessageStreamResponse`, multi-step tool calling next to Postgres, and persisting generated images to Object Storage), see [references/ai-sdk.md](references/ai-sdk.md); for the Mastra equivalent with built-in tracing, see [references/mastra-studio.md](references/mastra-studio.md).
|
|
429
|
-
|
|
430
|
-
**The fix: call the function directly from the client.** Don't route the long request through your app server.
|
|
431
|
-
|
|
432
|
-
```
|
|
433
|
-
Browser ──(Authorization: Bearer <JWT>)──▶ Neon Function (agent) ✅ no host timeout
|
|
434
|
-
Browser ──▶ your app backend ──▶ Neon Function ❌ host cuts the stream
|
|
555
|
+
```bash
|
|
556
|
+
neon logs query --branch production --source function --since 1h
|
|
435
557
|
```
|
|
436
558
|
|
|
437
|
-
|
|
438
|
-
- Hand the token to the client and have it call the Neon Function **directly** (cross-origin), e.g. with the Vercel AI SDK: `new DefaultChatTransport({ api: NEON_FUNCTION_URL, fetch })` where `fetch` attaches `Authorization: Bearer <token>`. Your app server is never in the path of the long stream.
|
|
439
|
-
- Add **CORS** so the browser can reach it (handle `OPTIONS`, set `Access-Control-Allow-Origin`/`-Headers`).
|
|
440
|
-
|
|
441
|
-
> [!WARNING]
|
|
442
|
-
> A Neon Function has a **public HTTPS URL — it is reachable by anyone.** A direct client→function call means there is no app backend in front of it to gate access, so **you must authenticate the function yourself.** Verify a JWT (e.g. against your app's JWKS), check a shared secret / API key, or validate a session token at the top of the handler and reject anything else. Never deploy an unauthenticated agent.
|
|
443
|
-
|
|
444
|
-
```typescript
|
|
445
|
-
// src/index.ts — verify the caller before doing any work
|
|
446
|
-
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
447
|
-
|
|
448
|
-
const jwks = createRemoteJWKSet(new URL(`${process.env.AUTH_BASE_URL}/api/auth/jwks`));
|
|
449
|
-
|
|
450
|
-
export default {
|
|
451
|
-
async fetch(request: Request) {
|
|
452
|
-
if (request.method === "OPTIONS") return new Response(null, { status: 204, headers: cors(request) });
|
|
453
|
-
|
|
454
|
-
const auth = request.headers.get("authorization");
|
|
455
|
-
if (!auth?.toLowerCase().startsWith("bearer ")) {
|
|
456
|
-
return new Response("Unauthorized", { status: 401, headers: cors(request) });
|
|
457
|
-
}
|
|
458
|
-
try {
|
|
459
|
-
const { payload } = await jwtVerify(auth.slice(7), jwks, {
|
|
460
|
-
issuer: process.env.AUTH_BASE_URL,
|
|
461
|
-
audience: process.env.AUTH_BASE_URL,
|
|
462
|
-
});
|
|
463
|
-
const userId = payload.sub; // scope the agent to this user
|
|
464
|
-
// ... run the agent, return result.toUIMessageStreamResponse({ headers: cors(request) })
|
|
465
|
-
} catch {
|
|
466
|
-
return new Response("Unauthorized", { status: 401, headers: cors(request) });
|
|
467
|
-
}
|
|
468
|
-
},
|
|
469
|
-
};
|
|
470
|
-
```
|
|
559
|
+
Functions is one of the two sources branch logs cover today, alongside Object Storage. Logs are scoped to a single branch, so pass `--branch` when the deployed function isn't on the branch you're checked out on. Everything else about logs — the required CLI version, filters, the SDK, and the Loki-compatible read API — is in the parent `neon` skill's **Observability** section.
|
|
471
560
|
|
|
472
|
-
|
|
561
|
+
### Application instrumentation
|
|
473
562
|
|
|
474
|
-
|
|
563
|
+
A function is a long-lived Node.js process running a web-standard request/response handler, so standard Node integration SDKs work unchanged. Initialize them once at module load, gated on an env var so local dev and unconfigured branches stay a no-op, and pass secrets via `--env` or `neon.ts` `env`.
|
|
475
564
|
|
|
476
|
-
|
|
565
|
+
- **Sentry** — error monitoring across the HTTP framework, the function runtime, and an agent's own caught/fallback failures (the long-running case Functions target): see [references/sentry.md](https://neon.com/docs/ai/skills/neon-functions/references/sentry.md).
|
|
566
|
+
- **Mastra Studio (Mastra Cloud)** — run a Mastra agent on a function and ship its traces to a Studio project for observability: see [references/mastra-studio.md](https://neon.com/docs/ai/skills/neon-functions/references/mastra-studio.md).
|
|
477
567
|
|
|
478
568
|
## Neon Documentation
|
|
479
569
|
|
|
480
570
|
The Neon documentation is the source of truth and Functions is evolving rapidly, so always verify against the official docs. Any doc page can be fetched as markdown by appending `.md` to the URL or by requesting `Accept: text/markdown`. Find the right page from the docs index (https://neon.com/docs/llms.txt) and the changelog announcements.
|
|
481
571
|
|
|
482
|
-
## Further
|
|
572
|
+
## Further Reading
|
|
483
573
|
|
|
484
574
|
- https://neon.com/docs/compute/functions/overview.md
|
|
485
575
|
- https://neon.com/docs/compute/functions/get-started.md
|