@netlify/agent-runner-cli 2.17.0-PAW-612.0 → 2.18.0-smtp.0

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.
@@ -0,0 +1,104 @@
1
+ # Advanced Identity Patterns
2
+
3
+ ## Password Recovery
4
+
5
+ Three-step flow: request recovery email, handle the callback, then set a new password.
6
+
7
+ ```typescript
8
+ import { requestPasswordRecovery, handleAuthCallback, updateUser, AuthError } from '@netlify/identity'
9
+
10
+ // Step 1: Send recovery email
11
+ async function handleForgotPassword(email: string) {
12
+ try {
13
+ await requestPasswordRecovery(email)
14
+ showSuccess('Check your email for a password reset link.')
15
+ } catch (error) {
16
+ if (error instanceof AuthError) showError(error.message)
17
+ }
18
+ }
19
+
20
+ // Step 2: handleAuthCallback() returns { type: 'recovery', user } — show password reset form
21
+ // (See the handleAuthCallback switch in SKILL.md)
22
+
23
+ // Step 3: Set new password
24
+ async function handlePasswordReset(newPassword: string) {
25
+ try {
26
+ await updateUser({ password: newPassword })
27
+ showSuccess('Password updated.')
28
+ } catch (error) {
29
+ if (error instanceof AuthError) showError(error.message)
30
+ }
31
+ }
32
+ ```
33
+
34
+ The recovery callback fires a `'recovery'` auth event, not `'login'`. The user is authenticated but should be prompted to set a new password before navigating away.
35
+
36
+ ## Invite Acceptance
37
+
38
+ When a user clicks an invite link, `handleAuthCallback()` returns `{ type: 'invite', user: null, token }`. Use the token to accept the invite and set a password.
39
+
40
+ ```typescript
41
+ import { acceptInvite, AuthError } from '@netlify/identity'
42
+
43
+ async function handleAcceptInvite(token: string, password: string) {
44
+ try {
45
+ const user = await acceptInvite(token, password)
46
+ showSuccess(`Welcome, ${user.email}! Your account is ready.`)
47
+ } catch (error) {
48
+ if (error instanceof AuthError) showError(error.message)
49
+ }
50
+ }
51
+ ```
52
+
53
+ ## Email Change
54
+
55
+ When a user verifies an email change, `handleAuthCallback()` returns `{ type: 'email_change', user }` with the change already applied — the returned `user` carries the new email. The user must be logged in when clicking the verification link.
56
+
57
+ Like recovery (and unlike invite acceptance), there is no token to consume and no follow-up call — `handleAuthCallback()` does the work. Handle it in the `handleAuthCallback()` switch (see SKILL.md) and confirm the new address.
58
+
59
+ ```typescript
60
+ // In the handleAuthCallback() switch (see SKILL.md):
61
+ case 'email_change':
62
+ showSuccess(`Email updated to ${result.user?.email}`)
63
+ break
64
+ ```
65
+
66
+ If you process the verification token yourself instead of letting `handleAuthCallback()` read the URL hash, call `verifyEmailChange(token)` directly — it applies the change and returns the updated user.
67
+
68
+ ## Session Hydration
69
+
70
+ `hydrateSession()` bridges server-set cookies to the browser session. Call it on page load when using server-side login (e.g., login inside a Netlify Function followed by a redirect).
71
+
72
+ ```typescript
73
+ import { hydrateSession } from '@netlify/identity'
74
+
75
+ const user = await hydrateSession()
76
+ if (user) {
77
+ // Browser session is now in sync with server-set cookies
78
+ }
79
+ ```
80
+
81
+ `getUser()` auto-hydrates from the `nf_jwt` cookie if no browser session exists, so explicit `hydrateSession()` is only needed when you want to restore the full session (including token refresh timers) after a server-side login.
82
+
83
+ ## SSR Integration Patterns
84
+
85
+ For SSR frameworks, the recommended pattern is:
86
+
87
+ - **Browser-side** for auth mutations: `login()`, `signup()`, `logout()`, `oauthLogin()`
88
+ - **Server-side** for reading auth state: `getUser()`, `getSettings()`, `getIdentityConfig()`
89
+
90
+ Browser-side auth mutations set the `nf_jwt` cookie and localStorage, and emit `onAuthChange` events. The server reads the cookie on the next request.
91
+
92
+ The library also supports server-side mutations (`login()`, `signup()`, `logout()` inside Netlify Functions), but these require the Netlify Functions runtime to set cookies. After a server-side mutation, use a full page navigation so the browser sends the new cookie.
93
+
94
+ Always use `window.location.href` (not framework router navigation) after server-side auth mutations in Next.js, TanStack Start, and SvelteKit. Remix `redirect()` is safe because Remix actions return real HTTP responses.
95
+
96
+ ## Full API Reference
97
+
98
+ For the complete API reference — all function signatures, type definitions, OAuth helpers, admin operations, session management, auth events, and framework-specific examples — read the package README:
99
+
100
+ ```
101
+ node_modules/@netlify/identity/README.md
102
+ ```
103
+
104
+ The README is shipped with the npm package and is always in sync with the installed version.
@@ -0,0 +1,24 @@
1
+ # Netlify Identity — authorization and session gotchas
2
+
3
+ Where role-based access actually gets enforced, and why a role change doesn't take effect immediately.
4
+
5
+ ## Admin operations run only in the Functions runtime
6
+
7
+ Identity's admin API — creating users, updating a user's roles or metadata, deleting users (the `admin.*` operations) — requires a privileged admin token that is available **only in the Netlify Functions runtime**. It is not exposed to browser code and is not available in Edge Functions. Do all role assignment and user administration from inside a modern v2 Function (or an Identity event function), never from the client and never from an edge function. A "promote this user to admin" button in the UI must call a Function endpoint that performs the change server-side — it cannot call the admin API directly from the browser.
8
+
9
+ You don't configure or read the admin token yourself — the Netlify Functions runtime provides it automatically to `admin.*` calls made inside a Function, so there is no env var to set and no `Netlify.env.get(...)` to call for it. Run the `admin.*` operations server-side in a Function; never hardcode an admin token, ship one in the client bundle, or expose the admin operations to the browser. Exposing an admin capability client-side would let any visitor grant themselves the `admin` role.
10
+
11
+ ## Redirect gating only covers CDN document requests
12
+
13
+ `conditions = { Role = [...] }` redirects are enforced by the CDN **only when it serves a document (navigation) request** — a fresh HTTP request for the path. They are a coarse page-level perimeter, not real authorization:
14
+
15
+ - **SPA client-side navigation bypasses them.** When a client-side router (React, Vue, SvelteKit, etc.) navigates to `/admin` in the browser, no new document request reaches the CDN, so the redirect rule never runs and the route renders regardless of the user's role.
16
+ - **Anything in the client bundle is downloadable by anyone.** Role-gated content compiled into the JavaScript bundle ships to every visitor who can load the page; hiding a component behind a client-side role check does not protect the data inside it.
17
+
18
+ So use redirect gating for coarse routing only. Enforce anything sensitive **server-side on every request** — a Netlify Function (or the API it calls) that resolves the user with `getUser()` and checks the server-controlled `app_metadata.roles` — never a client-side route guard or a hidden UI element as the only gate.
19
+
20
+ ## Role changes don't affect live sessions until the JWT refreshes
21
+
22
+ Roles are baked into the `nf_jwt` when the token is issued, and that JWT stays valid until it expires (about an hour). Changing a user's roles — via the dashboard, the admin API, or an Identity event function — does **not** update tokens already held by signed-in users. A user you just promoted keeps seeing the old view, and a user whose role you just revoked keeps their access, until their token refreshes (`AUTH_EVENTS.TOKEN_REFRESH`) or they log out and log back in. Both redirect `Role` conditions and function-side `app_metadata.roles` checks read the current token, so both see the stale roles until then.
23
+
24
+ Don't expect a role change to take effect mid-session. When it needs to apply right away, direct the user to log out and back in (or otherwise refresh their token) so a new `nf_jwt` carrying the updated roles is issued.
@@ -0,0 +1,225 @@
1
+ ---
2
+ name: netlify-server
3
+ description: Run a long-running HTTP server (Express, Hono, Fastify, or plain node:http) on Netlify. The default for server-side code — API endpoints, backend logic, WebSockets, server-side AI calls. Use Netlify Functions only for scheduled jobs, long background jobs, and platform event handlers.
4
+ ---
5
+
6
+ # Netlify Server
7
+
8
+ A Netlify Server is one long-running Node.js process deployed alongside your site. Unlike Netlify Functions, which run per request, the server process is started once, reused across requests, scaled to zero when idle, and started again on the next request. It runs standard Node.js servers with no Netlify-specific APIs: if it listens on a port, it works.
9
+
10
+ ## When to use this skill
11
+
12
+ Use a Netlify Server for server-side code by default (see the `general-backend` skill):
13
+
14
+ - Start a new project that needs a Netlify Server without a template. Every template is a full-stack framework whose catch-all function takes the requests before the server, so the server would never receive traffic.
15
+ - Implement all API routes and request-driven backend code inside the server app.
16
+ - Create files in `netlify/functions/` alongside it ONLY for scheduled functions, background functions for jobs that can run longer than about a minute, and platform event handlers (Forms submissions, Identity events, deploy events). See the `netlify-functions` skill.
17
+ - NEVER migrate an existing site's functions to a Netlify Server unless explicitly asked to.
18
+
19
+ ## Quick start
20
+
21
+ Create exactly one entrypoint at `netlify/server/index.mjs`.
22
+
23
+ Express:
24
+
25
+ ```javascript
26
+ // netlify/server/index.mjs
27
+ import express from 'express'
28
+
29
+ const app = express()
30
+
31
+ app.get('/api/hello', (req, res) => {
32
+ res.json({ hello: 'world' })
33
+ })
34
+
35
+ app.listen(process.env.PORT)
36
+ ```
37
+
38
+ Hono (no `listen` call; export the app as the default export):
39
+
40
+ ```javascript
41
+ // netlify/server/index.mjs
42
+ import { Hono } from 'hono'
43
+
44
+ const app = new Hono()
45
+
46
+ app.get('/api/hello', (c) => c.json({ hello: 'world' }))
47
+
48
+ export default app
49
+ ```
50
+
51
+ What the entry file exports decides how Netlify runs it:
52
+
53
+ - **An `http.Server`**, such as `http.createServer(app)`: requests go to that server. WebSocket libraries attached to it (`ws`, Socket.IO) work.
54
+ - **An object with a `fetch` method**, such as a Hono app: called for each request.
55
+ - **An Express app**, or a function with 3 or more parameters: hosted in an HTTP server Netlify creates. WebSocket upgrades get a `501`, so export `http.createServer(app)` instead when you need them.
56
+ - **Any other function**: called as a handler `(request, context) => Response`, the same signature as a Netlify Function. A plain Node.js `(req, res)` listener lands here too and breaks; wrap it in `http.createServer()`.
57
+ - **Anything else, or no default export**: Netlify waits for your code to start a server with `listen` on `process.env.PORT`, the way the framework's documentation shows. It must be listening within 10 seconds.
58
+
59
+ A handler or `fetch` app accepts WebSockets with `upgradeWebSocket(request)` from `@netlify/server`, which returns `{ socket, response }`; return `response` from the handler.
60
+
61
+ Declare dependencies in `package.json` as usual. Deploy is the normal site deploy; nothing extra to configure. The build log confirms detection with the line `Netlify Server detected at netlify/server/index.mjs`.
62
+
63
+ ## Rules
64
+
65
+ - **One entrypoint only.** `netlify/server/` must contain exactly one of `index.mjs`, `index.js`, `index.ts`, or `index.mts`. Two entry files fail the build. Prefer `index.mjs` with ESM syntax; split the rest of the server into modules that the entrypoint imports.
66
+ - **A server that calls `listen` uses `process.env.PORT`.** It is set before your code is imported. Do not hardcode a port.
67
+ - **Node.js 24 or later.** The server runs on the build's Node.js version, and the deploy fails on anything earlier.
68
+ - **Do not daemonize, fork, or spawn long-lived child processes.** One process serves the traffic.
69
+ - Environment variables work as usual via `process.env`.
70
+ - When adding npm modules, ensure `node_modules` is in `.gitignore`.
71
+
72
+ ## Routing and static files
73
+
74
+ By default, the server receives every request that nothing else in the project handles. Requests are handled in this order:
75
+
76
+ 1. Edge Functions.
77
+ 2. Redirect and rewrite rules.
78
+ 3. Static files in the publish directory.
79
+ 4. Netlify Functions, on the paths they're configured for.
80
+ 5. The server, for everything else (or only the paths it's configured for, see below).
81
+
82
+ Rules:
83
+
84
+ - Files in the publish directory win over the server for matching paths, even on paths the server claims. Use the publish directory for assets like images, fonts, or a favicon.
85
+ - Do NOT put an `index.html` in the publish directory if the server should render `/`. A static `index.html` shadows the server's root route.
86
+ - A rewrite rule that matches every path, such as a single-page app's `/* /index.html 200`, sends requests to its destination instead of the server. Limit such rules to the paths the frontend handles.
87
+ - Full-stack frameworks (Next.js, Nuxt, SvelteKit, Astro, TanStack Start) usually deploy a Netlify Function on `/*` that handles every request not served by a static file, covering both page rendering and the framework's API routes. Like the rewrite above, it takes those requests before the server does, so the server never receives any. If the project needs a Netlify Server (see the `general-backend` skill), do not use a full-stack framework. Otherwise, put server code in the framework's own server routes.
88
+ - There is no path prefix: the server sees its requests with their original paths.
89
+
90
+ To have the server handle only some paths, set `path` (defaults to `/*`) and `excludedPath`, using the same syntax as function paths. Export them as `config` from the entry file, or set them in a `[server]` section in `netlify.toml`:
91
+
92
+ ```javascript
93
+ export const config = {
94
+ path: '/api/*',
95
+ excludedPath: ['/api/legacy/*'],
96
+ }
97
+ ```
98
+
99
+ ## Lifecycle: scale to zero
100
+
101
+ This is the most important mental model for building correctly:
102
+
103
+ - After 5 minutes with no requests, the server is shut down. The next request starts a fresh process (a cold start).
104
+ - **All process memory is lost on every scale-to-zero, deploy, and platform maintenance event.** In-memory caches are fine as caches; they are NEVER storage. Anything that must survive belongs in Netlify Blobs or Netlify Database (see the `general-database` skill to choose).
105
+ - Netlify can run several instances of the server at once (up to 3; 1 on Free and Personal plans). Each has its own memory and its own WebSocket connections; nothing in memory is shared between them.
106
+ - In-flight requests and open WebSocket or Server-Sent Events connections are never cut off by the idle shutdown; while any are open, the instance stays running.
107
+ - The server must send a response's status and headers within 20 seconds, or the request fails with a `502`. Stream slow responses, such as AI output, or respond right away and finish with `waitUntil`.
108
+ - Background work that outlives the request that triggered it (for example, responding early and then finishing a task) MUST be registered with `getContext().waitUntil(promise)`. Registered work keeps the process alive until the promise settles; unregistered work may be reclaimed the moment no requests are active.
109
+ - Never keep the server alive on purpose: no self-pings, no timer that calls the server, no `waitUntil` chain that schedules more work to stay running, and no `scale.min` above `0` (during Early Access a deploy that sets it fails). Scale to zero is the design. If the user asks for an always-on server, explain this and build without it.
110
+ - Scheduled or recurring work (cron, nightly cleanup, periodic sync) goes in a scheduled function (see the `netlify-functions` skill) that calls a server route. Never use `setInterval` or other timers meant to outlive a request.
111
+ - `waitUntil` extends the idle lifetime, but deploys and platform maintenance still stop the process with only a short grace window. Keep background tasks short (seconds, not minutes), and persist anything critical before responding. For genuinely long jobs, call a background function from the server (see the `netlify-functions` skill), or persist a job record and process it in resumable increments across requests.
112
+
113
+ Graceful shutdown hook (optional): register it with `onShutdown` from `@netlify/server`. It runs as soon as the instance starts stopping and receives `drained`, a promise that resolves once in-flight requests and `waitUntil` work have finished. Stop background work and close WebSockets right away, then `await drained` before releasing what that work still uses. Apps that already listen for `SIGTERM` keep working.
114
+
115
+ ```javascript
116
+ import { onShutdown } from '@netlify/server'
117
+
118
+ onShutdown(async ({ drained }) => {
119
+ wss.close()
120
+ await drained
121
+ await pool.end()
122
+ })
123
+ ```
124
+
125
+ ## Request context: geolocation, cookies, site metadata
126
+
127
+ Inside request handling, get the Netlify request context with `getContext` from the `@netlify/server` package (add it to `package.json`):
128
+
129
+ ```javascript
130
+ import express from 'express'
131
+ import { getContext } from '@netlify/server'
132
+
133
+ const app = express()
134
+
135
+ app.get('/api/where', (req, res) => {
136
+ const context = getContext()
137
+ res.json({ city: context.geo?.city, country: context.geo?.country?.name })
138
+ })
139
+
140
+ app.listen(process.env.PORT)
141
+ ```
142
+
143
+ Rules:
144
+
145
+ - ONLY call `getContext()` while handling a request (inside a route handler, middleware, or code awaited from one). It throws outside a request.
146
+ - Works with every server form, including WebSocket upgrade handling.
147
+ - Anything started inside a request (a `setInterval`, a lazily started background loop, a client that creates its own timers, WebSocket message handlers) keeps that request's context for its whole life, so `getContext()` there returns a stale request instead of throwing. Start long-lived work outside request handling, and never read per-request context from it.
148
+
149
+ ### SSO user: `context.user`
150
+
151
+ On private projects and sites protected by Netlify team login, the context also carries the user who signed in through Netlify's SSO: `context.user` with `id`, `email`, and `expiresAt` (a `Date`). It is `undefined` when the request didn't come from such a user. Netlify verifies the visitor and sets it itself, so it can't be spoofed with request headers. This is the Netlify team member viewing the site, not a Netlify Identity user.
152
+
153
+ ```javascript
154
+ app.get('/api/whoami', (req, res) => {
155
+ const { user } = getContext()
156
+ if (!user) return res.status(401).send('Unauthorized')
157
+ res.json({ id: user.id, email: user.email })
158
+ })
159
+ ```
160
+
161
+ See the [changelog post](https://www.netlify.com/changelog/2026-10-07-context-user-in-functions/).
162
+
163
+ ## AI inference: Netlify AI Gateway
164
+
165
+ The AI Gateway works with zero configuration: provider credentials and base URLs (`OPENAI_API_KEY`, `OPENAI_BASE_URL`, and equivalents for other providers) are injected into `process.env` automatically. See the `netlify-ai-gateway` skill for supported models and usage.
166
+
167
+ AI Gateway, Netlify Blobs, and Netlify Database clients can be created once at module scope and reused: the platform keeps their credentials current. One exception: `NETLIFY_PURGE_API_TOKEN` expires after 15 minutes, so read it from `process.env` each time you call the purge API and never store it in a module-level variable.
168
+
169
+ ```javascript
170
+ import OpenAI from 'openai'
171
+
172
+ const client = new OpenAI()
173
+
174
+ app.post('/api/chat', async (req, res) => {
175
+ // ...
176
+ })
177
+ ```
178
+
179
+ ## WebSockets
180
+
181
+ WebSockets work for servers that handle HTTP upgrades. Create the HTTP server, attach the WebSocket server to it, and export it. Starting it with `server.listen(process.env.PORT)` instead of exporting it works too.
182
+
183
+ ```javascript
184
+ // netlify/server/index.mjs
185
+ import { createServer } from 'node:http'
186
+ import express from 'express'
187
+ import { WebSocketServer } from 'ws'
188
+
189
+ const app = express()
190
+ const server = createServer(app)
191
+ const wss = new WebSocketServer({ server, path: '/ws' })
192
+
193
+ wss.on('connection', (ws) => {
194
+ ws.send('connected')
195
+ })
196
+
197
+ export default server
198
+ ```
199
+
200
+ Rules for WebSocket clients you generate:
201
+
202
+ - ALWAYS implement automatic reconnection with backoff in the browser client. The platform may terminate long-lived connections periodically; a reconnecting client makes this invisible to users.
203
+ - Do not rely on a socket staying open indefinitely, and do not keep client state only on the server side of a socket: rebuild the needed state on reconnect.
204
+ - Clients can be connected to different instances, and each instance only sees its own sockets. Keep state every client must see, such as chat history, in Netlify Database or Blobs.
205
+
206
+ ## Fetching user-supplied URLs
207
+
208
+ An endpoint that fetches a URL taken from the request (link previews, webhook testers, image proxies) runs inside Netlify's network, so it must:
209
+
210
+ - Accept only `http:` and `https:` URLs.
211
+ - Resolve the hostname and refuse loopback, private, link-local, and unique-local addresses, including `169.254.169.254`.
212
+ - Follow redirects manually (`redirect: 'manual'`) and check every hop the same way.
213
+ - Return only what the feature needs (extracted fields, a capped body), never forward the caller's method, headers, or body. A generic `/proxy?url=` endpoint is an open relay; don't build one.
214
+
215
+ ## Common mistakes
216
+
217
+ - Treating process memory as a database. It is wiped on every cold start. Use Blobs or Database.
218
+ - Hardcoding a port or defaulting to `3000` without reading `process.env.PORT` first.
219
+ - Adding an `index.html` to the publish directory that shadows the server's `/` route.
220
+ - Creating `netlify/functions/` endpoints alongside the server. Put API routes in the server app; use functions only for scheduled jobs, long background jobs, and event handlers.
221
+ - Two entry files in `netlify/server/` (for example both `index.mjs` and `index.ts`). Keep one.
222
+ - Fire-and-forget background jobs without `getContext().waitUntil(promise)`. Unregistered work may be reclaimed mid-flight; registered work should still be short and non-critical.
223
+ - Calling `getContext()` at module scope. It only works while handling a request.
224
+ - Storing `NETLIFY_PURGE_API_TOKEN` in a module-level variable. Read it when you call the purge API.
225
+ - Keeping the server warm with self-pings or timers, or running scheduled work with `setInterval`. Use a scheduled function that calls a server route.
@@ -9,17 +9,20 @@ A Netlify Server is one long-running Node.js process deployed alongside your sit
9
9
 
10
10
  ## When to use this skill: explicit request only
11
11
 
12
- Netlify Server is an experimental primitive. The rule for choosing it is strict:
12
+ Netlify Server is in Early Access. The rule for choosing it is strict:
13
13
 
14
14
  - ONLY build with a Netlify Server when the prompt **explicitly requests a "Netlify Server"** by name (for example "build this as a Netlify Server" or "use the Netlify Server primitive").
15
15
  - If the prompt does not name it, discard this skill entirely, even when the request mentions Express, Hono, WebSockets, servers in general, or would otherwise seem like a good fit. In those cases use **Netlify Functions** (see the `netlify-functions` skill) and the other platform primitives as usual.
16
16
  - NEVER migrate an existing site's functions to a Netlify Server unless explicitly asked to.
17
17
 
18
- Once a Netlify Server has been explicitly requested: implement all API routes inside the server app, and do NOT create files in `netlify/functions/` alongside it.
18
+ Once a Netlify Server has been explicitly requested:
19
+
20
+ - Start without a template. Every template is a full-stack framework whose catch-all function takes the requests before the server, so the server would never receive traffic.
21
+ - Implement all API routes inside the server app. Create files in `netlify/functions/` only for scheduled jobs (see below), never for endpoints.
19
22
 
20
23
  ## Quick start
21
24
 
22
- Create exactly one entrypoint at `netlify/server/index.mjs`. Any Node.js server works. It MUST listen on `process.env.PORT`.
25
+ Create exactly one entrypoint at `netlify/server/index.mjs`.
23
26
 
24
27
  Express:
25
28
 
@@ -49,49 +52,87 @@ app.get('/api/hello', (c) => c.json({ hello: 'world' }))
49
52
  export default app
50
53
  ```
51
54
 
55
+ What the entry file exports decides how Netlify runs it:
56
+
57
+ - **An `http.Server`**, such as `http.createServer(app)`: requests go to that server. WebSocket libraries attached to it (`ws`, Socket.IO) work.
58
+ - **An object with a `fetch` method**, such as a Hono app: called for each request.
59
+ - **An Express app**, or a function with 3 or more parameters: hosted in an HTTP server Netlify creates. WebSocket upgrades get a `501`, so export `http.createServer(app)` instead when you need them.
60
+ - **Any other function**: called as a handler `(request, context) => Response`, the same signature as a Netlify Function. A plain Node.js `(req, res)` listener lands here too and breaks; wrap it in `http.createServer()`.
61
+ - **Anything else, or no default export**: Netlify waits for your code to start a server with `listen` on `process.env.PORT`, the way the framework's documentation shows. It must be listening within 10 seconds.
62
+
63
+ A handler or `fetch` app accepts WebSockets with `upgradeWebSocket(request)` from `@netlify/server`, which returns `{ socket, response }`; return `response` from the handler.
64
+
52
65
  Declare dependencies in `package.json` as usual. Deploy is the normal site deploy; nothing extra to configure. The build log confirms detection with the line `Netlify Server detected at netlify/server/index.mjs`.
53
66
 
54
67
  ## Rules
55
68
 
56
69
  - **One entrypoint only.** `netlify/server/` must contain exactly one of `index.mjs`, `index.js`, `index.ts`, or `index.mts`. Two entry files fail the build. Prefer `index.mjs` with ESM syntax; split the rest of the server into modules that the entrypoint imports.
57
- - **Listen on `process.env.PORT`.** It is set before your code is imported. Do not hardcode a port.
70
+ - **A server that calls `listen` uses `process.env.PORT`.** It is set before your code is imported. Do not hardcode a port.
71
+ - **Node.js 24 or later.** The server runs on the build's Node.js version, and the deploy fails on anything earlier.
58
72
  - **Do not daemonize, fork, or spawn long-lived child processes.** One process serves the traffic.
59
73
  - Environment variables work as usual via `process.env`.
60
74
  - When adding npm modules, ensure `node_modules` is in `.gitignore`.
61
75
 
62
76
  ## Routing and static files
63
77
 
64
- The server receives every request that is not served as a static file:
78
+ By default, the server receives every request that nothing else in the project handles. Requests are handled in this order:
79
+
80
+ 1. Edge Functions.
81
+ 2. Redirect and rewrite rules.
82
+ 3. Static files in the publish directory.
83
+ 4. Netlify Functions, on the paths they're configured for.
84
+ 5. The server, for everything else (or only the paths it's configured for, see below).
65
85
 
66
- - Files in the publish directory win over the server for matching paths (`preferStatic`). Use the publish directory for assets like images, fonts, or a favicon.
86
+ Rules:
87
+
88
+ - Files in the publish directory win over the server for matching paths, even on paths the server claims. Use the publish directory for assets like images, fonts, or a favicon.
67
89
  - Do NOT put an `index.html` in the publish directory if the server should render `/`. A static `index.html` shadows the server's root route.
68
- - There is no path prefix: the server sees `/`, `/api/anything`, and everything else, with the original paths.
90
+ - A rewrite rule that matches every path, such as a single-page app's `/* /index.html 200`, sends requests to its destination instead of the server. Limit such rules to the paths the frontend handles.
91
+ - Full-stack frameworks (Next.js, Nuxt, SvelteKit, Astro, TanStack Start) usually deploy a Netlify Function on `/*` that handles every request not served by a static file, covering both page rendering and the framework's API routes. Like the rewrite above, it takes those requests before the server does, so the server never receives any. If a Netlify Server is explicitly required, do not use a full-stack framework. Otherwise, put server code in the framework's own server routes.
92
+ - There is no path prefix: the server sees its requests with their original paths.
93
+
94
+ To have the server handle only some paths, set `path` (defaults to `/*`) and `excludedPath`, using the same syntax as function paths. Export them as `config` from the entry file, or set them in a `[server]` section in `netlify.toml`:
95
+
96
+ ```javascript
97
+ export const config = {
98
+ path: '/api/*',
99
+ excludedPath: ['/api/legacy/*'],
100
+ }
101
+ ```
69
102
 
70
103
  ## Lifecycle: scale to zero
71
104
 
72
105
  This is the most important mental model for building correctly:
73
106
 
74
- - After a period with no traffic, the server is shut down. The next request starts a fresh process (a cold start).
107
+ - After 5 minutes with no requests, the server is shut down. The next request starts a fresh process (a cold start).
75
108
  - **All process memory is lost on every scale-to-zero, deploy, and platform maintenance event.** In-memory caches are fine as caches; they are NEVER storage. Anything that must survive belongs in Netlify Blobs or Netlify Database (see the `general-database` skill to choose).
76
- - In-flight requests are never cut off by the idle shutdown.
109
+ - Netlify can run several instances of the server at once (up to 3; 1 on Free and Personal plans). Each has its own memory and its own WebSocket connections; nothing in memory is shared between them.
110
+ - In-flight requests and open WebSocket or Server-Sent Events connections are never cut off by the idle shutdown; while any are open, the instance stays running.
111
+ - The server must send a response's status and headers within 20 seconds, or the request fails with a `502`. Stream slow responses, such as AI output, or respond right away and finish with `waitUntil`.
77
112
  - Background work that outlives the request that triggered it (for example, responding early and then finishing a task) MUST be registered with `getContext().waitUntil(promise)`. Registered work keeps the process alive until the promise settles; unregistered work may be reclaimed the moment no requests are active.
113
+ - Never keep the server alive on purpose: no self-pings, no timer that calls the server, no `waitUntil` chain that schedules more work to stay running, and no `scale.min` above `0` (during Early Access a deploy that sets it fails). Scale to zero is the design. If the user asks for an always-on server, explain this and build without it.
114
+ - Scheduled or recurring work (cron, nightly cleanup, periodic sync) goes in a scheduled function (see the `netlify-functions` skill) that calls a server route. Never use `setInterval` or other timers meant to outlive a request.
78
115
  - `waitUntil` extends the idle lifetime, but deploys and platform maintenance still stop the process with only a short grace window. Keep background tasks short (seconds, not minutes), and persist anything critical before responding. For genuinely long jobs, persist a job record and process it in resumable increments across requests.
79
116
 
80
- Graceful shutdown hook (optional): export a `shutdown` function from the entrypoint; it is called before the process is stopped. Keep it fast (a few seconds), for example flushing a buffer to Blobs. For Express-style apps that call `listen`, you can also listen for `SIGTERM` directly.
117
+ Graceful shutdown hook (optional): register it with `onShutdown` from `@netlify/server`. It runs as soon as the instance starts stopping and receives `drained`, a promise that resolves once in-flight requests and `waitUntil` work have finished. Stop background work and close WebSockets right away, then `await drained` before releasing what that work still uses. Apps that already listen for `SIGTERM` keep working.
81
118
 
82
119
  ```javascript
83
- export const shutdown = async () => {
84
- await flushPendingWrites()
85
- }
120
+ import { onShutdown } from '@netlify/server'
121
+
122
+ onShutdown(async ({ drained }) => {
123
+ wss.close()
124
+ await drained
125
+ await pool.end()
126
+ })
86
127
  ```
87
128
 
88
129
  ## Request context: geolocation, cookies, site metadata
89
130
 
90
- Inside request handling, get the Netlify request context with `getContext` from the `@netlify/functions` package (add it to `package.json`):
131
+ Inside request handling, get the Netlify request context with `getContext` from the `@netlify/server` package (add it to `package.json`):
91
132
 
92
133
  ```javascript
93
134
  import express from 'express'
94
- import { getContext } from '@netlify/functions'
135
+ import { getContext } from '@netlify/server'
95
136
 
96
137
  const app = express()
97
138
 
@@ -107,6 +148,7 @@ Rules:
107
148
 
108
149
  - ONLY call `getContext()` while handling a request (inside a route handler, middleware, or code awaited from one). It throws outside a request.
109
150
  - Works with every server form, including WebSocket upgrade handling.
151
+ - Anything started inside a request (a `setInterval`, a lazily started background loop, a client that creates its own timers, WebSocket message handlers) keeps that request's context for its whole life, so `getContext()` there returns a stale request instead of throwing. Start long-lived work outside request handling, and never read per-request context from it.
110
152
 
111
153
  ### SSO user: `context.user`
112
154
 
@@ -126,52 +168,62 @@ See the [changelog post](https://www.netlify.com/changelog/2026-10-07-context-us
126
168
 
127
169
  The AI Gateway works with zero configuration: provider credentials and base URLs (`OPENAI_API_KEY`, `OPENAI_BASE_URL`, and equivalents for other providers) are injected into `process.env` automatically. See the `netlify-ai-gateway` skill for supported models and usage.
128
170
 
129
- One rule specific to servers: **construct AI SDK clients inside request handling, never at module scope.** Credentials are injected per request, so they do not exist when the module is first imported.
171
+ AI Gateway, Netlify Blobs, and Netlify Database clients can be created once at module scope and reused: the platform keeps their credentials current. One exception: `NETLIFY_PURGE_API_TOKEN` expires after 15 minutes, so read it from `process.env` each time you call the purge API and never store it in a module-level variable.
130
172
 
131
173
  ```javascript
132
174
  import OpenAI from 'openai'
133
175
 
176
+ const client = new OpenAI()
177
+
134
178
  app.post('/api/chat', async (req, res) => {
135
- const client = new OpenAI() // created per request: credentials are available here
136
179
  // ...
137
180
  })
138
181
  ```
139
182
 
140
183
  ## WebSockets
141
184
 
142
- WebSockets work for servers that handle HTTP upgrades (the `listen` form). Attach the WebSocket server to the same HTTP server:
185
+ WebSockets work for servers that handle HTTP upgrades. Create the HTTP server, attach the WebSocket server to it, and export it. Starting it with `server.listen(process.env.PORT)` instead of exporting it works too.
143
186
 
144
187
  ```javascript
145
188
  // netlify/server/index.mjs
189
+ import { createServer } from 'node:http'
146
190
  import express from 'express'
147
191
  import { WebSocketServer } from 'ws'
148
192
 
149
193
  const app = express()
150
- const server = app.listen(process.env.PORT)
151
- const wss = new WebSocketServer({ noServer: true })
152
-
153
- server.on('upgrade', (req, socket, head) => {
154
- if (req.url !== '/ws') {
155
- socket.destroy()
156
- return
157
- }
158
- wss.handleUpgrade(req, socket, head, (ws) => {
159
- ws.send('connected')
160
- })
194
+ const server = createServer(app)
195
+ const wss = new WebSocketServer({ server, path: '/ws' })
196
+
197
+ wss.on('connection', (ws) => {
198
+ ws.send('connected')
161
199
  })
200
+
201
+ export default server
162
202
  ```
163
203
 
164
204
  Rules for WebSocket clients you generate:
165
205
 
166
206
  - ALWAYS implement automatic reconnection with backoff in the browser client. The platform may terminate long-lived connections periodically; a reconnecting client makes this invisible to users.
167
207
  - Do not rely on a socket staying open indefinitely, and do not keep client state only on the server side of a socket: rebuild the needed state on reconnect.
208
+ - Clients can be connected to different instances, and each instance only sees its own sockets. Keep state every client must see, such as chat history, in Netlify Database or Blobs.
209
+
210
+ ## Fetching user-supplied URLs
211
+
212
+ An endpoint that fetches a URL taken from the request (link previews, webhook testers, image proxies) runs inside Netlify's network, so it must:
213
+
214
+ - Accept only `http:` and `https:` URLs.
215
+ - Resolve the hostname and refuse loopback, private, link-local, and unique-local addresses, including `169.254.169.254`.
216
+ - Follow redirects manually (`redirect: 'manual'`) and check every hop the same way.
217
+ - Return only what the feature needs (extracted fields, a capped body), never forward the caller's method, headers, or body. A generic `/proxy?url=` endpoint is an open relay; don't build one.
168
218
 
169
219
  ## Common mistakes
170
220
 
171
221
  - Treating process memory as a database. It is wiped on every cold start. Use Blobs or Database.
172
222
  - Hardcoding a port or defaulting to `3000` without reading `process.env.PORT` first.
173
223
  - Adding an `index.html` to the publish directory that shadows the server's `/` route.
174
- - Creating `netlify/functions/` endpoints alongside the server. Put API routes in the server app.
224
+ - Creating `netlify/functions/` endpoints alongside the server. Put API routes in the server app; use functions only for scheduled jobs.
175
225
  - Two entry files in `netlify/server/` (for example both `index.mjs` and `index.ts`). Keep one.
176
226
  - Fire-and-forget background jobs without `getContext().waitUntil(promise)`. Unregistered work may be reclaimed mid-flight; registered work should still be short and non-critical.
177
- - Constructing AI SDK clients (or calling `getContext()`) at module scope. Per-request state does not exist at import time.
227
+ - Calling `getContext()` at module scope. It only works while handling a request.
228
+ - Storing `NETLIFY_PURGE_API_TOKEN` in a module-level variable. Read it when you call the purge API.
229
+ - Keeping the server warm with self-pings or timers, or running scheduled work with `setInterval`. Use a scheduled function that calls a server route.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@netlify/agent-runner-cli",
3
3
  "type": "module",
4
- "version": "2.17.0-PAW-612.0",
4
+ "version": "2.18.0-smtp.0",
5
5
  "description": "CLI tool for running Netlify agents",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -20,6 +20,7 @@
20
20
  "scripts": {
21
21
  "prebuild": "rm -rf dist",
22
22
  "build": "tsup",
23
+ "build:bundle": "tsup --no-dts",
23
24
  "dev": "tsup --watch",
24
25
  "prepare": "husky install node_modules/@netlify/eslint-config-node/.husky/",
25
26
  "prepublishOnly": "npm ci && npm test",
@@ -34,7 +35,7 @@
34
35
  "format:check:prettier": "prettier --check --ignore-path .gitignore --loglevel=warn \"{src,scripts,test,.github}/**/*.{js,ts,md,yml,json,html}\" \"*.{js,ts,yml,json,html}\" \".*.{js,ts,yml,json,html}\" \"!**/package-lock.json\" \"!package-lock.json\" \"!src/skills/**/*.md\" \"!src/lib/__fixtures__/**\"",
35
36
  "format:fix:prettier": "prettier --write --ignore-path .gitignore --loglevel=warn \"{src,scripts,test,.github}/**/*.{js,ts,md,yml,json,html}\" \"*.{js,ts,yml,json,html}\" \".*.{js,ts,yml,json,html}\" \"!**/package-lock.json\" \"!package-lock.json\" \"!src/skills/**/*.md\" \"!src/lib/__fixtures__/**\"",
36
37
  "test:dev": "run-s build test:dev:*",
37
- "test:ci": "run-s build test:ci:*",
38
+ "test:ci": "run-s build:bundle test:ci:*",
38
39
  "test:dev:vitest": "LOG=0 vitest --exclude '**/integration/**'",
39
40
  "test:ci:vitest": "LOG=0 vitest run --coverage --exclude '**/integration/**'",
40
41
  "test:smoke": "node scripts/smoke-dist.js",
@@ -43,6 +44,7 @@
43
44
  "test:integration:claude": "vitest run test/integration/claude.test.ts",
44
45
  "test:integration:gemini": "vitest run test/integration/gemini.test.ts",
45
46
  "test:integration:opencode": "vitest run test/integration/opencode.test.ts",
47
+ "test:integration:antigravity": "vitest run test/integration/antigravity.test.ts",
46
48
  "test:integration:muse": "vitest run test/integration/muse.test.ts",
47
49
  "test:integration:skill-invocation": "vitest run test/integration/skill-invocation.test.ts",
48
50
  "check:types": "tsc --noEmit",
@@ -96,7 +98,7 @@
96
98
  "@google/gemini-cli": "0.63.0",
97
99
  "@modelcontextprotocol/sdk": "^1.29.0",
98
100
  "@netlify/cache-utils": "^6.0.5",
99
- "@netlify/database-proxy": "^0.2.2",
101
+ "@netlify/database-proxy": "^0.2.3",
100
102
  "@netlify/otel": "^6.0.6",
101
103
  "@netlify/ts-cli": "^1.2.0",
102
104
  "@openai/codex": "0.162.0",