Landing Page Hosting Architecture
Landing Page Hosting Architecture
Section titled “Landing Page Hosting Architecture”Overview
Section titled “Overview”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.
Architecture Decision
Section titled “Architecture Decision”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)
Why Workers for Platforms?
Section titled “Why Workers for Platforms?”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
High-Level Architecture
Section titled “High-Level Architecture”workspace123.tryhoneygrid.com ↓ Dispatch Worker (routes by subdomain) ↓ User Worker "workspace123" (has static assets) ↓ Serves: /index.html, /about.html, /contact.html, /css/*, /images/*Implementation Components
Section titled “Implementation Components”1. DNS Setup
Section titled “1. DNS Setup”Add wildcard A record in Cloudflare dashboard for tryhoneygrid.com:
Type: AName: *Content: 192.0.2.0 (dummy IP, Worker is origin)Proxy status: Proxied (orange cloud)2. Dispatch Worker
Section titled “2. Dispatch Worker”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 bindingKV: for subdomain → workspaceId lookups
3. User Worker Template
Section titled “3. User Worker Template”Each workspace gets a User Worker deployed to the landing-pages dispatch namespace.
// User Worker with static assetsexport 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-slashnot_found_handling:404-page
- Bindings:
ASSETS: assets binding for serving static files
4. File Structure Per Workspace
Section titled “4. File Structure Per Workspace”Workspace User Worker:├── index.html├── about.html├── contact.html├── 404.html├── css/│ └── styles.css├── js/│ └── main.js└── images/ └── logo.pngWorkspace Creation Flow
Section titled “Workspace Creation Flow”When a new workspace is created, the following steps occur:
1. Generate Landing Page Files
Section titled “1. Generate Landing Page Files”// In WorkspaceDO.tsasync 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);}2. Deploy User Worker with Static Assets
Section titled “2. Deploy User Worker with Static Assets”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 } );}Helper Functions
Section titled “Helper Functions”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'}API Routes
Section titled “API Routes”New routes needed in apps/api/src/routes/:
Create/Update Landing Page
Section titled “Create/Update Landing Page”POST /workspaces/:workspaceId/landing-pageBody: { files: { "index.html": "<html>...</html>", "css/styles.css": "body { ... }", ... }}Get Landing Page Status
Section titled “Get Landing Page Status”GET /workspaces/:workspaceId/landing-pageResponse: { subdomain: "workspace123", url: "https://workspace123.tryhoneygrid.com", status: "deployed" | "deploying" | "failed", deployedAt: "2025-01-15T12:00:00Z"}Delete Landing Page
Section titled “Delete Landing Page”DELETE /workspaces/:workspaceId/landing-pageDatabase Schema Updates
Section titled “Database Schema Updates”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(),})Environment Variables
Section titled “Environment Variables”Add to apps/api/wrangler.jsonc:
{ "vars": { "LANDING_PAGES_NAMESPACE": "landing-pages", }, "dispatch_namespaces": [ { "binding": "LANDING_PAGES", "namespace": "landing-pages", }, ],}Security Considerations
Section titled “Security Considerations”-
Subdomain Validation: Validate subdomain names to prevent:
- Reserved subdomains (www, api, admin, etc.)
- Invalid characters
- Subdomain squatting
-
Content Sanitization: Sanitize user-provided HTML/CSS/JS to prevent:
- XSS attacks
- Malicious scripts
- Phishing attempts
-
Rate Limiting: Limit landing page deployments per workspace to prevent abuse
-
Content Size Limits: Enforce maximum file sizes and total asset size per workspace
Future Enhancements
Section titled “Future Enhancements”- Custom Domains: Allow customers to use their own domains via Cloudflare for SaaS
- Dynamic Content: Add server-side rendering or API integration capabilities
- Analytics: Track landing page visits and conversions
- A/B Testing: Deploy multiple versions and split traffic
- Template Library: Provide pre-built landing page templates
- Visual Editor: WYSIWYG editor for landing page creation
- Form Handling: Built-in form submission handling and lead capture
Cost Considerations
Section titled “Cost Considerations”- 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
Testing Strategy
Section titled “Testing Strategy”- Unit Tests: Test subdomain parsing and routing logic
- Integration Tests: Test full deployment flow with mock Cloudflare API
- E2E Tests: Deploy test landing pages and verify they’re accessible
- Load Tests: Verify performance under high traffic scenarios
Rollout Plan
Section titled “Rollout Plan”- Phase 1: Deploy dispatch worker and test with single workspace
- Phase 2: Automated deployment from WorkspaceDO on workspace creation
- Phase 3: Add UI for landing page customization
- Phase 4: Template library and visual editor