Guide: Data Mode Integration

Data Mode turns PageGun into an encrypted headless CMS. Content is AES-256-GCM encrypted and stored on a public CDN. Your app fetches and decrypts it client-side, giving you full control over rendering.

When to Use Data Mode

  • You want to render content in your own app (Next.js, Nuxt, SvelteKit, etc.)
  • You need full control over styling and layout
  • You want CDN performance without vendor lock-in
  • You're building a developer-facing product that needs embedded docs

Setup

Step 1: Enable Data Mode

curl -X POST "https://api.pagegun.com/projects/PROJECT_ID/data-mode/enable" \ -H "Authorization: Bearer $PAGEGUN_API_KEY"

Response includes your encryption key — save it securely:

{ "data": { "data_mode": { "enabled": true, "cdn_base": "https://content.pagegun.com/PROJECT_ID/" }, "encryption_key": "your-content-key" } }

⚠️ The encryption key is only returned once. Store it immediately.

Step 2: Create and Publish Content

Create pages as normal via the API. When published, content is encrypted and stored on the CDN.

# Create a docs page curl -X POST "https://api.pagegun.com/pages" \ -H "Authorization: Bearer $PAGEGUN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "page_name": "Getting Started", "slug": "getting-started", "subroute": "docs", "type": "docs", "project_id": "PROJECT_ID", "markdown_content": "# Getting Started\n\nWelcome to our API...", "config": { "sections": [] } }' # Publish → encrypted content pushed to CDN curl -X POST "https://api.pagegun.com/pages/PAGE_ID/publish" \ -H "Authorization: Bearer $PAGEGUN_API_KEY"

Step 3: Fetch and Decrypt in Your App

The CDN stores encrypted files at predictable URLs:

https://content.pagegun.com/{project_id}/{slug}.enc
https://content.pagegun.com/{project_id}/nav.enc  (navigation tree)

Decryption (Node.js / Next.js)

import crypto from 'crypto'; async function fetchPage(projectId: string, slug: string, contentKey: string) { // Fetch encrypted content from CDN const url = `https://content.pagegun.com/${projectId}/${slug}.enc`; const response = await fetch(url); const encrypted = await response.text(); // Decrypt const [ivHex, authTagHex, ciphertext] = encrypted.split(':'); const iv = Buffer.from(ivHex, 'hex'); const authTag = Buffer.from(authTagHex, 'hex'); const key = Buffer.from(contentKey, 'hex'); const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv); decipher.setAuthTag(authTag); let decrypted = decipher.update(ciphertext, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return JSON.parse(decrypted); } // Usage const page = await fetchPage('PROJECT_ID', 'getting-started', process.env.CONTENT_KEY); console.log(page.title, page.markdown_content);

Decryption (Browser / Client-Side)

async function decryptContent(encrypted: string, keyHex: string): Promise<string> { const [ivHex, authTagHex, ciphertext] = encrypted.split(':'); const key = await crypto.subtle.importKey( 'raw', hexToBuffer(keyHex), { name: 'AES-GCM' }, false, ['decrypt'] ); const iv = hexToBuffer(ivHex); const data = hexToBuffer(ciphertext + authTagHex); const decrypted = await crypto.subtle.decrypt( { name: 'AES-GCM', iv, tagLength: 128 }, key, data ); return new TextDecoder().decode(decrypted); } function hexToBuffer(hex: string): Uint8Array { const bytes = new Uint8Array(hex.length / 2); for (let i = 0; i < hex.length; i += 2) { bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16); } return bytes; }

Step 4: Fetch Navigation

For docs sites, fetch the navigation tree:

const nav = await fetchPage('PROJECT_ID', 'nav', process.env.CONTENT_KEY); // nav = { items: [{ title: "Getting Started", slug: "getting-started", children: [...] }] }

Architecture

┌──────────────┐     ┌─────────────┐     ┌──────────────┐
│  PageGun API │────▶│  Encrypt    │────▶│  CDN (R2)    │
│  (publish)   │     │  AES-256-GCM│     │  Public URLs │
└──────────────┘     └─────────────┘     └──────┬───────┘
                                                │
                                         fetch encrypted
                                                │
                                         ┌──────▼───────┐
                                         │  Your App    │
                                         │  decrypt +   │
                                         │  render      │
                                         └──────────────┘

Security Model

  • Content is encrypted at rest on a public CDN — no auth required to fetch
  • Only your app with the encryption key can read the content
  • The encryption key never leaves your server (use it server-side or in env vars)
  • Each project has a unique key — compromising one doesn't affect others

Best Practices

  1. Store the encryption key as an environment variable — never in client-side code
  2. Decrypt server-side in SSR frameworks (Next.js getServerSideProps, Nuxt server routes)
  3. Cache decrypted content to avoid repeated decryption
  4. Use ISR/SSG for static sites — decrypt at build time
  5. Handle missing pages gracefully — check for 404 from CDN before decrypting
© 2026 PageGun. All rights reserved.