# 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.