# Custom Domains via Apache on the VM Operational plan for serving public production presentations on customer-owned hostnames without changing Cloudflare configuration. ## Current VM Facts The checked VM uses this public IPv4 address: ```text 185.8.107.221 ``` The current runtime ports are: | Component | Port | Notes | |-----------|------|-------| | Apache | 80 | Public HTTP reverse proxy | | Frontend | 3001 | Next.js production server | | Backend | 3000 | Express API | Apache currently listens on `:80`; `:443` is not enabled on the VM until a certificate is issued and an SSL virtual host is created. The existing Apache proxy pattern is: ```text /api/* -> http://127.0.0.1:3000 /* -> http://127.0.0.1:3001 ``` Cloudflare settings are out of scope for this workflow. Do not rely on a customer CNAME to `tbp.flatlogic.app`, because that hostname may be proxied by Cloudflare and reject unknown customer hostnames before the request reaches the VM. ## Customer DNS Setup For each customer hostname, ask the customer to create an `A` record pointing directly to the VM public IP: ```text Type: A Name: presentation Value: 185.8.107.221 TTL: 300 or Auto Proxy/CDN: DNS only / Off ``` For the full hostname: ```text presentation.customer.com A 185.8.107.221 ``` Customer-side notes: - DNS records cannot contain paths such as `/p/presentation`. - Do not configure an HTTP redirect to `tbp.flatlogic.app/p/...`. - If the customer uses Cloudflare or another CDN, start with DNS-only mode. - The customer can use multiple hostnames; each hostname should point to the same VM IP and will be routed by our host/path mapping. ## Required Route Data Before VM and app setup, collect these values: ```text CUSTOM_DOMAIN=presentation.customer.com CERTBOT_EMAIL=admin@flatlogic.com ROUTES: / -> presentation /normal -> presentation-normal /premium -> presentation-premium ``` Routes are resolved by `hostname + path`, not by DNS. The same customer can have several hostnames and several paths per hostname. Example mapping: ```text presentation.customer.com | / | presentation presentation.customer.com | /normal | presentation-normal presentation.customer.com | /premium | presentation-premium hotel-a.customer.com | / | hotel-a-tour hotel-b.customer.com | / | hotel-b-tour ``` ## DNS Verification After the customer creates the DNS record, verify resolution from the VM: ```bash dig +short presentation.customer.com ``` Expected output: ```text 185.8.107.221 ``` Verify HTTP reaches the VM: ```bash curl -I http://presentation.customer.com curl -I -H 'Host: presentation.customer.com' http://127.0.0.1/ ``` Before HTTPS is configured, only HTTP is expected to work. ## Apache Virtual Host Create a customer-specific Apache site: ```bash sudo nano /etc/apache2/sites-available/custom-presentation.customer.com.conf ``` Use this HTTP virtual host as the starting point: ```apache ServerName presentation.customer.com ProxyPreserveHost On ProxyRequests Off RewriteEngine On RewriteRule ^/api(/.*)?$ http://127.0.0.1:3000$0 [P,L] RewriteCond %{HTTP:Upgrade} =websocket [NC] RewriteRule /(.*) ws://127.0.0.1:3001/$1 [P,L] RewriteCond %{HTTP:Upgrade} !=websocket [NC] RewriteRule /(.*) http://127.0.0.1:3001/$1 [P,L] ProxyPassReverse /api http://127.0.0.1:3000/api ProxyPassReverse / http://127.0.0.1:3001/ ErrorLog /var/log/apache2/custom-presentation.customer.com-error.log CustomLog /var/log/apache2/custom-presentation.customer.com-access.log combined ``` Enable and reload: ```bash sudo a2ensite custom-presentation.customer.com.conf sudo apache2ctl configtest sudo systemctl reload apache2 ``` ## HTTPS with Certbot Install Certbot if needed: ```bash sudo apt update sudo apt install -y certbot python3-certbot-apache ``` Issue the certificate: ```bash sudo certbot --apache \ -d presentation.customer.com \ --email admin@flatlogic.com \ --agree-tos \ --no-eff-email ``` Verify Apache and certificate state: ```bash sudo ss -ltnp | grep -E ':80|:443' sudo certbot certificates curl -I https://presentation.customer.com sudo certbot renew --dry-run ``` After Certbot succeeds, Apache should listen on `:443` and serve HTTPS for the customer hostname. ## Application Changes The application needs a host/path routing layer for public production runtime. ### Backend Mapping Add a backend entity such as `custom_domain_routes` with these minimum fields: ```text hostname text, required path text, required project_slug text, required environment text, default production is_active boolean, default true created_at updated_at ``` Required constraint: ```text UNIQUE(hostname, path) ``` Normalization rules: - `hostname`: lowercase, no port. - `path`: starts with `/`; remove trailing slash except for `/`. - `environment`: first version should use only `production`. ### Backend Resolve Endpoint Add a public endpoint: ```text GET /api/runtime-context/custom-domain/resolve?path=/normal ``` Behavior: ```text hostname = request Host header / req.hostname path = normalized query path find active route by hostname + path return { projectSlug, environment } ``` Failure behavior: ```text 400 for invalid path 404 if no active mapping exists ``` Security requirements: - Do not accept hostname from query or body. - Use only the request host that reached Apache/backend. - Do not expose stage, constructor, or admin through customer hostnames. - Return only the data needed to render the public runtime. ### Frontend Custom-Domain Route Add a public catch-all route for customer-domain paths: ```text / /normal /premium ``` Runtime behavior: ```text 1. Read window.location.pathname. 2. Detect that the current host is not a standard platform host. 3. Call /api/runtime-context/custom-domain/resolve?path=. 4. Render . ``` Keep existing platform routes unchanged: ```text /p/[projectSlug] /p/[projectSlug]/stage /constructor ``` ## End-to-End Checks DNS: ```bash dig +short presentation.customer.com ``` Apache: ```bash sudo apache2ctl -S sudo ss -ltnp | grep -E ':80|:443|:3000|:3001' ``` HTTP and HTTPS: ```bash curl -I http://presentation.customer.com curl -I https://presentation.customer.com ``` Routes: ```bash curl -I https://presentation.customer.com/ curl -I https://presentation.customer.com/normal curl -I https://presentation.customer.com/premium curl -I https://presentation.customer.com/api/health ``` Direct upstream checks: ```bash curl -I http://127.0.0.1:3001 curl -I http://127.0.0.1:3000/api/health ``` Expected outcome: - Customer hostname resolves to `185.8.107.221`. - HTTPS certificate matches the customer hostname. - `/api/health` works through the customer hostname. - `/`, `/normal`, and `/premium` route to configured production presentations. - Browser URL remains on the customer hostname. ## Rollout Strategy 1. Configure one test customer hostname. 2. Start with a single route: `/ -> one project slug`. 3. Verify DNS, Apache, HTTPS, API, assets, and first page load. 4. Add `/normal` and `/premium`. 5. Verify images, video, audio, and service worker behavior. 6. Repeat for additional customer hostnames. ## Out of Scope - Cloudflare custom hostnames or DNS changes in the `flatlogic.app` zone. - Customer-side HTTP redirects. - Stage, constructor, or admin access through customer domains. - UI for managing custom-domain routes in the first version.