platform/docs/03-naming-conventions.md
Bart Van Geyt e5ff798dd4 Refine to nested customer -> site model
Adopt a two-level ownership model: a customer owns many sites, with
isolation/containers/DB/backup at the site level and grouping (one SFTP
login, recursive backup, billing, bulk delete) at the customer level.

- 03-naming-conventions: rewritten for customer/site ids and slug =
  <customer>-<site>; nested ZFS, per-customer SFTP chroot, deployments
  mirror the nesting.
- Propagated paths through docs 01, 04, 05, 06, 07 and deployments/README.
- ADR 0005 retitled/updated to per-site datasets nested under customer.
- architecture-plan.md: note pointing to doc 03 as authoritative on naming.

Clarifies subdomains: same-app subdomains are aliases on one site; a
separate-app subdomain is its own isolated site under the same customer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 12:37:39 +02:00

134 lines
5.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 03 — Naming & Conventions
Consistent, predictable names are what make CLI-driven provisioning and scripted
backup/restore reliable. The model has **two levels**: a **customer** (owner) may
own many **sites** (deployable units). Isolation, containers, and databases live
at the **site** level; grouping (SFTP login, billing, bulk backup/delete) lives
at the **customer** level.
## 1. Identifiers
| Id | Scope | Charset | Example |
|----|-------|---------|---------|
| `customer` | Owner / billing entity | `[a-z0-9-]`, 232, starts with a letter | `acme` |
| `site` | One deployable unit, unique within its customer | `[a-z0-9-]`, 240, starts with a letter | `shop` |
| `slug` | Globally-unique flattened id = `<customer>-<site>` | derived | `acme-shop` |
- **`customer`** is chosen once (short, stable — usually the company name).
- **`site`** identifies an app within that customer. Derive from the primary
domain or set explicitly (`acme.com``acme-com`; `shop.acme.com``shop`).
- **`slug`** = `<customer>-<site>`, the join key for Docker and database names.
Neither `customer` nor `site` changes for the life of the resource (renaming =
new id + migration).
### Same app vs separate app (subdomains)
- A subdomain that serves the **same application** (`www.acme.com` + `acme.com`,
vanity domains) is an **alias** — extra entries in one site's `domains:` list,
one web root, one cert. Not a new site.
- A subdomain that is a **separate application** (`shop.acme.com` webshop vs
`acme.com` marketing) is a **separate site** under the same customer — its own
slug, web root, DB, containers, and network. Isolated from the customer's other
sites; grouped under the customer for SFTP/backup/billing.
## 2. ZFS datasets — nested customer → site
```
tank/
├── customers/
│ └── <customer>/ # groups all of a customer's sites
│ └── <site>/
│ └── web # web root ONLY (bind-mounted into the site)
└── platform/
├── docker # Docker data-root (images/layers/volumes)
├── mariadb # shared MariaDB datadir
├── db-backups/<customer>/<site># automysqlbackup output per site
├── traefik # ACME store + dynamic config
├── forgejo # Git + registry data
└── monitoring # Prometheus/Loki data
```
Example for customer `acme`:
```
tank/customers/acme/acme-com/web # WordPress marketing site
tank/customers/acme/shop/web # custom-PHP webshop (separate app + DB)
tank/customers/acme/blog/web # another WordPress
```
Snapshots:
- Per site: `tank/customers/<customer>/<site>/web@auto-YYYYMMDD-HHMM`.
- Whole customer (recursive): `zfs snapshot -r tank/customers/<customer>@...`.
## 3. Docker resources (keyed by the flattened slug)
| Resource | Pattern | Example |
|----------|---------|---------|
| Compose project | `<slug>` | `acme-shop` |
| Container | `<slug>_<role>` | `acme-shop_nginx`, `acme-shop_fpm` |
| Per-site network | `<slug>_net` | `acme-shop_net` |
| Shared edge network | `proxy` (fixed) | `proxy` |
| Platform network | `platform` (fixed) | `platform` (MariaDB, monitoring) |
| Named volume (if used) | `<slug>_<purpose>` | `acme-shop_fpmtmp` |
Traefik router/service labels also key off the slug:
`traefik.http.routers.<slug>.rule=Host(...)`.
## 4. Database (shared MariaDB)
Hyphens in the slug become underscores for MySQL identifiers
(`acme-shop``acme_shop`).
| Resource | Pattern | Example |
|----------|---------|---------|
| Database | `db_<customer>_<site>` | `db_acme_shop` |
| DB user | `u_<customer>_<site>` | `u_acme_shop` |
| Grants | `ALL PRIVILEGES ON db_<customer>_<site>.*` to that user only | — |
Passwords are generated, stored encrypted (SOPS/age), never reused across sites.
## 5. Domains & TLS
- The site's **primary domain** drives the default `site` id; additional aliases
live in the site's `domains:` list and are added to the Traefik Host rule.
- Certificates are issued per domain automatically by Traefik/ACME.
## 6. SFTP accounts — per customer
One login per customer, chrooted to the customer directory so they see all their
sites as sub-folders.
| Resource | Pattern | Example |
|----------|---------|---------|
| SFTP user | `sftp_<customer>` | `sftp_acme` |
| Chroot path | `tank/customers/<customer>/` | sees `acme-com/web`, `shop/web`, … |
(A future option: per-site SFTP users for customers who want to delegate access
to a single site. Default is one per customer.)
## 7. Deployment config (in `deployments/`) — mirrors ZFS
```
deployments/
└── <customer>/
└── <site>/
├── site.yaml # declarative source of truth
├── docker-compose.yml # rendered from a site-templates profile
├── .env.example # non-secret defaults / references
└── secrets.enc.yaml # SOPS/age-encrypted secrets (committed)
```
`site.yaml` is the source of truth the CLI reads/writes; everything else is
rendered from it. It carries both ids:
```yaml
customer: acme
site: shop
profile: custom-php
domains: [shop.acme.com]
```
## 8. Reserved words
`customer` and `site` ids may not be: `proxy`, `platform`, `traefik`, `mariadb`,
`forgejo`, `monitoring`, `docker`, `customers`, `platform-infra`,
`site-templates`, `deployments`, `control-panel` (avoids collisions with
platform names).