webhouse.appwebhouse.appdocs

Push content changes to deployed Next.js sites in ~2 seconds via signed webhook, without full Docker rebuilds.

What is ICD?

Instant Content Deployment pushes content changes from the CMS admin directly to your deployed Next.js site via a signed webhook. Instead of triggering a full Docker rebuild (~73 seconds), the site receives the updated document, writes it to disk, and calls revalidatePath() — all in about 2 seconds.

When to use ICD

ICD works with Next.js SSR sites that have a persistent filesystem:

  • Fly.io with volumes
  • Self-hosted Docker
  • Any environment where the content directory is writable at runtime

ICD does not apply to static builds (Vercel, Netlify, GitHub Pages) where content is baked at build time.

How it works

  1. User saves content in CMS admin
  2. CMS sends the document JSON as an HMAC-SHA256 signed POST to the site's /api/revalidate endpoint
  3. The endpoint verifies the signature, writes the document to disk, and calls revalidatePath()
  4. Next.js serves fresh content on the next request
  5. Full Docker deploy is skipped when the revalidation webhook succeeds

Setup

1. Add the revalidation endpoint

Create app/api/revalidate/route.ts in your Next.js site:

typescript
import { revalidatePath } from "next/cache";
import { NextRequest, NextResponse } from "next/server";
import crypto from "node:crypto";
import { writeFileSync, mkdirSync, unlinkSync, existsSync } from "node:fs";
import { join, dirname } from "node:path";

const SECRET = process.env.REVALIDATE_SECRET;
const CONTENT_DIR = process.env.CONTENT_DIR ?? join(process.cwd(), "content");

export async function POST(request: NextRequest) {
  const signature = request.headers.get("x-cms-signature");
  const body = await request.text();

  if (SECRET) {
    if (!signature) {
      return NextResponse.json({ error: "Missing signature" }, { status: 401 });
    }
    const expected =
      "sha256=" +
      crypto.createHmac("sha256", SECRET).update(body).digest("hex");
    const sigBuf = Buffer.from(signature);
    const expBuf = Buffer.from(expected);
    if (
      sigBuf.length !== expBuf.length ||
      !crypto.timingSafeEqual(sigBuf, expBuf)
    ) {
      return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
    }
  }

  const payload = JSON.parse(body) as {
    paths?: string[];
    collection?: string;
    slug?: string;
    action?: string;
    document?: Record<string, unknown> | null;
  };

  if (payload.collection && payload.slug) {
    const filePath = join(CONTENT_DIR, payload.collection, `${payload.slug}.json`);
    if (payload.action === "deleted") {
      if (existsSync(filePath)) unlinkSync(filePath);
    } else if (payload.document) {
      mkdirSync(dirname(filePath), { recursive: true });
      writeFileSync(filePath, JSON.stringify(payload.document, null, 2), "utf-8");
    }
  }

  const paths: string[] = payload.paths ?? ["/"];
  for (const p of paths) {
    revalidatePath(p);
  }

  return NextResponse.json({
    revalidated: true,
    paths,
    collection: payload.collection,
    slug: payload.slug,
    timestamp: new Date().toISOString(),
  });
}

2. Set the environment variable

Generate a secret and set it on your deployed site:

bash
# Generate
openssl rand -hex 32

# Fly.io
fly secrets set REVALIDATE_SECRET=<generated-secret>

# Docker / .env
REVALIDATE_SECRET=<generated-secret>

3. Configure CMS admin

In Site Settings, add these fields to the site registry entry:

FieldValue
revalidateUrlhttps://your-site.fly.dev/api/revalidate
revalidateSecretThe same secret set as REVALIDATE_SECRET

Payload format

The CMS sends a POST request with:

  • Header x-cms-signature: sha256=<hmac-hex>
  • Body (JSON):
- `collection` — collection name (e.g. `posts`)

- slug — document slug

- actioncreated, updated, or deleted

- document — full document JSON (null for deletes)

- paths — array of URL paths to revalidate

Fallback behavior

If the webhook fails (site is down, authentication error, network timeout), the CMS falls back to a full Docker deploy automatically.

Security

  • The secret must be generated with openssl rand -hex 32 (64 hex characters)
  • Never hardcode the secret in source code — always use environment variables
  • The endpoint uses HMAC-SHA256 with timing-safe comparison to prevent signature forgery
  • Without a valid REVALIDATE_SECRET, the endpoint rejects all requests
Tags:Deploy: VercelWebhooksNext.js
Previous
Docker Deployment
Next
Releases & downloads
JSON API →Edit on GitHub →