Edge ISR & Cache Purges
Traditional headless CMS webhooks frequently suffer from stale cache reads on edge workers. GN-Apex resolves this through a synchronized 4-tier cache eviction pipeline.
“Publish once in the dashboard. Watch every edge node across 300+ data centers evict stale HTML instantly.”
The 4-Tier Invalidation Engine
Direct R2 S3 Purge
Deletes cached Next.js JSON and HTML files directly from Cloudflare R2 via paginated S3 DeleteObjects commands.
Cloudflare Edge Purge
Executes host-level cache purge pings against Cloudflare's global API to evict cached HTML from edge nodes.
Signed ISR Webhook
Sends an HMAC-SHA256 signed ping to the Next.js app's '/api/revalidate' route to flush tag caches.
Real-Time SSE Invalidation
Broadcasts instant invalidation events over active Server-Sent Events streams to update open browser tabs.
Direct R2 S3-API Eviction
On Cloudflare Pages and Workers (@opennextjs/cloudflare), standard revalidateTag()calls can fail silently if the underlying KV/D1 binding is busy. GN-Apex connects directly to the project's dedicated R2 bucket ([subdomain]-cache-r2) and deletes all files under incremental-cache/ via the S3 API:
| 1 | // backend/src/content/content.service.ts |
| 2 | async purgeR2Cache(bucketName: string): Promise<void> { |
| 3 | const s3 = this.getR2Client(); |
| 4 | const prefix = "incremental-cache/"; |
| 5 | |
| 6 | const list = await s3.send(new ListObjectsV2Command({ |
| 7 | Bucket: bucketName, |
| 8 | Prefix: prefix, |
| 9 | })); |
| 10 | |
| 11 | if (list.Contents?.length) { |
| 12 | await s3.send(new DeleteObjectsCommand({ |
| 13 | Bucket: bucketName, |
| 14 | Delete: { |
| 15 | Objects: list.Contents.map(obj => ({ Key: obj.Key! })), |
| 16 | Quiet: true, |
| 17 | }, |
| 18 | })); |
| 19 | this.logger.log(`✅ Evicted ${list.Contents.length} stale Next.js cache files from R2.`); |
| 20 | } |
| 21 | } |
Signed ISR Webhook Endpoint
The Next.js route at app/api/revalidate/route.ts is automatically generated by npx nexus init. It verifies a cryptographic signature before flushing Next.js tags:
| 1 | import { NextRequest, NextResponse } from "next/server"; |
| 2 | import { revalidatePath, revalidateTag } from "next/cache"; |
| 3 | import { generateDocMetadata } from "@/lib/docs-metadata"; |
| 4 | |
| 5 | // Auto-generated SEO Metadata |
| 6 | export const metadata = generateDocMetadata("/docs/content/edge-isr-revalidation"); |
| 7 | |
| 8 | export async function GET(request: NextRequest) { |
| 9 | const secret = request.nextUrl.searchParams.get("secret"); |
| 10 | const EXPECTED_SECRET = process.env.NEXT_PUBLIC_NEXUS_KEY; |
| 11 | const PROJECT_ID = process.env.NEXT_PUBLIC_NEXUS_ID; |
| 12 | |
| 13 | if (!secret || secret !== EXPECTED_SECRET) { |
| 14 | return NextResponse.json({ message: "Unauthorized Handshake" }, { status: 401 }); |
| 15 | } |
| 16 | |
| 17 | try { |
| 18 | // Flush all page layout caches |
| 19 | revalidatePath("/", "layout"); |
| 20 | |
| 21 | // Flush project-specific tag cache |
| 22 | if (PROJECT_ID) { |
| 23 | revalidateTag("nexus_project_" + PROJECT_ID); |
| 24 | } |
| 25 | |
| 26 | return NextResponse.json({ |
| 27 | revalidated: true, |
| 28 | now: Date.now(), |
| 29 | message: "Edge cache purged successfully.", |
| 30 | }); |
| 31 | } catch (err: any) { |
| 32 | return NextResponse.json({ message: err.message }, { status: 500 }); |
| 33 | } |
| 34 | } |
Cryptographic Handshake Verification
Revalidation calls include a timestamped HMAC-SHA256 signature to protect against replay attacks:
| 1 | // Formula evaluated by the control plane |
| 2 | const timestamp = Date.now(); |
| 3 | const signature = createHash("sha256") |
| 4 | .update(`${secretKey}.${projectId}.${timestamp}`) |
| 5 | .digest("hex"); |
| 6 | |
| 7 | // Dispatch: GET /api/revalidate?projectId=...&ts=...&sig=... |