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
- Store the encryption key as an environment variable — never in client-side code
- Decrypt server-side in SSR frameworks (Next.js
getServerSideProps, Nuxt server routes) - Cache decrypted content to avoid repeated decryption
- Use ISR/SSG for static sites — decrypt at build time
- Handle missing pages gracefully — check for 404 from CDN before decrypting
Related
- Enable Data Mode API — API reference
- Disable Data Mode API — Revert to standard hosting
- Hosting Modes — Compare all hosting options