Last active
July 14, 2026 08:48
-
-
Save MarcL/c36ca085ca37acab095d7110ec668d98 to your computer and use it in GitHub Desktop.
Deckchair Asia CMS Proposal
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Headless CMS Platform — Deckchair Asia</title> | |
| <style> | |
| body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; max-width: 860px; margin: 0 auto; padding: 2rem; color: #1a1a1a; line-height: 1.7; } | |
| h1 { font-size: 2rem; border-bottom: 3px solid #0070f3; padding-bottom: 0.5rem; margin-bottom: 0.25rem; } | |
| h2 { font-size: 1.4rem; margin-top: 2.5rem; color: #0070f3; border-bottom: 1px solid #e0e0e0; padding-bottom: 0.25rem; } | |
| h3 { font-size: 1.1rem; margin-top: 1.5rem; color: #333; } | |
| code { background: #f4f4f4; padding: 0.15em 0.4em; border-radius: 4px; font-size: 0.9em; } | |
| pre { background: #1e1e1e; color: #d4d4d4; padding: 1.25rem; border-radius: 8px; overflow-x: auto; } | |
| pre code { background: none; padding: 0; color: inherit; font-size: 0.9rem; } | |
| ul, ol { padding-left: 1.5rem; } | |
| li { margin: 0.35rem 0; } | |
| ul.tasks { list-style: none; padding-left: 0; } | |
| li.task { padding: 0.2rem 0; color: #555; } | |
| a { color: #0070f3; text-decoration: none; } | |
| a:hover { text-decoration: underline; } | |
| strong { color: #111; } | |
| hr { border: none; border-top: 1px solid #e0e0e0; margin: 2rem 0; } | |
| p { margin: 0.75rem 0; } | |
| .header-meta { color: #666; font-size: 0.9rem; margin-bottom: 2rem; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="header-meta">Prepared by Marc Littlemore · deckchairasia.com</div> | |
| <h1>Modernising the Stack for Mark's Web Agency</h1> | |
| <p>A proposal for Mark (Deckchair Asia) to think through whether — and how — to modernise his agency's CMS and development workflow.</p> | |
| <p>This isn't a straight recommendation to switch stacks. It's a structured look at the real problem, two viable paths, and an honest comparison of the trade-offs.</p> | |
| <hr> | |
| <h2>The Real Problem</h2> | |
| <p>Before choosing a solution, it's worth being precise about what's actually painful.</p> | |
| <p>The most common problem for agencies at this scale is <strong>no shared codebase</strong>. Every client site is a bespoke build from scratch. That means:</p> | |
| <ul> | |
| <li>Engineers reinvent the same things repeatedly — auth, content types, admin UIs, deployment</li> | |
| <li>Bugs fixed on one site don't carry over to others</li> | |
| <li>Quality and consistency are hard to maintain across a growing client list</li> | |
| <li>Onboarding a new client takes too long</li> | |
| </ul> | |
| <p>The question is: <strong>is the problem WordPress, or is it how WordPress is being used?</strong></p> | |
| <p>If the team has been building fully bespoke themes with no shared foundation, the same problem will follow them to a new stack. The fix is templating and a shared codebase — and that's achievable in WordPress too.</p> | |
| <hr> | |
| <h2>Path A — WordPress Done Better</h2> | |
| <p>WordPress gets a bad reputation, often fairly, but not always. For a small agency with non-technical clients in Asia, it has real advantages worth taking seriously.</p> | |
| <h3>What WordPress does well</h3> | |
| <ul> | |
| <li><strong>Clients already know it.</strong> Many hotel managers and property owners in Asia have used WordPress before. Zero training overhead</li> | |
| <li><strong>ACF Pro (Advanced Custom Fields)</strong> — define structured content types (rooms, amenities, pricing), clients get a clean custom admin UI. Not far off what Directus offers</li> | |
| <li><strong>Page builders</strong> — Elementor or Divi let designers build and iterate without engineering involvement. Real cost saving</li> | |
| <li><strong>WordPress Multisite</strong> — one WordPress installation, multiple client sites. Shared theme, shared plugins. Updates roll out across all clients at once. Directly addresses the reinventing-the-wheel problem</li> | |
| <li><strong>WPGraphQL + Astro frontend</strong> — WordPress as a headless CMS is a real and popular pattern. Modern Astro frontend, WordPress as the content backend. Best of both worlds if the team wants to move away from PHP templating without abandoning WordPress entirely</li> | |
| <li><strong>Managed hosting with Asian data centres</strong> — Kinsta has Singapore and Tokyo nodes. Handles security patches, updates, performance, backups. Ops burden largely disappears</li> | |
| <li><strong>Talent pool</strong> — finding a WordPress developer in Bangkok is easy. Finding a Directus + Astro specialist is harder</li> | |
| </ul> | |
| <h3>Where WordPress falls short</h3> | |
| <ul> | |
| <li>Plugin conflicts and version drift are real, even on managed hosting</li> | |
| <li>Developer experience is poor — engineers won't enjoy it, and that has a real cost</li> | |
| <li>ACF schema portability between sites is doable but fiddly — not as clean as a JSON snapshot</li> | |
| <li>Multisite has quirks, especially with custom domain mapping per client</li> | |
| <li>Performance ceiling is lower — PHP, needs caching layers, more server resources per site</li> | |
| <li>Less control over data structure than a purpose-built CMS</li> | |
| </ul> | |
| <h3>What Path A looks like in practice</h3> | |
| <ul> | |
| <li>WordPress Multisite on Kinsta Singapore (~$30–50/month to start, scales per client)</li> | |
| <li>ACF Pro for structured content types</li> | |
| <li>Shared parent theme with child themes per client for branding</li> | |
| <li>WPGraphQL + Astro if the team wants a modern frontend (optional, addable later)</li> | |
| <li>Existing clients migrate naturally — no big-bang replatform</li> | |
| </ul> | |
| <p><strong>Upfront cost:</strong> Low. Engineers already know WordPress. A few days to set up Multisite and the shared theme structure.</p> | |
| <hr> | |
| <h2>Path B — New Stack (Directus + Astro)</h2> | |
| <p>A clean break. Purpose-built headless CMS, modern frontend framework, self-hosted on DigitalOcean Singapore.</p> | |
| <h3>What this does well</h3> | |
| <ul> | |
| <li><strong>Developer experience</strong> — Astro + Directus is a much better stack to build on. Engineers will move faster and enjoy the work more</li> | |
| <li><strong>Schema as code</strong> — Directus schema exported as JSON. Template a new client site in minutes, not days</li> | |
| <li><strong>No plugin roulette</strong> — a controlled stack the team owns is more stable and predictable</li> | |
| <li><strong>Cost at scale</strong> — self-hosted on DigitalOcean Singapore, ~$12/month shared infrastructure for multiple clients</li> | |
| <li><strong>Clean content model</strong> — Directus generates a structured admin UI directly from the schema. Clients get only what they need, nothing more</li> | |
| </ul> | |
| <h3>The honest trade-offs</h3> | |
| <ul> | |
| <li><strong>Upfront investment</strong> — building the templates, deployment scripts, and schema snapshots is probably a few weeks of engineering work before the first client goes live</li> | |
| <li><strong>New territory</strong> — Docker, Coolify, Directus, Astro. The engineers are capable, but there will be a learning curve and unexpected gaps</li> | |
| <li><strong>Clients won't know it</strong> — Directus is unfamiliar. That's fine once they're in it, but expect some hand-holding at launch</li> | |
| <li><strong>Smaller talent pool</strong> — harder to hire for if an engineer leaves</li> | |
| </ul> | |
| <h3>The key question</h3> | |
| <p>Is the problem interesting enough to justify the investment? If the engineers are genuinely frustrated with WordPress and Mark wants to build a productised, scalable agency model — yes. If the primary goal is just "build client sites faster" — WordPress Multisite gets there with less risk.</p> | |
| <hr> | |
| <h2>Comparison</h2> | |
| <p><strong>WordPress Multisite + ACF Pro</strong></p> | |
| <ul> | |
| <li>Upfront cost: Low</li> | |
| <li>Time to first client: Days</li> | |
| <li>Client familiarity: High</li> | |
| <li>Developer experience: Poor</li> | |
| <li>Ops burden: Low (managed hosting)</li> | |
| <li>Latency (Asia): Good (Kinsta Singapore)</li> | |
| <li>Flexibility: Medium</li> | |
| <li>Scalability: Medium</li> | |
| </ul> | |
| <p><strong>Directus + Astro</strong></p> | |
| <ul> | |
| <li>Upfront cost: High (weeks of engineering)</li> | |
| <li>Time to first client: Weeks</li> | |
| <li>Client familiarity: Low</li> | |
| <li>Developer experience: Excellent</li> | |
| <li>Ops burden: Medium (self-managed infra)</li> | |
| <li>Latency (Asia): Good (DigitalOcean Singapore)</li> | |
| <li>Flexibility: High</li> | |
| <li>Scalability: High</li> | |
| </ul> | |
| <hr> | |
| <h2>Recommendation</h2> | |
| <p><strong>If the goal is to reduce time-per-client and improve consistency now:</strong> Path A. WordPress Multisite + ACF Pro. Lower risk, faster payoff, clients already know it.</p> | |
| <p><strong>If the goal is to build a better agency platform for the long term, and the engineers are bought in:</strong> Path B. Directus + Astro. More investment upfront, significantly better foundation.</p> | |
| <p><strong>Either way:</strong> don't start by migrating existing clients. Pilot with one new client first.</p> | |
| <hr> | |
| <h2>AI & MCP Integration — Investigation Notes</h2> | |
| <p><em>This is exploratory research, not settled architecture. Mark's requirements doc is pending.</em></p> | |
| <h3>How Directus MCP actually works</h3> | |
| <p>Built into Directus v11.12+. Disabled by default. Enabled via Settings → AI → Model Context Protocol.</p> | |
| <p><strong>The critical detail:</strong> the MCP server runs under the permissions of the authenticated user. Exactly like the REST API. AI tools can only do what that user's role allows. No special bypass.</p> | |
| <p>Authentication via OAuth (recommended) or static access token. OAuth lets clients sign in via browser and approve MCP access — the issued token is scoped to the MCP endpoint only.</p> | |
| <h3>The two roles Directus explicitly documents</h3> | |
| <p><strong>Content Editor</strong> (recommended for clients/AI):</p> | |
| <ul> | |
| <li>Read/Create/Update on content collections</li> | |
| <li>Files and folders CRUD</li> | |
| <li>System collections: read only</li> | |
| <li><strong>No schema access whatsoever</strong></li> | |
| </ul> | |
| <p><strong>Developer</strong> (required for schema changes):</p> | |
| <ul> | |
| <li>Everything above, plus:</li> | |
| <li>Collections CRUD</li> | |
| <li>Fields CRUD</li> | |
| <li>Relations CRUD</li> | |
| <li>Flows CRUD</li> | |
| </ul> | |
| <p>This directly validates the concern about schema safety. If clients connect via a Content Editor role, they literally cannot change the schema — neither via the UI, the API, nor MCP. The capability doesn't exist for that role.</p> | |
| <h3>Additional safeguards in MCP</h3> | |
| <ul> | |
| <li><strong>Allow Deletes</strong> toggle — disabled by default globally. Even delete of content items is off until explicitly enabled</li> | |
| <li><strong>DISABLE_TOOLS</strong> env var — can block specific MCP tools regardless of role (e.g. <code>["delete-item", "update-field"]</code>)</li> | |
| <li><strong>Custom system prompt</strong> — can constrain what the AI attempts to do</li> | |
| <li><strong>AI Prompts Collection</strong> — reusable prompt templates stored in Directus, scoped per use case</li> | |
| <li><strong>Tool approval</strong> — in the AI Assistant chat, all tool calls require user approval by default. Can be configured per tool</li> | |
| </ul> | |
| <h3>The AI Assistant (chat interface)</h3> | |
| <p>Released as GA in v11.15. Built into the Directus Data Studio sidebar. Requires an API key for OpenAI, Anthropic, Google Gemini, or any OpenAI-compatible provider (including Ollama for self-hosted).</p> | |
| <ul> | |
| <li>Context-aware — clients can attach content items to a conversation</li> | |
| <li>Streams responses in real-time with Markdown</li> | |
| <li>Runs under the authenticated user's permissions (same model as MCP)</li> | |
| <li>Tool calls shown as cards, approval required by default</li> | |
| </ul> | |
| <p>This is the client-facing AI interface. A hotel manager could open Directus, open the AI sidebar, and say "update the description of The Garden Suite and translate it into Thai" — and it would work safely within their Content Editor permissions.</p> | |
| <h3>Schema change workflow</h3> | |
| <p>If a client legitimately needs a new field (e.g. <code>breakfast_included</code> toggle):</p> | |
| <ol> | |
| <li>Client requests it via support or chat</li> | |
| <li>Engineer adds field to schema (Developer role)</li> | |
| <li>Engineer updates Astro frontend template</li> | |
| <li>Deploy</li> | |
| <li>Field available for client/AI to populate</li> | |
| </ol> | |
| <p>This is a developer touchpoint per schema change. Correct behaviour — keeps the frontend stable. Schema changes should never be self-service for clients.</p> | |
| <h3>What needs further investigation</h3> | |
| <ul> | |
| <li><strong>Multi-tenant MCP</strong> — each client instance has its own Directus with its own MCP endpoint. Straightforward. But if we explore shared multi-tenant (one Directus, multiple client namespaces), how does MCP auth scope to a specific tenant? Not documented — needs testing</li> | |
| <li><strong>Vercel deployment module</strong> — v11.15 added one-click Vercel deploys from the Data Studio. Worth exploring for static rebuild triggers if we go that route</li> | |
| <li><strong>Ollama for self-hosted AI</strong> — if Mark wants to avoid OpenAI/Anthropic API costs at scale, running a local LLM on the VPS is possible. Trade-off: quality vs cost. Worth a spike once client numbers grow</li> | |
| <li><strong>Tool approval UX</strong> — approval required by default is safe but potentially annoying for high-frequency content updates. Need to test what the client experience actually feels like</li> | |
| </ul> | |
| <h3>Summary</h3> | |
| <p>The permission model is solid and well-documented. The concern about clients arbitrarily changing schema is a real risk but one Directus has already solved — Content Editor role simply cannot touch schema. The MCP surface respects the same access control as everything else.</p> | |
| <p>This is a green light on the approach, subject to the multi-tenant investigation above.</p> | |
| <hr> | |
| <p>After an initial conversation about stack modernisation, the focus shifted to something more interesting: <strong>a productised, subscription-based CMS with AI integration</strong> that Mark can sell to his clients.</p> | |
| <p>This is a product business, not just an agency workflow improvement.</p> | |
| <h3>The shift in framing</h3> | |
| <p>The earlier discussion was about making Mark's team more efficient. This is different — it's about building something clients pay for monthly. The value proposition isn't "better website" it's "ongoing tool that makes running their business easier."</p> | |
| <h3>What AI integration might actually mean for hotel and property clients in Asia</h3> | |
| <p>Practically useful, not just buzzword-worthy:</p> | |
| <ul> | |
| <li><strong>Multilingual content generation</strong> — describe a room, AI writes copy in English, Thai, Japanese, Chinese. Huge for Asian hospitality clients</li> | |
| <li><strong>Image alt text and SEO metadata</strong> — auto-generated from uploaded photos</li> | |
| <li><strong>Translation management</strong> — maintain one source of truth, AI handles language variants</li> | |
| <li><strong>Review summarisation</strong> — pull Google/TripAdvisor reviews, surface sentiment in the dashboard</li> | |
| <li><strong>Enquiry handling</strong> — AI-assisted first response to contact form submissions</li> | |
| </ul> | |
| <p>That last one starts to blur the line between CMS and client operations tool. Worth exploring.</p> | |
| <h3>How this links to the Directus + Astro stack</h3> | |
| <p>Directus has a built-in Flows engine that can call external APIs — including OpenAI. AI-assisted content creation could live directly in the content editor, not bolted on. This is cleaner than WordPress where you're dependent on third-party plugins for AI features.</p> | |
| <p>The subscription model also changes hosting economics. Centralised control and low per-client cost matters more than per-client isolation — which points back to a shared VPS approach (DigitalOcean Singapore) rather than separate instances.</p> | |
| <h3>What the requirements doc should answer</h3> | |
| <p>Mark is writing a business requirements document. Key things to look for:</p> | |
| <ul> | |
| <li>Who is the actual user — Mark's clients (hotel managers), or their end customers?</li> | |
| <li>What does "subscription" mean — per site, per seat, per usage?</li> | |
| <li>Is AI a core feature or a differentiator on top of a CMS?</li> | |
| <li>What do clients currently pay for that this replaces or augments?</li> | |
| <li>What do hotel managers in Thailand actually struggle with day-to-day? That's where the real product is</li> | |
| <li>Is Mark trying to productise for scale, or deepen relationships with existing clients?</li> | |
| </ul> | |
| <h3>What stays, what changes</h3> | |
| <p><strong>Stays relevant:</strong></p> | |
| <ul> | |
| <li>Directus as the CMS backbone — Flows for AI integration</li> | |
| <li>Astro or similar for the frontend</li> | |
| <li>DigitalOcean Singapore for hosting</li> | |
| <li>Template approach for onboarding new clients</li> | |
| </ul> | |
| <p><strong>Likely needs rethinking:</strong></p> | |
| <ul> | |
| <li>One instance per client vs shared multi-tenant — subscription model may favour multi-tenant</li> | |
| <li>Pricing and packaging</li> | |
| <li>Whether the "product" is the CMS admin, the client-facing site, or both</li> | |
| </ul> | |
| <p><strong>Hold until requirements doc arrives before going deeper on architecture.</strong></p> | |
| <hr> | |
| <p>The rest of this document covers the Directus + Astro stack in full. Skip to here if Path B is the direction.</p> | |
| <hr> | |
| <h3>CMS — <a href="https://directus.io">Directus</a></h3> | |
| <ul> | |
| <li>Open source, self-hosted</li> | |
| <li>Define your data schema (rooms, properties, images, amenities) and Directus generates the admin UI automatically</li> | |
| <li>Excellent built-in auth and role-based permissions</li> | |
| <li>Non-technical clients (hotel managers, property owners) can update content without any coding knowledge</li> | |
| <li>Handles media well — built-in image transformations</li> | |
| <li>Exports schema as JSON — perfect for templating</li> | |
| </ul> | |
| <h3>Frontend — <a href="https://astro.build">Astro</a></h3> | |
| <ul> | |
| <li>Outputs static HTML by default — fast, cheap to host</li> | |
| <li>Templates look like enhanced HTML, so WordPress designers can adapt without learning React or Vue</li> | |
| <li>First-class Directus integration</li> | |
| <li>Supports component islands for any interactive bits</li> | |
| <li>Easy to maintain and hand off</li> | |
| </ul> | |
| <hr> | |
| <h2>The Template Approach</h2> | |
| <p>Build two reusable templates in git:</p> | |
| <p><strong>Hotel template</strong> — content types:</p> | |
| <ul> | |
| <li>Rooms (name, description, images, price, amenities)</li> | |
| <li>Hotel info (name, location, contact, about)</li> | |
| <li>Gallery</li> | |
| <li>Facilities</li> | |
| </ul> | |
| <p><strong>Property template</strong> — content types:</p> | |
| <ul> | |
| <li>Listings (address, price, bedrooms, features, images)</li> | |
| <li>Agency info</li> | |
| <li>Enquiry form</li> | |
| </ul> | |
| <p>Each template repo contains:</p> | |
| <ul> | |
| <li><code>docker-compose.yml</code> — Directus + PostgreSQL</li> | |
| <li>Directus schema snapshot (JSON) — imported on first deploy</li> | |
| <li>Deployment script — spins up the instance, imports schema, creates admin user, configures domain</li> | |
| <li>Astro frontend — connected to the Directus API</li> | |
| </ul> | |
| <p><strong>New client = create repo from template + run script.</strong> Done in minutes.</p> | |
| <p>See Template Repos — Structure & Per-Client Customisation below for the full detail.</p> | |
| <hr> | |
| <h2>Multi-Tenancy — One Instance Per Client</h2> | |
| <p>The safest and simplest approach is <strong>one Directus instance per client</strong>. Each client gets their own database and their own Directus deployment. Complete data isolation — no risk of one client seeing another's data.</p> | |
| <p>This is easier to manage than multi-tenant (shared instance with permission scoping) until the client count gets very large.</p> | |
| <p>Tools like <strong>Coolify</strong> or <strong>Dokploy</strong> (both open source) make deploying and managing multiple Docker instances straightforward from a single dashboard.</p> | |
| <hr> | |
| <h2>Hosting — Choosing the Right Region</h2> | |
| <p>Mark's agency is based in Koh Samui. Engineers are in Bangkok. Clients are primarily in Asia. <strong>Hetzner is the wrong choice here</strong> — their data centres are in Germany, Finland, and the US. Bangkok to Frankfurt is ~200ms minimum latency. For Asian clients that's noticeable.</p> | |
| <h3>The trade-off: EU pricing vs Asia latency</h3> | |
| <p>Hetzner is the cheapest quality VPS in Europe, but cheapest doesn't mean best value when your clients are 9,000km away. A slightly more expensive Singapore server with fast response times is a better product.</p> | |
| <h3>Recommended: DigitalOcean Singapore</h3> | |
| <p>Closest equivalent to Hetzner for simplicity, pricing, and Coolify compatibility — just in the right region.</p> | |
| <ul> | |
| <li>Basic Droplet: 2 vCPU, 2GB RAM — <strong>~$12/month</strong></li> | |
| <li>Singapore data centre — low latency to Bangkok, Koh Samui, and most of Southeast Asia</li> | |
| <li>Engineers in Bangkok get fast access to the server too</li> | |
| <li>Coolify works identically — no other changes to the stack</li> | |
| </ul> | |
| <p><strong>Vultr Singapore</strong> is a viable alternative at ~$10/month for the same spec. Less polished but solid.</p> | |
| <h3>Frontend: consider Cloudflare Workers for global edge</h3> | |
| <p>If Mark's clients span beyond Southeast Asia — Japan, Hong Kong, India, Australia — the VPS location still creates latency for the Astro frontend. The solution is to decouple frontend and backend hosting:</p> | |
| <ul> | |
| <li><strong>Directus + Postgres</strong> → Singapore VPS (DigitalOcean)</li> | |
| <li><strong>Astro frontend</strong> → Cloudflare Workers (global edge)</li> | |
| </ul> | |
| <p>Cloudflare runs at the edge worldwide. Bangkok, Singapore, Hong Kong, Tokyo all get sub-20ms response times regardless of where the origin server is. One deployment, good latency everywhere.</p> | |
| <p><strong>The trade-off:</strong> Cloudflare Workers adds a small amount of complexity — the Astro frontend deploys differently (via <code>wrangler</code> CLI rather than Coolify). Engineers will be comfortable with it, but it's one more moving part. Worth it if the client base is geographically spread; less important if most clients are in Thailand and Southeast Asia.</p> | |
| <h3>Option comparison</h3> | |
| <p><strong>Option 1 — DigitalOcean Singapore + Coolify (recommended to start)</strong></p> | |
| <ul> | |
| <li>Everything on one VPS, Coolify manages frontend and backend</li> | |
| <li>Simple, cheap, good latency for Southeast Asia</li> | |
| <li>~$12/month for the server, scales to 5–10 client instances</li> | |
| </ul> | |
| <p><strong>Option 2 — DigitalOcean Singapore + Cloudflare Workers frontend</strong></p> | |
| <ul> | |
| <li>Directus on VPS, Astro deployed to Cloudflare edge</li> | |
| <li>Global latency for the frontend, regional latency for content API calls</li> | |
| <li>Best for a geographically spread client base</li> | |
| <li>Small additional complexity for engineers</li> | |
| </ul> | |
| <p><strong>Option 3 — Separate VPS per client (Singapore)</strong></p> | |
| <ul> | |
| <li>More isolation, easier to bill clients directly</li> | |
| <li>~$12/month per client on DigitalOcean basic droplet</li> | |
| <li>Clean but higher cost at scale</li> | |
| </ul> | |
| <p><strong>Recommendation:</strong> Start with Option 1. If the client base grows beyond Southeast Asia, move the frontend to Cloudflare Workers.</p> | |
| <hr> | |
| <h2>Domain & DNS Automation</h2> | |
| <p>The goal: Mark enters a client name and the script handles everything — no manual DNS work.</p> | |
| <h3>Recommended approach — two tiers</h3> | |
| <p><strong>Tier 1: Subdomains of Mark's own domain (zero DNS work per client)</strong></p> | |
| <ul> | |
| <li>Set one wildcard DNS record on Mark's domain once: <code>*.deckchairasia.com → VPS IP</code></li> | |
| <li>Each new client gets <code>clientname.deckchairasia.com</code> automatically</li> | |
| <li>Coolify routes traffic by hostname, Traefik handles SSL via Let's Encrypt</li> | |
| <li>No per-client DNS setup at all</li> | |
| </ul> | |
| <p><strong>Tier 2: Client's own custom domain (Cloudflare API)</strong></p> | |
| <p>When a client wants their own domain:</p> | |
| <ol> | |
| <li>Ask client to move domain to Cloudflare nameservers (free, one-off ~5 mins)</li> | |
| <li>Mark runs the script with the custom domain</li> | |
| <li>Script calls Cloudflare API → creates A record pointing to VPS IP automatically</li> | |
| <li>Coolify picks up the domain, SSL cert issued automatically</li> | |
| </ol> | |
| <p>Mark just needs one Cloudflare API token stored on the server. The script handles the rest.</p> | |
| <h3>What the deployment script looks like for Mark</h3> | |
| <pre><code class="language-"> | |
| Client name: Hilton Koh Samui | |
| Custom domain? (leave blank for subdomain): hilton-koh-samui | |
| → Site will be available at: hilton-koh-samui.deckchairasia.com | |
| </code></pre> | |
| <p>That's it. Mark doesn't need to understand DNS, SSL, or server config.</p> | |
| <h3>What happens under the hood</h3> | |
| <ol> | |
| <li>Docker Compose spins up Directus + PostgreSQL</li> | |
| <li>Directus schema template imported automatically</li> | |
| <li>Admin user created with a generated password</li> | |
| <li>Domain registered in Coolify — Traefik picks it up</li> | |
| <li>Cloudflare API called (if custom domain) → A record created</li> | |
| <li>Let's Encrypt SSL cert issued automatically</li> | |
| <li>Script outputs: admin URL, login credentials, client-facing URL</li> | |
| </ol> | |
| <h3>Tools needed</h3> | |
| <ul> | |
| <li><a href="https://cloudflare.com">Cloudflare</a> — free DNS management, API access</li> | |
| <li><a href="https://coolify.io">Coolify</a> — handles domain routing and SSL</li> | |
| <li><a href="https://traefik.io">Traefik</a> — reverse proxy (built into Coolify)</li> | |
| <li>Let's Encrypt — free SSL (built into Traefik)</li> | |
| </ul> | |
| <hr> | |
| <h2>Content Updates & Rebuilds</h2> | |
| <p>When a client edits content in Directus, the Astro frontend needs to reflect those changes. The approach depends on whether Astro is running in static or SSR mode.</p> | |
| <h3>Option 1 — SSR (recommended for Mark's use case)</h3> | |
| <p>Run Astro in SSR mode with the Node adapter on Coolify. Pages render on request — always fresh, no rebuild needed. Any content change in Directus is live immediately.</p> | |
| <ul> | |
| <li>Slightly more server resource usage than static</li> | |
| <li>Still fast — especially for low-traffic client sites</li> | |
| <li>Zero ops overhead: no build triggers, no webhooks, no delays</li> | |
| <li>Simplest setup for a small agency</li> | |
| </ul> | |
| <p><strong>Recommendation:</strong> Start here. It's less moving parts and instant updates are a better client experience.</p> | |
| <h3>Option 2 — Static with Directus Webhooks</h3> | |
| <p>If staying with static Astro, use Directus Flows to trigger a rebuild on content change:</p> | |
| <ol> | |
| <li>Editor updates content in Directus</li> | |
| <li>Directus Flow fires a webhook on <code>items.create</code>, <code>items.update</code>, or <code>items.delete</code></li> | |
| <li>Webhook hits the Coolify deploy hook URL for that site</li> | |
| <li>Astro rebuilds and redeploys</li> | |
| </ol> | |
| <p>Directus Flows is built-in — no extra tooling. Each Coolify deployment exposes a webhook URL you paste into the Flow.</p> | |
| <p><strong>Caveat:</strong> Full Astro rebuild on every save. Fine for occasional updates, annoying for frequent ones. Can add a debounce delay in the Flow to batch changes (e.g. only trigger after 5 minutes of inactivity).</p> | |
| <hr> | |
| <h2>Template Repos — Structure & Per-Client Customisation</h2> | |
| <h3>Repo structure</h3> | |
| <p>Two template repos, owned by Mark:</p> | |
| <ul> | |
| <li><code>agency-hotel-template</code></li> | |
| <li><code>agency-property-template</code></li> | |
| </ul> | |
| <p>Each contains:</p> | |
| <ul> | |
| <li><code>docker-compose.yml</code> — Directus + PostgreSQL</li> | |
| <li><code>directus/schema-base.json</code> — core content types (template, never edited per client)</li> | |
| <li><code>directus/schema-custom.json</code> — client-specific content additions</li> | |
| <li><code>client.config.js</code> — all client-specific config (see below)</li> | |
| <li><code>assets/</code> — logo, fonts, images</li> | |
| <li>Astro frontend</li> | |
| <li>Deployment script</li> | |
| </ul> | |
| <h3>Frontend and backend: same repo, separate deployments</h3> | |
| <p>They live together in source but deploy independently via Coolify. Redeploy the frontend without touching the database. The <code>docker-compose.yml</code> handles Directus + Postgres; Coolify deploys the Astro frontend as a separate service pointing at the same repo.</p> | |
| <h3>Per-client repos</h3> | |
| <p>Each new client gets their own repo created from the template:</p> | |
| <pre><code class="language-bash"> | |
| gh repo create agency/hilton-koh-samui --template agency/agency-hotel-template | |
| </code></pre> | |
| <p>Client customisations never touch the template repos. Template improvements don't accidentally break live sites.</p> | |
| <h3>Config-driven customisation</h3> | |
| <p>All client-specific settings live in one file:</p> | |
| <pre><code class="language-js"> | |
| // client.config.js | |
| export default { | |
| name: "Hilton Koh Samui", | |
| domain: "hilton-koh-samui.deckchairasia.com", | |
| theme: { | |
| primaryColour: "#c8a96e", | |
| font: "Playfair Display", | |
| logo: "/assets/logo.png" | |
| }, | |
| features: { | |
| enquiryForm: true, | |
| gallery: true, | |
| bookingWidget: false | |
| } | |
| } | |
| </code></pre> | |
| <p>The Astro template reads from this — colours, fonts, feature flags. Client customisation is isolated to <code>client.config.js</code> and <code>assets/</code>. Template code stays untouched.</p> | |
| <h3>Pulling template updates into client repos</h3> | |
| <p>Each customer repo keeps the template as an upstream remote:</p> | |
| <pre><code class="language-bash"> | |
| git remote add upstream git@github.com:agency/hotel-template.git | |
| git pull upstream main | |
| </code></pre> | |
| <p>Because client changes are isolated to <code>client.config.js</code> and <code>assets/</code>, merge conflicts are rare. Template improvements flow through cleanly.</p> | |
| <h3>Bespoke client work</h3> | |
| <p>If a client needs a genuinely unique component — a custom booking widget, say — it goes in a <code>custom/</code> directory by convention, never touching core template files. That way <code>git pull upstream</code> never conflicts with bespoke work.</p> | |
| <p><strong>For 90% of clients it's just config + branding. The other 10% get a <code>custom/</code> directory.</strong></p> | |
| <hr> | |
| <h2>Next Steps</h2> | |
| <ul class="tasks"> | |
| <li class="task">☐ Build a proof of concept — one hotel site using Directus + Astro</li> | |
| <li class="task">☐ Export Directus schema as a reusable template</li> | |
| <li class="task">☐ Write a deployment script (Docker Compose + schema import)</li> | |
| <li class="task">☐ Set up Coolify on a Hetzner VPS to test the workflow</li> | |
| <li class="task">☐ Present to Mark with a working demo</li> | |
| </ul> | |
| <hr> | |
| <h2>Links</h2> | |
| <ul> | |
| <li><a href="https://directus.io">Directus</a> — <a href="https://docs.directus.io">Docs</a></li> | |
| <li><a href="https://astro.build">Astro</a> — <a href="https://docs.astro.build">Docs</a></li> | |
| <li><a href="https://coolify.io">Coolify</a> — open source self-hosting platform</li> | |
| <li><a href="https://dokploy.com">Dokploy</a> — alternative to Coolify</li> | |
| <li><a href="https://www.hetzner.com/cloud">Hetzner Cloud</a> — cheapest quality VPS in Europe</li> | |
| <li><a href="https://railway.app">Railway</a> — managed hosting alternative</li> | |
| </ul> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment