← Back to dashboard
netlifyhostingfreshReader view (for NotebookLM)

Netlify Integration Guide

What is Netlify?

The real model

Framework-agnostic primitives: Edge Functions, Functions, managed Postgres, Blobs, Image CDN, and a keyless AI Gateway.

Edge Functions run on Deno at the request CDN node; Functions run in a regional Node runtime (60s sync, 15 min background). Whatever framework you use compiles down to those two, so primitives land a layer below the framework. Netlify Database is managed Postgres that branches per deploy preview and applies migrations automatically. AI Gateway injects provider API keys and base URLs so `new Anthropic()` works with no key of your own, billed to Netlify credits. Cache control is unusually deep — `Netlify-CDN-Cache-Control` plus `Netlify-Vary` on query, country, language, or cookie — which is the real lever for 2G/3G markets.

Eight Netlify primitives

Framework-agnostic building blocks — your framework compiles down onto them.

Text
███╗   ██╗███████╗████████╗██╗     ██╗███████╗██╗   ██╗
████╗  ██║██╔════╝╚══██╔══╝██║     ██║██╔════╝╚██╗ ██╔╝
██╔██╗ ██║█████╗     ██║   ██║     ██║█████╗   ╚████╔╝
██║╚██╗██║██╔══╝     ██║   ██║     ██║██╔══╝    ╚██╔╝
██║ ╚████║███████╗   ██║   ███████╗██║██║        ██║
╚═╝  ╚═══╝╚══════╝   ╚═╝   ╚══════╝╚═╝╚═╝        ╚═╝

Netlify Integration Guide

Focus: Deploying sites, managing edge functions, and automating Netlify projects from Claude Code using the official Netlify MCP server and netlify-cli.

Overview

Netlify is a platform for hosting web apps with built-in CI/CD, edge functions, forms, and identity. Its official MCP server (launched Feb 2025) lets Claude Code create projects, trigger deploys, manage environment variables, install extensions, and query build logs — all via natural language inside your coding session.

Here is the big picture of how a change reaches your users — once you see the flow, the commands below click into place:

Platform Primitives

Netlify's capabilities are exposed as framework-agnostic platform primitives rather than framework features. Whatever you build with — Next.js, Astro, Nuxt, Remix, TanStack Start, SvelteKit — the framework's server code is compiled into Netlify Functions and Edge Functions at build time, and every primitive below becomes available without framework-specific plumbing.

That is the whole design argument: you are not waiting for your framework to add support for image optimisation or cache control, because those live a layer below it.

PrimitiveWhat it isSection
FunctionsRegional Node handlers, web-standard Request → Response, 60s sync / 15 min backgroundServerless Functions
Edge FunctionsDeno at the CDN node — auth gates, redirects, geo, A/BEdge Function Example
Netlify DatabaseManaged Postgres with per-preview branching and automatic migrationsNetlify Database
BlobsZero-config key-value / object store, callable from Functions and the CLI—
Image CDNOn-demand resize + format negotiation via /.netlify/images—
CachingNetlify-CDN-Cache-Control, cache tags, SWR, durable cacheEdge Network
AI GatewayKeyless access to OpenAI / Anthropic / Gemini / OpenRouter modelsAI Gateway
Agent RunnersCoding agents run on Netlify, one DB branch + preview per runAgent Runners
FormsHTML form capture with no backend code—
IdentityBuilt-in auth (GoTrue) — codeAmani uses Clerk insteadSecurity

Everything above runs locally. netlify dev emulates the full set — Functions, Edge Functions, Blobs, the database, AI Gateway, redirects, and the Image CDN — and Vite projects get the same through @netlify/vite-plugin without invoking the CLI. For tests, @netlify/dev exposes that emulator as a library. This is the practical reason the local/production gap on Netlify is small: it is the same engine, not a mock.


Official Documentation

ResourceURL
Netlify Docshttps://docs.netlify.com
Netlify MCP Serverhttps://docs.netlify.com/build/build-with-ai/netlify-mcp-server/
Netlify CLI Referencehttps://docs.netlify.com/cli/get-started/
Edge Functionshttps://docs.netlify.com/edge-functions/overview/
Netlify Functionshttps://docs.netlify.com/functions/overview/
Netlify Databasehttps://docs.netlify.com/build/data-and-storage/netlify-database/
Database: getting startedhttps://docs.netlify.com/build/data-and-storage/netlify-database/getting-started/
Database: API referencehttps://docs.netlify.com/build/data-and-storage/netlify-database/api/
Database: migrationshttps://docs.netlify.com/build/data-and-storage/netlify-database/migrations/
Database: CLI referencehttps://docs.netlify.com/build/data-and-storage/netlify-database/cli/
Database: billing & limitshttps://docs.netlify.com/build/data-and-storage/netlify-database/billing-and-usage/
AI Gatewayhttps://docs.netlify.com/build/ai-gateway/overview/
AI Gateway: quickstarthttps://docs.netlify.com/build/ai-gateway/quickstart-for-ai-gateway/
Agent Runnershttps://docs.netlify.com/build/build-with-ai/agent-runners/overview/
Observabilityhttps://docs.netlify.com/manage/monitoring/observability/overview/
Log drainshttps://docs.netlify.com/manage/monitoring/log-drains/
Security overviewhttps://docs.netlify.com/manage/security/overview/
Secrets Controllerhttps://docs.netlify.com/build/environment-variables/secrets-controller/
Caching (Edge Network)https://docs.netlify.com/build/caching/caching-overview/

MCP Server Setup

Official Netlify MCP Server

The official Netlify MCP server ships as the @netlify/mcp npm package (repo: netlify/netlify-mcp). The older netlify-mcp package name no longer exists — use @netlify/mcp or the hosted remote endpoint.

Bash
# Local (stdio) — runs the package via npx
claude mcp add netlify -- npx -y @netlify/mcp

# Remote (hosted) — Netlify's managed endpoint, handles auth via OAuth in the client
npx -y add-mcp https://netlify-mcp.netlify.app/mcp

.mcp.json Configuration

JSON
{
  "mcpServers": {
    "netlify": {
      "command": "npx",
      "args": ["-y", "@netlify/mcp"],
      "env": {
        "NETLIFY_AUTH_TOKEN": "${NETLIFY_AUTH_TOKEN}"
      }
    }
  }
}

Requirements: Node.js 22+, a Netlify account with a personal access token (NETLIFY_AUTH_TOKEN). The remote endpoint (https://netlify-mcp.netlify.app/mcp) authenticates over OAuth instead of a token and needs no local Node runtime.

Available MCP Tools

The current server is capability/service based (not one tool per action). Tools group into reader/updater services plus a coding-context helper:

ToolDescription
get-netlify-coding-contextFetch Netlify's current best-practice context for a capability (serverless, edge-functions, blobs, image-cdn, forms, db) — call before writing Netlify code
netlify-project-services-reader / …-updaterRead / create + configure projects (sites), env vars, and project settings
netlify-deploy-services-reader / …-updaterQuery deploy status + logs; trigger and manage deploys
netlify-extension-services-reader / …-updaterDiscover and install/configure Netlify extensions
netlify-team-services-readerTeam, membership, and billing info
netlify-user-services-readerAuthenticated user info

The server also exposes documentation skills for each primitive: netlify-functions, netlify-edge-functions, netlify-blobs, netlify-db, netlify-image-cdn, netlify-forms, netlify-config, netlify-cli-and-deploy, netlify-caching, and netlify-ai-gateway.


CLI Integration

Installation

Bash
npm install -g netlify-cli

Current major is netlify-cli v27 (requires Node.js 22+). Verify with netlify --version; upgrade with npm install -g netlify-cli@latest.

Authentication

Bash
# Interactive login (browser OAuth)
netlify login

# Token-based (for CI)
export NETLIFY_AUTH_TOKEN=...

Key Commands

Bash
# Initialize / link a site
netlify init
netlify link

# Deploy (draft)
netlify deploy

# Deploy to production
netlify deploy --prod

# Open site dashboard
netlify open

# Run dev server (with Functions/Edge Functions)
netlify dev

# Invoke a serverless function locally
netlify functions:invoke my-function --payload '{"key":"value"}'

# Manage environment variables
netlify env:set MY_VAR "my-value"
netlify env:list
netlify env:unset MY_VAR

# View build and deploy logs
netlify status
netlify logs:deploy

# Pull environment to local file
netlify env:import .env

Environment Variables

Bash
# Required for MCP and CLI
NETLIFY_AUTH_TOKEN=...          # From app.netlify.com/user/applications

# Optional project-specific
NETLIFY_SITE_ID=...             # From site settings or `netlify link`

# Variables set for your deployed site
DATABASE_URL=postgresql://...     # only if you bring your own database
NEXT_PUBLIC_API_URL=https://api.example.com

NETLIFY_DB_URL is injected for you. If the project uses Netlify Database, Netlify sets this connection string automatically in builds, agent runners, functions, and edge functions, resolved to the correct database branch for the current deploy. Never set it by hand, never commit it, and never pin it in a secrets vault — a hard-coded value will point at the wrong branch (or a rotated credential). Read it via Netlify.env.get("NETLIFY_DB_URL"), or better, call getConnectionString() from @netlify/database.

Inside function code, read variables via the global Netlify.env — not process.env. Netlify injects a web-standard Netlify object into both serverless and edge functions:

TypeScript
const key = Netlify.env.get("DARAJA_CONSUMER_SECRET"); // server-side only

Store all secrets (Stripe signing secret, Daraja consumer key/secret) as env vars — never in code or in netlify.toml, which is committed.


Automation Workflows

Claude Code Hook: Lint Before Deploy

.claude/settings.json:

JSON
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "if echo \"$CLAUDE_TOOL_INPUT\" | grep -q 'netlify deploy --prod'; then npm run lint && npm run test; fi"
          }
        ]
      }
    ]
  }
}

Slash Command: Deploy to Netlify

.claude/commands/netlify-deploy.md:

Markdown
Deploy the current project to Netlify production.

Run the following steps:
1. Use Bash to run `npm run build` and confirm it succeeds
2. Use Bash to run `netlify deploy --prod --dir=dist` (adjust dir as needed)
3. Report the deploy URL and any warnings
4. If deploy fails, use Bash to run `netlify logs:deploy` and report errors

Usage: /project:netlify-deploy

GitHub Actions: Preview + Production Deploy

YAML
# .github/workflows/netlify.yml
name: Netlify Deploy
on:
  pull_request:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '22' }
      - run: npm ci && npm run build

      - name: Deploy Preview
        if: github.event_name == 'pull_request'
        env:
          NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
          NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
        run: |
          npm install -g netlify-cli
          deploy_url=$(netlify deploy --dir=dist --json | jq -r '.deploy_url')
          echo "Preview: $deploy_url"

      - name: Deploy Production
        if: github.ref == 'refs/heads/main'
        env:
          NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
          NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
        run: netlify deploy --prod --dir=dist

Edge Function Example

Edge functions are great for running logic close to the user before a request reaches your site — here is how the auth example below fits into a request:

netlify/edge-functions/auth.ts:

TypeScript
import type { Config, Context } from "@netlify/edge-functions";

export default async function handler(req: Request, context: Context) {
  const token = req.headers.get("Authorization");
  if (!token || !token.startsWith("Bearer ")) {
    return new Response("Unauthorized", { status: 401 });
  }
  return context.next();
}

export const config: Config = {
  path: "/api/*",
};

Serverless Functions

Edge functions run light logic at the CDN edge; serverless functions are full Node.js handlers for heavier work — talking to a database, calling the Daraja STK Push API, signing tokens, or handling M-Pesa callbacks. They live in netlify/functions/ and use the same web-standard Request → Response signature as edge functions, but run in a regional Node runtime with the full npm ecosystem available.

Function Example

netlify/functions/stk-push.mts:

TypeScript
import type { Config, Context } from "@netlify/functions";

export default async (req: Request, context: Context) => {
  if (req.method !== "POST") {
    return new Response("Method Not Allowed", { status: 405 });
  }

  const { phone, amount } = await req.json();

  // Heavy lifting belongs here: DB writes, Daraja token + STK Push, etc.
  // const token = await getDarajaToken();
  // const result = await initiateStkPush({ phone, amount, token });

  return new Response(JSON.stringify({ ok: true, phone, amount }), {
    status: 200,
    headers: { "content-type": "application/json" },
  });
};

export const config: Config = {
  path: "/api/stk-push",
};

Install the types once: npm install @netlify/functions. The .mts extension opts into ES modules; netlify/functions/stk-push.mts, netlify/functions/stk-push/stk-push.mts, and netlify/functions/stk-push/index.mts all define a function named stk-push.

How to Invoke

  • With config.path (recommended): the function answers at the clean route you set, e.g. https://your-site.netlify.app/api/stk-push. Named params like path: "/order/:id" arrive on context.params.
  • Without config: it falls back to the default route https://your-site.netlify.app/.netlify/functions/stk-push.
  • Locally: netlify dev serves functions on localhost:8888; or call one directly with netlify functions:invoke stk-push --payload '{"phone":"254708374149","amount":1}'.

[functions] Config

The default directory is netlify/functions. esbuild is now the default bundler, so this block is optional — set it only to change the directory or opt out:

TOML
[functions]
  directory = "netlify/functions"
  node_bundler = "esbuild"

Timeouts & long-running work

Function limits are fixed and uniform across all plans (they are not configurable — the old 10s-free / 26s-Pro tiering is gone):

Function typeExecution limitHow to opt in
Synchronous (default)60 secondsdefault
Background15 minutes-background suffix on the file/dir name; returns 202 immediately, response body ignored
Scheduled (cron)30 secondsexport const config = { schedule: "@hourly" } — runs on published deploys only, UTC cron

Background and scheduled functions are the escape hatch for work that exceeds 60s (e.g. Daraja STK Push polling, batch AI inference): persist the result to Netlify Blobs or Postgres and read it back from a synchronous function.

Gotcha: Edge vs Serverless

Reach for the right runtime — they are not interchangeable:

Edge functions run on Deno at the CDN edge (no full Node API, no node_modules bundling), so M-Pesa/Daraja calls, raw Postgres drivers like pg, and Node-only SDKs belong in serverless functions. Use edge functions only for fast request manipulation (auth gates, redirects, geo, A/B) close to the user.

Exception — @netlify/database. getDatabase() picks a connector suited to its runtime, so it does work in edge functions where a raw pg client would not. A lightweight lookup at the edge (feature flag, tenant routing, session check) is therefore fair game; keep heavier transactional work, which needs db.pool, in serverless functions. See Netlify Database below.


Netlify Database

Netlify Database is a fully managed Postgres database built into the platform. Netlify provisions it, applies migrations, and branches it for you. It is built on Neon (serverless Postgres) under the hood, but the setup and management surface is entirely Netlify's — you never interact with Neon directly, and there is no separate Neon account to link.

The headline feature is database branching. Production deploys are the only deploys allowed to touch the production database; every deploy preview gets its own isolated branch, seeded with a copy of production data taken when the preview is first created. Schema changes and data mutations made in a preview never reach production, and a bad branch can simply be reset.

This is the same safety model Deploy Previews gave your code, extended to your data — which is also why each Agent Runner run gets its own branch: an agent can rewrite the schema and delete rows without any risk to the live site.

When to reach for it

SituationPick
App already hosted on Netlify, wants Postgres with zero setupNetlify Database
Need per-preview isolated data for PRs or agent runsNetlify Database (branching is the differentiator)
Need row-level security tied to an auth provider, storage, realtimeSupabase (see supabase/)
Need Postgres decoupled from the host, or used from VercelNeon directly (see neon/)
Key/value or unstructured blobs, not relational dataNetlify Blobs

Prerequisites

  • Node.js 20.12.2 or later
  • Netlify CLI 26.0.0 or later (npm install -g netlify-cli; check with netlify --version)
  • A credit-based plan — Netlify Database is not offered on other plans

Setup

The fastest path on an existing project is the interactive initialiser:

Bash
netlify database init          # interactive
netlify database init --yes    # non-interactive (CI / AI agents)

It installs @netlify/database, lets you pick a query style (raw SQL or Drizzle ORM), scaffolds a starter migration, optionally seeds sample data, and verifies the database is reachable.

To wire it up manually instead:

Bash
npm install @netlify/database

Then write your first migration under netlify/database/migrations/, add a function that queries it, and deploy — Netlify provisions the database and applies the migration as part of the deploy lifecycle.

Gotcha — provisioning is triggered by the package. If @netlify/database is not installed in the project, Netlify will not auto-provision a database. Either install it, or create the database by hand in the UI under Data & Storage → Database.

Querying with @netlify/database

getDatabase() returns a client already configured for wherever the code is running — it selects a different underlying connector for builds and long-running servers than it does for Functions and Edge Functions. That is why it works in an edge function, where a raw pg client would not.

TypeScript
import { getDatabase } from "@netlify/database";

const db = getDatabase();

// Tagged template — interpolated values are ALWAYS parameterized (no SQL injection)
const userId = 42;
const users = await db.sql`SELECT * FROM users WHERE id = ${userId}`;

// Type the returned rows
interface User { id: number; name: string; email: string }
const typed = await db.sql<User>`SELECT id, name, email FROM users`;

// Stream large result sets instead of buffering them
for await (const chunk of db.sql`SELECT * FROM orders`.chunked(100)) {
  console.log(`Processing ${chunk.length} rows`);
}

The sql tagged template is based on Waddler. A SQLTemplate is thenable and also exposes execute(), stream(), chunked(size), and toSQL() (returns the SQL string + params without running it — handy for debugging).

Helpers on db.sql:

HelperPurpose
sql.identifier(v)Safely quote a dynamic table/column name
sql.values(rows)Build a bulk VALUES list from a 2-D array
sql.defaultThe SQL DEFAULT keyword, for inserts
sql.raw(v)Inject an unparameterized fragment — bypasses injection protection
sql.unsafe(q, params?)Run a raw query string with positional $1 params

Security: sql.raw() is the one escape hatch that will happily interpolate attacker-controlled input into SQL. Never pass user input to it — use sql.identifier() for dynamic table/column names and ordinary ${} interpolation for values.

Transactions

db.sql does not pin a connection, so BEGIN/COMMIT must run on a single client from the pool. db.pool is a standard pg.Pool:

TypeScript
import { getDatabase } from "@netlify/database";

const db = getDatabase();
const client = await db.pool.connect();

try {
  await client.query("BEGIN");
  await client.query("INSERT INTO users (name, email) VALUES ($1, $2)", ["Ada", "ada@example.com"]);
  await client.query("INSERT INTO posts (author_id, title) VALUES ($1, $2)", [1, "First post"]);
  await client.query("COMMIT");
} catch (e) {
  await client.query("ROLLBACK");
  throw e;
} finally {
  client.release();   // always release, or the pool leaks connections
}

Migrations

Migrations live in netlify/database/migrations/ — either as flat .sql files or as one subdirectory per migration containing migration.sql:

Text
netlify/database/migrations/
├── 20260301143000_create_users.sql
├── 20260318091500_add_posts.sql
└── 20260425103000_create_comments.sql

Names must match <number>_<slug>: digits setting the order, then a slug of lowercase letters, digits, hyphens, and underscores. Migrations are sorted lexicographically, which is why timestamp prefixes are safer than hand-numbered ones once more than one person (or agent) is adding them.

Netlify applies them automatically:

  • Production deploys — applied immediately before the deploy is published; a failure blocks publication
  • Deploy previews — applied on every new deploy, just before it goes live; a failure fails the deploy

Because they run immediately before the new code goes live, the window where old code meets a new schema is small — but it is not zero. Write backwards-compatible migrations. For breaking changes use the expand-and-contract pattern: add the new column alongside the old and write to both, backfill, then drop the old one in a later migration.

Gotcha — the directory is magic. Anything in netlify/database/migrations/ is auto-applied. If you bring your own migration tool (Prisma Migrate, Atlas, raw scripts), point it at a different directory or Netlify will run those files too.

CLI reference

Every command takes --json for structured output, which is what makes this surface agent-friendly:

Bash
netlify database status                    # enabled? package installed? applied + pending migrations
netlify database status --branch my-feat   # target a remote branch instead of local
netlify database status --show-credentials # include the full connection string

netlify database connect                             # interactive SQL REPL
netlify database connect --query "SELECT * FROM users"
netlify database connect --json                      # print connection details as JSON

netlify database migrations new --description "add users table" --scheme timestamp
netlify database migrations apply            # apply pending migrations locally
netlify database migrations apply --to 0003  # apply up to a specific migration
netlify database migrations pull             # overwrite local files from a remote branch
netlify database migrations reset            # delete local, unapplied migration files

netlify database reset                       # wipe the LOCAL dev database only

netlify database reset and netlify database migrations reset only ever touch the local development database — they cannot damage production or a preview branch.

Local development

netlify dev starts a real Postgres-compatible database on your machine and shuts it down with the dev server — there is no Docker container or local Postgres install to manage:

Bash
netlify dev
netlify database migrations apply     # the local DB does NOT auto-apply migrations

Vite projects can get the same emulated environment without netlify dev by adding @netlify/vite-plugin. Both use the same engine, so data and migrations are interchangeable between them.

Connect any Postgres tool (psql, TablePlus, DataGrip) while it runs:

Bash
psql "$(netlify database connect --json | jq -r .connection_string)"

For integration tests, @netlify/database-dev exposes the emulator as a library (new NetlifyDB() → start() / applyMigrations(dir) / stop()), defaulting to in-memory on a random port. Use @netlify/dev when a test needs the whole Netlify runtime, not just the database.

Differences from production worth knowing: it is a single local process (not a load-testing target), branching is a deploy-time concept so locally there is exactly one database, and auto-scale/sleep settings do not apply.

Bringing your own driver or ORM

The connection string is available two ways — getConnectionString() from @netlify/database, or the NETLIFY_DB_URL environment variable, which is injected into builds, agent runners, functions, and edge functions.

TypeScript
import { getConnectionString } from "@netlify/database";
import pg from "pg";

const pool = new pg.Pool({ connectionString: getConnectionString() });
const { rows } = await pool.query("SELECT * FROM users");

Drizzle ORM has a native adapter. Install from the beta tag (these become 1.0 shortly and carry a better migration format), and point Drizzle Kit's output at Netlify's migrations directory or the automatic runner will never see them:

Bash
npm install @netlify/database drizzle-orm@beta
npm install -D drizzle-kit@beta
TypeScript
// drizzle.config.ts
export default defineConfig({
  dialect: "postgresql",
  schema: "./db/schema.ts",
  out: "netlify/database/migrations",   // ← not the default "drizzle"
});
TypeScript
// db/index.ts — the connection is configured automatically
import { drizzle } from "drizzle-orm/netlify-db";
import * as schema from "./schema";

export const db = drizzle({ schema });

Scaling, sleep, and cost

Compute is metered in database compute units — one unit = 25% of a vCPU + 1 GB RAM. Auto-scale sets a min/max the database moves between; sleep on inactivity (default: after 5 minutes idle) pauses it so an idle database stops consuming credits.

MeterCost
Database compute10 credits per compute unit
Database bandwidth (data out)20 credits per GB
StorageFree until 1 July 2026, then billed at rates announced in advance

Selected plan limits (the full table is in the billing docs):

LimitFreePersonalProEnterprise
Databases per account3550500
Branches per database20100300450
Max compute units141632
Max sleep-on-inactivity5 min5 minAlways onAlways on
Storage per database5 GB100 GB100 GBNo limit
Bandwidth per billing period5 GB100 GB100 GBNo limit

REST API

All endpoints are site-scoped, rooted at https://api.netlify.com/api/v1, and authenticate via OAuth 2:

EndpointPurpose
POST / GET /sites/{site_id}/databaseCreate or read the database (returns connection_string)
POST /sites/{site_id}/database/branchCreate a branch for a deploy_id
GET / DELETE /sites/{site_id}/database/branch/{deploy_id}Read or delete a deploy's branch
POST /sites/{site_id}/database/snapshotPoint-in-time snapshot (defaults to production)
GET /sites/{site_id}/database/snapshotsList snapshots
POST /sites/{site_id}/database/snapshot/{id}/restoreRestore a snapshot to a branch

codeAmani notes

  • Never store cardholder data. Netlify Database is not PCI-DSS certified. Card numbers, PANs, and other regulated cardholder data must not be stored, processed, or transmitted through it. This is a non-issue if you keep the standard codeAmani pattern — Stripe holds the card, we persist only the Stripe customer / payment-intent IDs.
  • Not HIPAA-eligible by default. Protected Health Information must not go in unless the account has been explicitly configured for HIPAA with Netlify. Anything health-adjacent (e.g. DoseVault) needs that conversation before this is chosen as the store.
  • There is no RLS layer. Unlike Supabase, this is plain Postgres with no policy engine wired to an auth provider — a leaked connection string is full database access. Authorize every query inside the function against the Clerk session; do not expect the database to do it for you.
  • NETLIFY_DB_URL is server-side only. It belongs in functions, edge functions, and builds. Never inline it into a client bundle, and never prefix it with NEXT_PUBLIC_ / VITE_.
  • M-Pesa idempotency fits the branching model well. Store CheckoutRequestID on STK Push with a UNIQUE constraint and dedupe callbacks with INSERT ... ON CONFLICT DO NOTHING; preview branches then let you replay Daraja sandbox callbacks against real-shaped data without touching production ledgers.
  • Mind sleep-on-inactivity for Kenya-targeted apps. On Free/Personal the database sleeps after 5 minutes idle, so the first request after a lull pays a wake-up cost on top of an already slow 2G/3G round trip. Pro and Enterprise can set it always-on; below that, warm it or set expectations in the UI.
  • The hosting default is unchanged. Vercel remains the codeAmani default. Netlify Database is a reason to stay on Netlify when a project already lives there and wants per-preview data isolation — it is not on its own a reason to migrate.

Edge Network

Every deploy is published to Netlify's global edge network — a CDN with atomic deploys: a deploy either goes live completely or not at all, and publishing automatically invalidates the cache for changed content. There is no manual purge step in the normal workflow.

Cache-control headers

Netlify honours three cache-control fields, most specific wins:

HeaderApplies toPrecedence
Netlify-CDN-Cache-ControlNetlify's CDN onlyHighest
CDN-Cache-ControlAny CDN that supports itMiddle
Cache-ControlBrowsers and CDNsLowest (fallback)

Use Netlify-CDN-Cache-Control to cache aggressively at the edge while keeping browsers on a short leash:

TypeScript
return new Response(body, {
  headers: {
    "Netlify-CDN-Cache-Control": "public, durable, s-maxage=86400, stale-while-revalidate=604800",
    "Cache-Control": "public, max-age=0, must-revalidate",   // browser always revalidates
  },
});

Two directives are worth knowing:

  • stale-while-revalidate=<s> — keep serving the stale object for this many seconds while it is refreshed in the background. This is what turns a slow origin into a fast page.
  • durable — promotes the object into Netlify's durable cache, so it survives beyond a single edge node and is shared across the network. (Not yet supported on edge function responses.)

Cache key variation with Netlify-Vary

Netlify-Vary controls what makes a request a different cache entry — finer-grained than the standard Vary header:

HTTP
Netlify-Vary: query=page|per_page, country=ke|us, language=en|sw, cookie=is_logged_in
InstructionVaries on
query / query=a|bAll query params, or only the listed ones
header=X|YNamed request headers
language=en|swAccept-Language, honouring quality values
country=ke|usGeo-IP country
cookie=keyNamed cookies

Rules that bite:

  • A given URL should return the same Netlify-Vary on every response — the first one cached wins and later ones are ignored.
  • High-cardinality headers (Accept-Language, Cookie, …) are rejected as raw header= instructions; use the dedicated language= / cookie= instructions with an explicit list.
  • Query instructions are case-sensitive in name, but parameter order does not matter.
  • If you put Cloudflare in front of Netlify, use standard Vary for it — Netlify-Vary is Netlify-specific.
  • Cache key variation is not compatible with basic auth, and On-demand Builders ignore it entirely.

High-Performance Edge (Enterprise)

An Enterprise-only upgrade to the standard network: 70+ global points of presence with dynamic PoP adjustment, a dedicated Site Reliability Engineer, 24×7×365 incident response, and proactive DDoS protection. Netlify quotes up to 50% faster than their standard network (and up to 300% faster than a traditional monolith). Relevant only if a client is on Enterprise — the standard edge is what codeAmani projects actually run on.

codeAmani notes

  • This is the lever for Kenya-targeted projects. On 2G/3G the round trip dominates, so a durable + stale-while-revalidate policy on API responses and rendered pages does more for perceived speed than any bundle-size work. Cache first, optimise JavaScript second.
  • Netlify-Vary: country pairs well with KES/locale switching — one cached page per country rather than a personalised uncached render.
  • Do not cache authenticated responses at the edge. Anything behind a Clerk session needs Cache-Control: private, no-store, and cache key variation is unavailable under basic auth anyway.

AI Gateway

AI Gateway lets project code call OpenAI, Anthropic, Google Gemini, OpenRouter, and TypeSafe AI models with no API keys of your own. Netlify injects both the API key and a provider-specific base URL into every compute context, proxies the call, and bills the token usage to your Netlify credits.

How it works

When a Function or Edge Function initialises, Netlify sets these — but never overrides a value you have already set at project or team level:

ProviderInjected variables
OpenAIOPENAI_API_KEY, OPENAI_BASE_URL
AnthropicANTHROPIC_API_KEY, ANTHROPIC_BASE_URL
Google GeminiGEMINI_API_KEY, GOOGLE_GEMINI_BASE_URL
OpenRouterOPENROUTER_API_KEY, OPENROUTER_BASE_URL
TypeSafe AITYPESAFE_API_KEY, TYPESAFE_BASE_URL

NETLIFY_AI_GATEWAY_KEY and NETLIFY_AI_GATEWAY_URL are always injected and never collide with the above — use them when you deliberately mix your own keys with Netlify's, or want to be explicit about which path a call takes.

Because the official SDKs read these variables by default, the code is just the SDK with no configuration:

TypeScript
// netlify/functions/summarise.ts
import Anthropic from "@anthropic-ai/sdk";
import type { Config, Context } from "@netlify/functions";

// No apiKey argument — ANTHROPIC_API_KEY and ANTHROPIC_BASE_URL are injected.
const anthropic = new Anthropic();

export default async (req: Request, context: Context) => {
  const { text } = await req.json();

  const message = await anthropic.messages.create({
    model: "claude-sonnet-4-5-20250929",
    max_tokens: 1024,
    messages: [{ role: "user", content: `Summarise in two sentences:\n\n${text}` }],
  });

  return Response.json({ summary: message.content });
};

export const config: Config = { path: "/api/summarise" };

Framework server code (Next.js, Astro, Nuxt, TanStack Start, …) is packaged into Functions at build time, so the same variables are available there with no extra setup. The OpenRouter SDK needs v1.2.43 or later.

Requirements and gotchas

  • Credit-based plan, and the project must have had at least one production deploy — AI Gateway does not activate on a project that has never shipped.
  • Works locally through netlify dev or @netlify/vite-plugin.
  • Netlify does not store prompts or model outputs; AI features can be disabled team-wide.

Cost and rate limits

Token usage is converted to USD at published provider rates, then to credits: $1 USD of model usage = 180 credits. Rate limits are per minute, per team, across all projects:

PlanCredits / minute
Free90
Personal450
Pro1,800
Enterprise9,000

Current limitations: context window capped at 200k tokens; Anthropic prompt caching is limited to the default 5-minute ephemeral cache; Gemini explicit context caching is unsupported; request headers are not passed through (so header-gated experimental features are unavailable); no batch inference; no OpenAI priority processing.

codeAmani notes

  • This does not replace the AI routing policy — it changes the wiring. Claude stays primary for reasoning and code generation, OpenAI for structured output; AI Gateway is simply a way to reach them without provisioning ANTHROPIC_API_KEY per project. For a Netlify-hosted prototype it removes a whole class of secret management.
  • Prefer direct provider keys for anything heavy or long-running. The 200k context cap, missing header passthrough, and absent batch inference make the Gateway a poor fit for large-document pipelines; wire those to Anthropic directly via Hazina-managed keys.
  • Always add rate-limiting rules to any function that calls the Gateway. Without them, one abusive visitor drains team-wide credits — and the limit is shared across every project on the team.
  • Set your own key to opt out per project. Because Netlify never overrides a variable you set, providing ANTHROPIC_API_KEY yourself silently routes around the Gateway. That is the migration path when a project outgrows it.

Agent Runners

Agent Runners run a coding agent inside Netlify's infrastructure, prompted from the dashboard (or a phone) rather than a local terminal. The agent gets the project's repo, environment variables, build settings, and deploy pipeline, and ships its work to a deploy preview for review.

Supported agents: Claude Code, OpenAI Codex, Google Gemini, and OpenCode. Each run's model and reasoning effort are configurable per agent, and those settings are a personal preference — they do not apply team-wide. OpenCode is served via OpenRouter and Netlify routes only to providers with a Zero Data Retention policy; the other three run on their vendors' own models.

Available on credit-based plans (Free, Personal, Pro); Enterprise teams go through their account manager.

The pairing with Netlify Database is the point: every run gets its own database branch, so an agent can add tables, write migrations, and mutate data with no path to production until a human publishes.

Good fits: well-defined backlog items, broken links and redirects, copy and content updates from non-engineers, landing/404/maintenance pages, scaffolding a platform primitive, and on-the-go fixes. Poor fits: anything needing deep local iteration, a debugger, or judgement about architecture.

codeAmani notes

  • Complementary to Claude Code locally, not a replacement. Real feature work stays in the terminal where tests, git history, and the superpowers workflow live. Agent Runners are for the small, well-specified changes that are not worth a local checkout — and for letting non-engineers file a change safely.
  • Review the preview before publishing, every time. The isolation guarantee covers production data, not correctness; an approved run ships real code.
  • Runs consume credits from the same team pool as AI Gateway and builds — watch the two together.

Observability

Analytics & metrics → Observability gives near-real-time visibility into production: requests, bandwidth, runtime behaviour, Functions, and Edge Functions. It answers "what is actually happening on the site right now", and it replaces Function Metrics on credit-based plans.

Retention depends on plan:

PlanTime window
Free / PersonalPast 24 hours
ProPast 7 days
EnterprisePast 30 days

Quick actions apply pre-built filter sets across three axes:

AxisAnswers
TrafficTop URLs, top 404s, top URLs with errors, client types, top AI-crawler searches, browser-only traffic
BandwidthBandwidth by URL, by client type, by content type
ComputeMost-invoked functions, slowest URLs, which clients drive function usage

The rest of the monitoring surface sits alongside it:

ToolUse
Log drainsStream deploy/function/traffic logs to an external sink (Datadog, S3, …)
LogsPer-deploy and per-function logs in the dashboard
Real User MonitoringField performance data from actual visitors
LighthouseScores generated as part of the build
NotificationsDeploy/build events to Slack, email, or webhooks
Split testingBranch-based A/B at the edge

Observability does not show credit usage. Spend lives under billing — monitor usage for credit-based plans. Two different questions, two different screens.

codeAmani notes

  • Sentry stays the error-monitoring system of record. Observability is platform-level (requests, cache status, bandwidth, invocation counts); Sentry is application-level (stack traces, releases, user context). Use Observability to find that /api/mpesa/callback is erroring or slow, then Sentry to find why.
  • Cache-status and bandwidth filters are the feedback loop for the Edge Network work above — if a Kenya-targeted page is slow, check cache misses here before touching code.
  • On Free/Personal the 24-hour window means an incident review the next morning has already lost the data. Wire log drains for anything that matters.

Security

Netlify's security surface splits into three areas: access to your sites, access to Netlify itself, and platform-level protections.

Secure access to sites

ControlWhat it does
Password protectionSingle shared password on a site or deploy preview
Project visibilityPublic / private project listings
Firewall traffic rulesAllow or block by IP address or geography
Rate limiting rulesPer-visitor request caps (use these in front of AI Gateway functions)
Web Application FirewallManaged rule sets against common attack traffic
Role-based access controlPer-role permissions on site access
Basic auth via custom headersCredentials enforced at the edge

Secure access to Netlify

SAML SSO through an identity provider, SCIM directory sync for provisioning, enforced 2FA, role-based access control, and the Secrets Controller — an enhanced policy for the most sensitive environment variables that blocks them from being exposed in builds and adds secret scanning. Netlify also scans deploys for leaked secrets and can fail the build when it finds one.

Platform protections

Proactive DDoS monitoring with automatic detection, rate limiting, and client blocking; global load balancing; AES-256 (or stronger) encryption at rest; TLS 1.2+ in transit; Content Security Policy support; log drains; and Private Connectivity for Enterprise. Enterprise teams also get a Security Scorecard that grades the team's posture.

Compliance

SOC 2 Type 2 and ISO 27001 reports, PCI DSS, GDPR and CCPA — current details live at the Netlify trust center.

Careful — platform PCI DSS does not extend to Netlify Database. The hosting platform carries PCI DSS, but Database Services are explicitly not PCI-DSS certified and are not HIPAA-eligible by default. Hosting a payment page on Netlify is fine; writing cardholder data into Netlify Database is not. See the Netlify Database compliance notes.

codeAmani notes

  • Clerk remains the auth provider. Netlify Identity exists and still works, but codeAmani standardises on Clerk (clerk/) so auth is portable across Vercel and Netlify. Do not introduce Identity into a new build without a specific reason.
  • Hazina remains the secret store. Secrets Controller is a good second line of defence inside Netlify — enable it for production keys — but the source of truth stays Hazina, and values still never pass through chat.
  • Secret scanning is a safety net, not the gate. The repo-side gate is gitleaks before push; Netlify's scan catches what slips into a build.
  • Rate limiting is a cost control, not just a security control — it is the single most effective guard on AI Gateway and Functions spend.
  • Still verify webhook signatures yourself (Stripe signing secret, Svix for Clerk, M-Pesa callback validation). None of the above authenticates a webhook payload for you.

Common Use Cases

Use CaseApproach
Deploy on mergeGitHub Actions + netlify deploy --prod
Preview URLs for PRsnetlify deploy (no --prod) in PR workflow
Edge function authnetlify/edge-functions/ directory
Form submissionsNetlify Forms + MCP netlify-project-services-reader
Environment managementMCP netlify-project-services-updater / CLI env:set
Extension managementMCP netlify-extension-services-updater
Relational dataNetlify Database — netlify database init, query via @netlify/database
Safe schema changesMigrations in netlify/database/migrations/, auto-applied per deploy
Isolated data per PRDeploy previews get their own DB branch automatically — no setup
Local DB for dev/testsnetlify dev (bundled Postgres) or @netlify/database-dev in tests
LLM calls without keysAI Gateway — official SDK with no apiKey, billed to Netlify credits
Agent-driven small fixesAgent Runners from the dashboard; each run gets its own DB branch + preview
Fast pages on 2G/3GNetlify-CDN-Cache-Control: durable, stale-while-revalidate=...
Per-country cached pagesNetlify-Vary: country=ke|us instead of uncached personalisation
Find slow / erroring routesObservability → Quick actions → Compute or Traffic insights
Cap per-visitor AI spendRate limiting rules on the function that calls AI Gateway

Troubleshooting

IssueFix
netlify: command not foundnpm install -g netlify-cli
Not linked to a siteRun netlify link in project root
Build fails in CICheck NETLIFY_AUTH_TOKEN and NETLIFY_SITE_ID secrets
Edge function not triggeringVerify config.path matches the route
Function hits the 60s timeoutTimeout is fixed (not configurable) — move long work to a background (-background) or scheduled function
No database provisioned on deploy@netlify/database must be installed, or create it manually under Data & Storage → Database
netlify database command not foundNeeds Netlify CLI ≥ 26.0.0 and Node ≥ 20.12.2 — npm install -g netlify-cli
Drizzle migrations never runSet out: "netlify/database/migrations" in drizzle.config.ts (default drizzle dir is ignored)
Local queries hit an empty schemaThe local DB does not auto-migrate — run netlify database migrations apply
First request after idle is slowSleep-on-inactivity (5 min default); Pro/Enterprise can set the database always-on
AI Gateway vars missing / OPENAI_BASE_URL undefinedNeeds a credit-based plan and at least one production deploy
AI calls bypass the Gateway unexpectedlyYou set that provider's key yourself — Netlify never overrides it; unset it or use NETLIFY_AI_GATEWAY_*
AI Gateway 429sTeam-wide per-minute credit limit (Free 90 / Personal 450 / Pro 1,800); add rate-limiting rules
Netlify-Vary seems ignoredFirst response cached for that URL wins; also unsupported under basic auth and On-demand Builders
Stale content after deployAtomic deploys auto-invalidate — check for a manual durable policy or an upstream CDN (Cloudflare)
Observability data has vanishedRetention is 24h on Free/Personal, 7d Pro, 30d Enterprise — use log drains to keep history

netlify.toml example:

TOML
[build]
  command = "npm run build"
  publish = "dist"

[functions]
  node_bundler = "esbuild"

[[edge_functions]]
  path = "/api/*"
  function = "auth"