@netlify/agent-runner-cli 2.16.0-server.2 → 2.16.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.
@@ -9,20 +9,17 @@ 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 in Early Access. The rule for choosing it is strict:
12
+ Netlify Server is an experimental primitive. 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:
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.
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.
22
19
 
23
20
  ## Quick start
24
21
 
25
- Create exactly one entrypoint at `netlify/server/index.mjs`.
22
+ Create exactly one entrypoint at `netlify/server/index.mjs`. Any Node.js server works. It MUST listen on `process.env.PORT`.
26
23
 
27
24
  Express:
28
25
 
@@ -52,87 +49,49 @@ app.get('/api/hello', (c) => c.json({ hello: 'world' }))
52
49
  export default app
53
50
  ```
54
51
 
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
-
65
52
  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`.
66
53
 
67
54
  ## Rules
68
55
 
69
56
  - **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.
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.
57
+ - **Listen on `process.env.PORT`.** It is set before your code is imported. Do not hardcode a port.
72
58
  - **Do not daemonize, fork, or spawn long-lived child processes.** One process serves the traffic.
73
59
  - Environment variables work as usual via `process.env`.
74
60
  - When adding npm modules, ensure `node_modules` is in `.gitignore`.
75
61
 
76
62
  ## Routing and static files
77
63
 
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).
64
+ The server receives every request that is not served as a static file:
85
65
 
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.
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.
89
67
  - 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.
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
- ```
68
+ - There is no path prefix: the server sees `/`, `/api/anything`, and everything else, with the original paths.
102
69
 
103
70
  ## Lifecycle: scale to zero
104
71
 
105
72
  This is the most important mental model for building correctly:
106
73
 
107
- - After 5 minutes with no requests, the server is shut down. The next request starts a fresh process (a cold start).
74
+ - After a period with no traffic, the server is shut down. The next request starts a fresh process (a cold start).
108
75
  - **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).
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`.
76
+ - In-flight requests are never cut off by the idle shutdown.
112
77
  - 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.
115
78
  - `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.
116
79
 
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.
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.
118
81
 
119
82
  ```javascript
120
- import { onShutdown } from '@netlify/server'
121
-
122
- onShutdown(async ({ drained }) => {
123
- wss.close()
124
- await drained
125
- await pool.end()
126
- })
83
+ export const shutdown = async () => {
84
+ await flushPendingWrites()
85
+ }
127
86
  ```
128
87
 
129
88
  ## Request context: geolocation, cookies, site metadata
130
89
 
131
- Inside request handling, get the Netlify request context with `getContext` from the `@netlify/server` package (add it to `package.json`):
90
+ Inside request handling, get the Netlify request context with `getContext` from the `@netlify/functions` package (add it to `package.json`):
132
91
 
133
92
  ```javascript
134
93
  import express from 'express'
135
- import { getContext } from '@netlify/server'
94
+ import { getContext } from '@netlify/functions'
136
95
 
137
96
  const app = express()
138
97
 
@@ -148,7 +107,6 @@ Rules:
148
107
 
149
108
  - ONLY call `getContext()` while handling a request (inside a route handler, middleware, or code awaited from one). It throws outside a request.
150
109
  - 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.
152
110
 
153
111
  ### SSO user: `context.user`
154
112
 
@@ -168,62 +126,52 @@ See the [changelog post](https://www.netlify.com/changelog/2026-10-07-context-us
168
126
 
169
127
  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.
170
128
 
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.
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.
172
130
 
173
131
  ```javascript
174
132
  import OpenAI from 'openai'
175
133
 
176
- const client = new OpenAI()
177
-
178
134
  app.post('/api/chat', async (req, res) => {
135
+ const client = new OpenAI() // created per request: credentials are available here
179
136
  // ...
180
137
  })
181
138
  ```
182
139
 
183
140
  ## WebSockets
184
141
 
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.
142
+ WebSockets work for servers that handle HTTP upgrades (the `listen` form). Attach the WebSocket server to the same HTTP server:
186
143
 
187
144
  ```javascript
188
145
  // netlify/server/index.mjs
189
- import { createServer } from 'node:http'
190
146
  import express from 'express'
191
147
  import { WebSocketServer } from 'ws'
192
148
 
193
149
  const app = express()
194
- const server = createServer(app)
195
- const wss = new WebSocketServer({ server, path: '/ws' })
196
-
197
- wss.on('connection', (ws) => {
198
- ws.send('connected')
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
+ })
199
161
  })
200
-
201
- export default server
202
162
  ```
203
163
 
204
164
  Rules for WebSocket clients you generate:
205
165
 
206
166
  - 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.
207
167
  - 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.
218
168
 
219
169
  ## Common mistakes
220
170
 
221
171
  - Treating process memory as a database. It is wiped on every cold start. Use Blobs or Database.
222
172
  - Hardcoding a port or defaulting to `3000` without reading `process.env.PORT` first.
223
173
  - Adding an `index.html` to the publish directory that shadows the server's `/` route.
224
- - Creating `netlify/functions/` endpoints alongside the server. Put API routes in the server app; use functions only for scheduled jobs.
174
+ - Creating `netlify/functions/` endpoints alongside the server. Put API routes in the server app.
225
175
  - Two entry files in `netlify/server/` (for example both `index.mjs` and `index.ts`). Keep one.
226
176
  - 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.
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.
177
+ - Constructing AI SDK clients (or calling `getContext()`) at module scope. Per-request state does not exist at import time.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@netlify/agent-runner-cli",
3
3
  "type": "module",
4
- "version": "2.16.0-server.2",
4
+ "version": "2.16.0",
5
5
  "description": "CLI tool for running Netlify agents",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -43,6 +43,7 @@
43
43
  "test:integration:claude": "vitest run test/integration/claude.test.ts",
44
44
  "test:integration:gemini": "vitest run test/integration/gemini.test.ts",
45
45
  "test:integration:opencode": "vitest run test/integration/opencode.test.ts",
46
+ "test:integration:antigravity": "vitest run test/integration/antigravity.test.ts",
46
47
  "test:integration:muse": "vitest run test/integration/muse.test.ts",
47
48
  "test:integration:skill-invocation": "vitest run test/integration/skill-invocation.test.ts",
48
49
  "check:types": "tsc --noEmit",
@@ -1,4 +1,5 @@
1
- import { appendFile, cp, mkdir, readdir, readFile, rm } from 'node:fs/promises'
1
+ import { existsSync } from 'node:fs'
2
+ import { cp, mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises'
2
3
  import { createRequire } from 'node:module'
3
4
  import path from 'node:path'
4
5
  import process from 'node:process'
@@ -8,34 +9,55 @@ const localSkillsDir = new URL('../src/skills/', import.meta.url)
8
9
  const sharedPackagePath = createRequire(import.meta.url).resolve('@netlify/skills/package.json')
9
10
  const sharedSkillsDir = path.join(path.dirname(sharedPackagePath), 'skills')
10
11
  export const bundledSkillsDir = fileURLToPath(new URL('../dist/skills/', import.meta.url))
11
- const selectedSharedSkills = [
12
- { name: 'netlify-access-control' },
13
- {
14
- name: 'netlify-forms',
15
- instructions: new URL('../src/skill-instructions/netlify-forms.md', import.meta.url),
16
- assets: new URL('../src/skill-assets/netlify-forms/', import.meta.url),
17
- },
18
- {
19
- name: 'netlify-image-cdn',
20
- instructions: new URL('../src/skill-instructions/netlify-image-cdn.md', import.meta.url),
21
- },
22
- ]
12
+ const skillOverridesDir = fileURLToPath(new URL('../src/skill-overrides/', import.meta.url))
13
+ export const selectedSharedSkills = ['netlify-access-control', 'netlify-forms', 'netlify-image-cdn', 'netlify-identity']
14
+
15
+ const readOptionalFile = async (filePath) => {
16
+ try {
17
+ return await readFile(filePath, 'utf8')
18
+ } catch (error) {
19
+ if (error.code === 'ENOENT') return undefined
20
+ throw error
21
+ }
22
+ }
23
+
24
+ export const applySkillReplacements = (content, replacements) => {
25
+ let result = content
26
+ for (const { from, to } of replacements) {
27
+ if (!from || result.split(from).length !== 2) {
28
+ throw new Error(`Expected exactly one occurrence of shared skill text: ${JSON.stringify(from)}`)
29
+ }
30
+ result = result.replace(from, () => to)
31
+ }
32
+ return result
33
+ }
23
34
 
24
35
  export const buildSkills = async (targetDir = bundledSkillsDir) => {
25
36
  await rm(targetDir, { recursive: true, force: true })
26
37
  await cp(localSkillsDir, targetDir, { recursive: true })
27
38
 
28
- for (const { name, instructions, assets } of selectedSharedSkills) {
39
+ for (const name of selectedSharedSkills) {
29
40
  await cp(path.join(sharedSkillsDir, name), path.join(targetDir, name), {
30
41
  recursive: true,
31
42
  force: false,
32
43
  errorOnExist: true,
33
44
  })
34
- if (instructions) {
35
- await appendFile(path.join(targetDir, name, 'SKILL.md'), `\n\n${await readFile(instructions, 'utf8')}`)
45
+ const overrideDir = path.join(skillOverridesDir, name)
46
+ const replacements = await readOptionalFile(path.join(overrideDir, 'replacements.json'))
47
+ const instructions = await readOptionalFile(path.join(overrideDir, 'instructions.md'))
48
+ if (instructions !== undefined || replacements !== undefined) {
49
+ const skillPath = path.join(targetDir, name, 'SKILL.md')
50
+ let content = await readFile(skillPath, 'utf8')
51
+ if (replacements !== undefined) {
52
+ content = applySkillReplacements(content, JSON.parse(replacements))
53
+ }
54
+ if (instructions !== undefined) {
55
+ content += `\n\n${instructions}`
56
+ }
57
+ await writeFile(skillPath, content)
36
58
  }
37
- if (assets) {
38
- const assetsDir = fileURLToPath(assets)
59
+ const assetsDir = path.join(overrideDir, 'assets')
60
+ if (existsSync(assetsDir)) {
39
61
  for (const entry of await readdir(assetsDir, { recursive: true, withFileTypes: true })) {
40
62
  if (!entry.isFile()) continue
41
63
  const source = path.join(entry.parentPath, entry.name)
@@ -1,78 +0,0 @@
1
- ---
2
- name: general-backend
3
- description: Use this skill whenever the task involves server-side code — API endpoints, backend logic, request handlers, WebSockets, server-side AI calls, auth checks, or any code that must run on a server rather than in the browser. Covers Netlify Server, Netlify Functions, Edge Functions, and full-stack framework server routes.
4
- ---
5
-
6
- # Server-side code on Netlify
7
-
8
- Where server-side code goes depends on the project. Work out which case applies before writing code, and for a
9
- new project, before scaffolding a template.
10
-
11
- ## New project that needs a Netlify Server: no template
12
-
13
- The create-mode templates are built on a full-stack framework that deploys its server-side rendering as a Netlify
14
- Function on every path, so a Netlify Server added to a template project never receives requests. If the project
15
- needs a Netlify Server, don't scaffold a template. A new project needs one when it has any request-driven backend
16
- code: API endpoints, data saved and read through the backend, WebSockets, server-side AI calls, or an HTTP server
17
- framework such as Express, Hono, or Fastify.
18
-
19
- A site with no backend code (static pages, a marketing or content site, a client-only tool) needs neither a server
20
- nor functions. Use the templates as usual for it.
21
-
22
- ## Project with a full-stack framework: use its server routes
23
-
24
- Full-stack frameworks (such as TanStack Start, Next.js, Astro, SvelteKit, Remix, Nuxt) build
25
- their own server code into Netlify Functions at deploy time. In these projects, put request handling in the
26
- framework's server routes or server functions instead of adding a Netlify Server. Call Netlify Database, Blobs, and
27
- the AI Gateway from those routes. Use Netlify Functions alongside them only for the cases listed under "Only when
28
- Server can't" below.
29
-
30
- HTTP server frameworks such as Express, Hono, Fastify, and Koa are different: nothing deploys them for you, so they
31
- run as a Netlify Server, covered next.
32
-
33
- ## Otherwise: Netlify Server
34
-
35
- **Netlify Server** runs one long-running Node.js process (Express, Hono, Fastify, or plain `node:http`) from
36
- `netlify/server/index.mjs`. Put every request-driven piece of backend code in it:
37
-
38
- - API endpoints and route handlers
39
- - Server-rendered pages and form handling in your own routes
40
- - WebSockets and streaming responses
41
- - Calls to the Netlify AI Gateway, Netlify Database, and Netlify Blobs
42
- - Short work that continues after the response (seconds, such as sending an email), registered with `waitUntil`
43
- - Auth checks, webhooks from third-party services, proxies to external APIs
44
-
45
- Don't pick Functions just because the task is small or looks like a single endpoint.
46
-
47
- See the **netlify-server** skill for complete setup instructions and usage examples.
48
-
49
- ## Only when Server can't: Netlify Functions
50
-
51
- Use **Netlify Functions** alongside the server only for:
52
-
53
- - **Scheduled functions**: work that runs on a cron schedule, with no incoming request.
54
- - **Event handlers**: code triggered by platform events, such as Netlify Forms submissions, Netlify Identity events
55
- (signup, login, validate), or deploy events.
56
- - **Background functions**: jobs that can run longer than about a minute and must not be cut off midway, such as
57
- generating a report or processing an upload (up to 15 minutes). When a server instance is replaced by a deploy or
58
- scaling event, its background work gets only 90 seconds to finish. The server enqueues the job by calling the
59
- background function and returns right away.
60
-
61
- Scheduled functions and event handlers run without an incoming request, which a server can't do, and background
62
- functions aren't cut off when a server instance is replaced. Everything else stays in the server, including the
63
- endpoints these functions talk to.
64
-
65
- See the **netlify-functions** skill for complete setup instructions and usage examples.
66
-
67
- ## Request and response changes at the edge: Edge Functions
68
-
69
- Use **Edge Functions** for logic that runs on every request before it reaches the server or the site: rewrites and
70
- redirects based on geolocation or cookies, A/B tests, header changes, or lightweight auth checks. They run in Deno
71
- close to visitors and don't replace the server for application logic.
72
-
73
- See the **netlify-edge-functions** skill for setup instructions.
74
-
75
- ## Existing projects
76
-
77
- If an existing project already serves its backend from `netlify/functions/`, keep using that for changes to it.
78
- Don't migrate an existing backend to a Netlify Server unless asked.