@wriven-ai/next
v0.2.2
Published
Next.js helpers for Wriven: a signed webhook → revalidate route handler and preview/draft wiring.
Maintainers
Readme
@wriven-ai/next
Next.js helpers for Wriven: a signature-verified webhook → ISR revalidation route handler, plus the raw signature verifier for custom handling.
- Verified before anything runs — HMAC over the raw body, timing-safe compare, timestamp replay guard
- Revalidate paths or tags per event — wire it to however you fetch
- Zero config surface — one function, one export
Peer dependency: next >= 14 (App Router route handlers). next/cache is
imported lazily, so this package never bundles Next itself.
npm i @wriven-ai/client @wriven-ai/nextA full Next.js setup uses all three Wriven packages:
clientfetches,reactrenders the body,nextrevalidates on publish.
Table of contents
- Quickstart
- Events
- Payload
- Options
- Signature verification
- Tag-based revalidation
- Custom handling
- Responses
- FAQ
Quickstart
Create an App Router route handler and re-export the generated POST:
// app/api/wriven/route.ts
import { createWebhookRoute } from '@wriven-ai/next';
export const { POST } = createWebhookRoute({
secret: process.env.WRIVEN_WEBHOOK_SECRET!, // dashboard → Project Settings → Webhooks
revalidate: (p) => ({
paths: [`/blog/${p.entry.slug}`, '/blog'],
}),
});Then register a webhook in the dashboard pointing at
https://yoursite.com/api/wriven. On every publish/unpublish/delete Wriven
POSTs a signed payload; the route verifies it and revalidates the paths (or
tags) you return for that event.
Events
| Event | Fires when |
|-------|------------|
| entry.published | an entry is published or re-published with changes |
| entry.unpublished | a published entry goes back to draft |
| entry.deleted | an entry is deleted |
Filter by event inside revalidate — return nothing (or {}) to skip:
revalidate: (p) =>
p.event === 'entry.deleted'
? { paths: [`/blog/${p.entry.slug}`, '/blog'] }
: { paths: ['/blog', `/blog/${p.entry.slug}`] },Payload
revalidate and onEvent receive the verified body:
| Field | Type | Notes |
|-------|------|-------|
| event | 'entry.published' \| 'entry.unpublished' \| 'entry.deleted' | |
| projectId | string | project the entry belongs to |
| firedAt | string | ISO timestamp (also sent as X-Wriven-Timestamp) |
| entry.id | string | |
| entry.type | string | content type apiId, e.g. "blog_post" |
| entry.slug | string | |
| entry.status | string | draft / published / archived |
| entry.publishedAt | string \| null | |
| entry.updatedAt | string | ISO timestamp |
Options
createWebhookRoute({
secret: string, // required — the whsec_… signing secret
revalidate?: (payload) => {
paths?: (string | { path: string; type?: 'page' | 'layout' })[];
tags?: string[];
} | void,
onEvent?: (payload) => void | Promise<void>,
})| Option | Notes |
|--------|-------|
| secret | Shown exactly once when the webhook is created (dashboard → Project Settings → Webhooks). Keep it in a server env var. |
| revalidate | Map an event to paths and/or tags to revalidate. Return nothing to skip. |
| onEvent | Arbitrary side effect per verified event — logging, queueing a rebuild, analytics. Runs after revalidation; awaited. |
A plain string path is an exact URL. For a dynamic-segment pattern (all
/blog/[slug] pages at once), wrap it and pass the route type — a bare
'/blog/[slug]' string is treated as a literal URL and matches nothing:
revalidate: (p) => ({
paths: ['/blog', { path: '/blog/[slug]', type: 'page' }],
})Signature verification
Every Wriven webhook delivery carries:
X-Wriven-Signature: sha256=<hex>— HMAC-SHA256 of${timestamp}.${rawBody}keyed with the webhook's signing secretX-Wriven-Timestamp— ISO timestamp of the fire
verifyWrivenSignature (used internally, exported too) checks:
- both headers present,
- the timestamp is within ±5 minutes (replay guard — configurable via
options.toleranceMs), - the signature matches a timing-safe comparison over the raw body — never a re-serialized parse.
The route verifies before parsing or revalidating anything, so an unsigned/tampered request never triggers cache invalidation.
Tag-based revalidation
Paths are one option; tags scale better — for routes rendered on demand.
Fetch with @wriven-ai/client
using matching next.tags, then revalidate the tag for whole-type changes:
// When fetching (e.g. in a page or generateStaticParams)
const posts = await wriven.getEntries('blog_post', {
next: { revalidate: 60, tags: ['type_blog_post'] },
});
// app/api/wriven/route.ts
export const { POST } = createWebhookRoute({
secret: process.env.WRIVEN_WEBHOOK_SECRET!,
revalidate: (p) => ({
tags: ['type_blog_post'], // every cached fetch with this tag is purged
paths: [`/blog/${p.entry.slug}`], // plus the affected page
}),
});Every cached fetch tagged type_blog_post is invalidated at once — no path
list to maintain.
⚠️ Next.js 15/16 caveat — statically prerendered pages ignore tag purges. Build-time fetches for pages prerendered at build (fully static routes, no
export const revalidate) are inlined into the prerender and never registered as tagged data-cache entries.revalidateTagfor them is a silent no-op — the page stays stale forever. Two fixes, use both:
- Add
export const revalidate = 300;(or similar) to every page that fetches Wriven content, so it is a real ISR route.- Have
revalidatereturn explicitpathsfor those pages (list the route per content type) —revalidatePathinvalidates the full route cache regardless of tags:const PATHS_BY_TYPE = { blog_post: ['/blog', { path: '/blog/[slug]', type: 'page' }], job_posting: ['/jobs'], }; revalidate: (p) => ({ paths: ['/', ...(PATHS_BY_TYPE[p.entry.type] ?? [])], tags: [`type_${p.entry.type}`], })
Custom handling
Skip the route builder and verify yourself (works in any Node runtime — this function has no Next.js dependency):
import { verifyWrivenSignature } from '@wriven-ai/next';
export async function POST(req: Request) {
const raw = await req.text();
const headers = Object.fromEntries(req.headers); // keys are lowercase
if (!verifyWrivenSignature(raw, headers, secret)) {
return new Response('Bad signature', { status: 401 });
}
// …your logic
}Tighten the replay window if your clocks are trusted:
verifyWrivenSignature(raw, headers, secret, { toleranceMs: 60_000 }); // ±1 minResponses
| Status | Body | Meaning |
|--------|------|---------|
| 200 | { ok: true, event } | verified, revalidated, onEvent ran |
| 401 | Invalid signature | missing/stale/tampered signature — not processed |
| 400 | Invalid payload | body is not valid JSON |
Wriven retries failed deliveries (non-2xx) a few times with backoff; a 200
stops retries.
FAQ
Pages Router? The verifier works anywhere; createWebhookRoute targets
App Router route.ts files (it returns a Request → Response handler). Use
verifyWrivenSignature + res.revalidate() in Pages Router API routes.
Is next/cache bundled? No — it's dynamically imported at request time and
marked external, so this package stays runtime- and version-agnostic
(next >= 14).
Multiple webhooks / secrets? One route per secret, or read the signature
yourself with verifyWrivenSignature and branch on X-Wriven-Event.
MIT
