Skip to content

Landing Page Hosting Architecture

HoneyGrid provides landing page creation and hosting for customers. Each workspace receives a placeholder landing page on a subdomain of tryhoneygrid.com when created. This document outlines the technical architecture using Cloudflare Workers for Platforms.

We use Workers for Platforms with Static Assets rather than:

  • Simple Worker routing (can’t handle multi-page sites with isolated assets per workspace)
  • Cloudflare for SaaS (designed for custom vanity domains, not subdomain hosting)

Each workspace gets its own User Worker with isolated multi-page static assets. This allows:

  • ✅ Multi-page sites (/index, /about, /contact, etc.)
  • ✅ Isolated static assets per workspace (HTML, CSS, JS, images)
  • ✅ Custom 404 pages per workspace
  • ✅ Scales to millions of workspaces (no route limits)
  • ✅ Can add dynamic logic per workspace later if needed
  • ✅ Automatic SSL certificate management
workspace123.tryhoneygrid.com
Dispatch Worker (routes by subdomain)
User Worker "workspace123" (has static assets)
Serves: /index.html, /about.html, /contact.html, /css/*, /images/*

Add wildcard A record in Cloudflare dashboard for tryhoneygrid.com:

Type: A
Name: *
Content: 192.0.2.0 (dummy IP, Worker is origin)
Proxy status: Proxied (orange cloud)

Location: apps/api/src/landing-page-dispatcher.ts

The dispatch worker handles all incoming requests to *.tryhoneygrid.com and routes them to the appropriate User Worker.

export default {
async fetch(request: Request, env: Env) {
const url = new URL(request.url)
const subdomain = url.hostname.split('.')[0]
// Look up workspace by subdomain
const workspaceId = await env.KV.get(`subdomain:${subdomain}`)
if (!workspaceId) {
return new Response('Workspace not found', { status: 404 })
}
// Route to the user worker for this workspace
const userWorker = env.LANDING_PAGES.get(workspaceId)
return await userWorker.fetch(request)
},
}

Configuration:

  • Route: *.tryhoneygrid.com/*
  • Bindings:
    • LANDING_PAGES: dispatch namespace binding
    • KV: for subdomain → workspaceId lookups

Each workspace gets a User Worker deployed to the landing-pages dispatch namespace.

// User Worker with static assets
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// Serve static assets directly
return env.ASSETS.fetch(request)
},
}

Configuration:

  • Namespace: landing-pages
  • Assets config:
    • html_handling: auto-trailing-slash
    • not_found_handling: 404-page
  • Bindings:
    • ASSETS: assets binding for serving static files
Workspace User Worker:
├── index.html
├── about.html
├── contact.html
├── 404.html
├── css/
│ └── styles.css
├── js/
│ └── main.js
└── images/
└── logo.png

When a new workspace is created, the following steps occur:

// In WorkspaceDO.ts
async createLandingPage(workspaceId: string, subdomain: string) {
// 1. Build/generate landing page HTML, CSS, JS
const landingPageFiles = await this.buildLandingPage(workspaceId);
// 2. Upload User Worker with static assets via API
await this.deployUserWorker(workspaceId, landingPageFiles);
// 3. Store subdomain mapping in KV
await this.env.KV.put(`subdomain:${subdomain}`, workspaceId);
}

The deployment follows Cloudflare’s three-step process:

async deployUserWorker(workspaceId: string, files: Map<string, string>) {
const accountId = this.env.CLOUDFLARE_ACCOUNT_ID;
const apiToken = this.env.CLOUDFLARE_API_TOKEN;
const namespace = 'landing-pages';
// Step 1: Create upload session with manifest
const manifest = Array.from(files.keys()).map(path => ({
key: path,
content_type: getContentType(path),
base64: true
}));
const sessionResponse = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${accountId}/workers/dispatch/namespaces/${namespace}/scripts/${workspaceId}/assets-upload-session`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${apiToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ manifest })
}
);
const session = await sessionResponse.json();
// Step 2: Upload each file
for (const [path, content] of files) {
const uploadResponse = await fetch(
session.result.buckets[0].upload_url,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${apiToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
key: path,
value: btoa(content), // base64 encode
metadata: {
uploaded_by: 'honeygrid',
workspace_id: workspaceId
}
})
}
);
}
// Step 3: Deploy the Worker with assets
const workerCode = `
export default {
async fetch(request, env) {
return env.ASSETS.fetch(request);
}
}
`;
const formData = new FormData();
formData.append('metadata', JSON.stringify({
main_module: 'index.js',
assets: {
jwt: session.result.jwt, // completion token from step 1
config: {
html_handling: 'auto-trailing-slash',
not_found_handling: '404-page'
}
},
bindings: [
{ name: 'ASSETS', type: 'assets' }
],
compatibility_date: '2025-01-29'
}));
formData.append('index.js', new Blob([workerCode], { type: 'application/javascript' }));
await fetch(
`https://api.cloudflare.com/client/v4/accounts/${accountId}/workers/dispatch/namespaces/${namespace}/scripts/${workspaceId}`,
{
method: 'PUT',
headers: {
'Authorization': `Bearer ${apiToken}`
},
body: formData
}
);
}
function getContentType(path: string): string {
const ext = path.split('.').pop()?.toLowerCase()
const contentTypes: Record<string, string> = {
html: 'text/html',
css: 'text/css',
js: 'application/javascript',
json: 'application/json',
png: 'image/png',
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
gif: 'image/gif',
svg: 'image/svg+xml',
webp: 'image/webp',
woff: 'font/woff',
woff2: 'font/woff2',
ttf: 'font/ttf',
ico: 'image/x-icon',
}
return contentTypes[ext || ''] || 'application/octet-stream'
}

New routes needed in apps/api/src/routes/:

POST /workspaces/:workspaceId/landing-page
Body: {
files: {
"index.html": "<html>...</html>",
"css/styles.css": "body { ... }",
...
}
}
GET /workspaces/:workspaceId/landing-page
Response: {
subdomain: "workspace123",
url: "https://workspace123.tryhoneygrid.com",
status: "deployed" | "deploying" | "failed",
deployedAt: "2025-01-15T12:00:00Z"
}
DELETE /workspaces/:workspaceId/landing-page

Add to WorkspaceDOSchema.ts:

export const landingPages = sqliteTable('landing_pages', {
id: text('id').primaryKey(),
workspaceId: text('workspace_id').notNull(),
subdomain: text('subdomain').notNull().unique(),
status: text('status').notNull(), // 'deploying' | 'deployed' | 'failed'
deployedAt: integer('deployed_at', { mode: 'timestamp' }),
errorMessage: text('error_message'),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
})

Add to apps/api/wrangler.jsonc:

{
"vars": {
"LANDING_PAGES_NAMESPACE": "landing-pages",
},
"dispatch_namespaces": [
{
"binding": "LANDING_PAGES",
"namespace": "landing-pages",
},
],
}
  1. Subdomain Validation: Validate subdomain names to prevent:

    • Reserved subdomains (www, api, admin, etc.)
    • Invalid characters
    • Subdomain squatting
  2. Content Sanitization: Sanitize user-provided HTML/CSS/JS to prevent:

    • XSS attacks
    • Malicious scripts
    • Phishing attempts
  3. Rate Limiting: Limit landing page deployments per workspace to prevent abuse

  4. Content Size Limits: Enforce maximum file sizes and total asset size per workspace

  1. Custom Domains: Allow customers to use their own domains via Cloudflare for SaaS
  2. Dynamic Content: Add server-side rendering or API integration capabilities
  3. Analytics: Track landing page visits and conversions
  4. A/B Testing: Deploy multiple versions and split traffic
  5. Template Library: Provide pre-built landing page templates
  6. Visual Editor: WYSIWYG editor for landing page creation
  7. Form Handling: Built-in form submission handling and lead capture
  • Workers for Platforms: Pay per request to User Workers
  • KV: Storage for subdomain mappings (minimal)
  • R2 (future): Could store static assets in R2 instead of bundling with Workers
  • Free tier includes 100,000 requests/day per User Worker
  1. Unit Tests: Test subdomain parsing and routing logic
  2. Integration Tests: Test full deployment flow with mock Cloudflare API
  3. E2E Tests: Deploy test landing pages and verify they’re accessible
  4. Load Tests: Verify performance under high traffic scenarios
  1. Phase 1: Deploy dispatch worker and test with single workspace
  2. Phase 2: Automated deployment from WorkspaceDO on workspace creation
  3. Phase 3: Add UI for landing page customization
  4. Phase 4: Template library and visual editor