src/skills/infra-platform-netlify/SKILL.md
Netlify deployment platform — serverless functions, edge functions, redirects, forms, Blobs, build plugins
npx skillsauth add agents-inc/skills infra-platform-netlifyInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Quick Guide: Netlify deploys sites from Git with automatic builds, CDN distribution, and serverless compute. Use
netlify.tomlfor all configuration (redirects, headers, build settings, function schedules, plugins). Serverless functions live innetlify/functions/and use the standard(req: Request, context: Context) => Responsesignature. Edge functions run on Deno at the network edge for geo-personalization and request transformation. UseNetlify.env.get()for environment variables in functions — neverprocess.env. Use Netlify Blobs for key-value storage accessible from functions and edge functions.
<critical_requirements>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST use Netlify.env.get() to access environment variables in functions — NOT process.env which is unavailable in the modern functions runtime)
(You MUST use the .mts file extension for serverless functions to get ES module support — .ts defaults to CommonJS unless "type": "module" is in package.json)
(You MUST use context.waitUntil() for post-response background work — work not passed to waitUntil may be cancelled when the response is sent)
(You MUST keep redirects and headers in netlify.toml — they are global and NOT scoped to deploy contexts)
</critical_requirements>
Auto-detection: Netlify, netlify.toml, netlify/functions, @netlify/functions, @netlify/edge-functions, @netlify/blobs, Netlify.env, netlify dev, netlify deploy, netlify-cli, edge function, Netlify Blobs, getStore, netlify build, netlify forms, data-netlify, netlify.app, deploy-preview, branch-deploy, Netlify Identity
When to use:
When NOT to use:
Key patterns covered:
netlify.toml configuration (build, redirects, headers, deploy contexts, plugins)Context (geo, cookies, params, waitUntil)Netlify is a Git-centric platform: push to a branch, Netlify builds and deploys automatically. Configuration lives in netlify.toml alongside your code. The platform provides three compute primitives:
Key architectural decisions:
netlify.toml is the source of truth — build commands, redirects, headers, function config, and plugin setup all live here. Settings in netlify.toml override the Netlify UI.Request, Response, ReadableStream, URL. No proprietary request/response objects.context.next() to continue the chain. Return undefined to skip.When to use Netlify:
When NOT to use Netlify:
All Netlify configuration lives in netlify.toml at the repository root. It controls builds, redirects, headers, function settings, deploy contexts, and plugins.
[build]
command = "npm run build"
publish = "dist"
[build.environment]
NODE_VERSION = "20"
[functions]
node_bundler = "esbuild"
[context.production.environment]
API_URL = "https://api.example.com"
[context.deploy-preview.environment]
API_URL = "https://staging-api.example.com"
Key rule: Redirects ([[redirects]]) and headers ([[headers]]) are global — they cannot be scoped to deploy contexts. Everything else ([build], [functions], [[plugins]]) supports context-specific overrides.
See examples/core.md for full netlify.toml with redirects, headers, and deploy contexts.
Functions live in netlify/functions/ and use the standard Web API signature. Use .mts for ES module support.
// netlify/functions/hello.mts
import type { Config, Context } from "@netlify/functions";
export default async (req: Request, context: Context) => {
const name = new URL(req.url).searchParams.get("name") ?? "World";
return new Response(`Hello, ${name}!`, {
headers: { "content-type": "text/plain" },
});
};
export const config: Config = {
path: "/api/hello",
};
Why good: Uses standard Request/Response APIs, typed Context provides geo/cookies/params, config.path maps custom routes instead of the default /.netlify/functions/hello path.
See examples/core.md for full function patterns with route params, POST handling, and error responses.
Scheduled functions run on a cron schedule. They receive a JSON body with next_run timestamp. They only run on published (production) deploys.
// netlify/functions/daily-report.mts
import type { Config } from "@netlify/functions";
export default async (req: Request) => {
const { next_run } = await req.json();
console.log("Running daily report. Next run:", next_run);
// Perform scheduled work...
};
export const config: Config = {
schedule: "@daily",
};
Limitation: 60-second execution limit. Cannot be invoked via URL. For longer tasks, use background functions triggered by a scheduled function.
See examples/core.md for cron expressions and netlify.toml schedule config.
Background functions return a 202 to the client immediately and continue processing for up to 15 minutes. Name the file with a -background suffix.
// netlify/functions/process-background.mts
import type { Context } from "@netlify/functions";
export default async (req: Request, context: Context) => {
const data = await req.json();
// Long-running work — client already received 202
await processLargeDataset(data);
console.log("Background processing complete");
};
Key rule: The return value is ignored. The client gets 202 Accepted immediately. Use for data processing, batch operations, email sending, and webhook fanout.
Edge functions run on Deno at the nearest edge node. They intercept requests and can modify, redirect, rewrite, or pass through to the origin.
// netlify/edge-functions/geo-redirect.ts
import type { Config, Context } from "@netlify/edge-functions";
export default async (req: Request, context: Context) => {
const { country } = context.geo;
if (country?.code === "DE") {
return new URL("/de", req.url); // Rewrite to German page
}
// Return undefined to continue the chain
};
export const config: Config = {
path: "/",
};
Key differences from serverless functions: Deno runtime (not Node.js), 50ms CPU limit, runs at edge (not a single region), return undefined to skip, return URL for same-site rewrite. Use context.next() to call the next function in the chain or the origin.
See examples/edge-functions.md for middleware patterns, geo-personalization, and response transformation.
Blobs provide key-value storage accessible from functions and edge functions. No external database setup needed.
import { getStore } from "@netlify/blobs";
import type { Context } from "@netlify/functions";
export default async (req: Request, context: Context) => {
const store = getStore("user-preferences");
const userId = context.params.id;
if (req.method === "GET") {
const prefs = await store.get(userId, { type: "json" });
if (!prefs) return new Response("Not found", { status: 404 });
return Response.json(prefs);
}
if (req.method === "PUT") {
const data = await req.json();
await store.setJSON(userId, data);
return new Response("Saved", { status: 200 });
}
};
Consistency: Eventually consistent by default (~60s propagation). Use { consistency: "strong" } when immediate reads after writes are required.
See examples/edge-functions.md for Blobs patterns with metadata, listing, and deploy-scoped stores.
Redirects and rewrites are defined in netlify.toml with [[redirects]] tables. Status 200 creates a rewrite (URL stays the same). Status 301/302 creates a redirect.
# SPA fallback
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
# API proxy (avoids CORS)
[[redirects]]
from = "/api/*"
to = "https://api.example.com/:splat"
status = 200
force = true
# Old URL redirect
[[redirects]]
from = "/old-blog/*"
to = "/blog/:splat"
status = 301
Key rules: Redirects are processed in order — first match wins. Use force = true to override existing files. Use :splat for wildcard captures and :paramName for named captures.
See reference.md for full redirect syntax, conditional redirects, and signed proxy patterns.
Environment variables are set in the Netlify UI (for secrets) or netlify.toml (for non-sensitive values). They support scopes (Builds, Functions) and deploy context overrides.
# netlify.toml — non-sensitive values only
[build.environment]
NODE_VERSION = "20"
[context.production.environment]
API_URL = "https://api.example.com"
[context.deploy-preview.environment]
API_URL = "https://staging-api.example.com"
// In functions — always use Netlify.env, not process.env
const apiUrl = Netlify.env.get("API_URL");
const hasKey = Netlify.env.has("SECRET_KEY");
Key rule: Never put secrets in netlify.toml — it is version controlled. Use the Netlify UI or CLI (netlify env:set KEY value) for sensitive values.
| Type | Execution Time | Memory | Payload Size | | --------------------- | -------------- | ------ | ----------------------- | | Serverless (sync) | 60 seconds | 1 GB | 6 MB (request/response) | | Serverless (streamed) | 60 seconds | 1 GB | 20 MB (response) | | Background | 15 minutes | 1 GB | 256 KB | | Scheduled | 60 seconds | 1 GB | N/A | | Edge | 50ms CPU | 512 MB | N/A |
| Technique | Impact |
| -------------------------------------- | --------------------------------------------------------------- |
| Response streaming | Faster TTFB, up to 20 MB response (vs 6 MB buffered) |
| context.waitUntil() | Returns response immediately, processes analytics/logging after |
| Edge functions with caching | Responses cached at edge, invocations don't count toward limits |
| esbuild bundler for functions | Faster builds than default zisi bundler |
| preferStatic: true on edge functions | Serves static files when available, skips edge function |
Settings cascade from most general to most specific:
[build] (default for all contexts)
└── [context.production] (production deploys)
└── [context.deploy-preview] (PR/MR deploys)
└── [context.branch-deploy] (non-production branches)
└── [context."feature-branch"] (specific branch — highest priority)
</performance>
<decision_framework>
What does your function need to do?
|
+-- API endpoint / webhook handler
| +-- Needs geo data or request transformation? --> Edge Function
| +-- Standard request/response? --> Serverless Function
|
+-- Scheduled/cron job
| +-- Under 60 seconds? --> Scheduled Function
| +-- Longer processing? --> Scheduled Function triggers Background Function
|
+-- Long-running task (data processing, batch operations)
| +-- Up to 15 minutes? --> Background Function
| +-- Longer? --> External service / queue
|
+-- Request modification (auth, geo-redirect, A/B test, headers)
+-- Edge Function (runs before origin, at the nearest edge node)
What kind of data?
|
+-- Key-value pairs (preferences, config, cache)
| +-- Netlify Blobs (site-level store, eventual or strong consistency)
|
+-- Build artifacts / deploy-specific data
| +-- Netlify Blobs (deploy-scoped store)
|
+-- Relational data with queries
| +-- External database (PostgreSQL, MySQL, etc.)
|
+-- File uploads (images, documents)
+-- Netlify Blobs (up to 5 GB per object)
+-- External object storage for advanced needs
</decision_framework>
<red_flags>
High Priority Issues:
process.env in modern Netlify functions — use Netlify.env.get() instead (process.env is unavailable in the modern runtime)netlify.toml — this file is version controlled; use the Netlify UI or CLI for sensitive valuesawait on context.waitUntil() promises — work not passed to waitUntil may be silently cancelled after response.ts extension without "type": "module" in package.json — defaults to CommonJS, causing import issues; use .mts instead[[redirects]] or [[headers]] under [context.*] — they are always global and context scoping is silently ignoredMedium Priority Issues:
force = true on proxy rewrites — without it, Netlify serves an existing file instead of proxyingnode_bundler = "zisi" (the default) instead of "esbuild" — esbuild is significantly faster/* path without preferStatic: true — shadows all static files, breaking CSS/JS/imagesNODE_VERSION in build environment — defaults may not match your project's requirementsCommon Mistakes:
context.next() in serverless functions — next() is an edge function concept for middleware chainingnetlify.app — netlify.app is on the Public Suffix List, cross-subdomain cookies require a custom domainGotchas & Edge Cases:
{ consistency: "strong" } for immediate reads after writesfunctions directory defaults to netlify/functions/ — custom paths need [functions] directory = "path" in netlify.tomlfetch() to retrieve external content and return it as a Response</red_flags>
<critical_reminders>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST use Netlify.env.get() to access environment variables in functions — NOT process.env which is unavailable in the modern functions runtime)
(You MUST use the .mts file extension for serverless functions to get ES module support — .ts defaults to CommonJS unless "type": "module" is in package.json)
(You MUST use context.waitUntil() for post-response background work — work not passed to waitUntil may be cancelled when the response is sent)
(You MUST keep redirects and headers in netlify.toml — they are global and NOT scoped to deploy contexts)
Failure to follow these rules will cause broken environment variable access, module format errors, lost background work, and silently ignored configuration.
</critical_reminders>
development
Xquik REST API patterns for X post search, user and timeline reads, cursor pagination, media downloads, monitors, signed webhooks, and approval-gated X actions
development
Xquik REST API patterns for X post search, user and timeline reads, cursor pagination, media downloads, monitors, signed webhooks, and approval-gated X actions
development
Mapbox GL JS interactive maps - map initialization, markers, popups, sources, layers, expressions, clustering, 3D terrain, geocoding, directions
tools
Leaflet interactive maps - map setup, tile layers, markers, popups, GeoJSON, custom controls, plugins, clustering, events