Documentation source
Documentation source
Documentation source
Tenant-owned public sites, exact hostname routing, pages, and immutable publication snapshots
# Public Sites Amble can store multiple public sites for one tenant. Each site owns its metadata, pages, and immutable numbered publication snapshots; one shared renderer serves the site selected from the request hostname. ## Domain routing `tenant_domains.site_id` optionally binds a hostname to an exact tenant site. Existing domain rows keep `site_id = null`, which resolves the tenant's published default site exactly as before. The public resolver returns a site only when all of these conditions hold: - the hostname has an `active` domain row; - a non-null binding points to a site owned by the same tenant; - the exact bound site is published; or, for a null binding, the tenant's default site is published. Invalid, pending, unverified, foreign, missing, or unpublished bindings return no row. A non-null invalid binding never falls back to another site. Middleware strips incoming tenant/site/render headers, resolves the hostname, then stamps the database-derived tenant slug and site ID. The page, Open Graph image, and sitemap readers re-check the exact `(tenant_id, site_id, status = published)` tuple before reading pages or snapshots. ## Canonical writes `features/sites/server/site-writer.ts` is the single server-side mutation seam for: - creating or updating site metadata; - creating or updating draft pages; - validating URL paths and kind-specific source references; - publishing selected pages as a numbered immutable snapshot. The minimal authenticated endpoint is `POST /api/admin/sites`. Its discriminated operations are `create`, `update`, `upsertPage`, and `publish`. Tenant and publisher identities always come from the auth adapter, never the request body. Callers require `sites.team.create` or `sites.team.update`. Publication runs through `publish_tenant_site(...)`, which locks the site row before allocating the next version. Site publication, selected-page publication, and snapshot insertion occur in one database transaction and roll back together on any error. ## Source safety Page sources follow the current renderer contract: - `view` references a tenant-owned View UUID; - `entity` is rejected by the canonical writer until the renderer becomes entity-native; - `collection` references an available entity-type slug; - `mdx` is restricted to the current tenant's `content/sites/<tenant>/` subtree; - `offer` accepts a bare offer slug or the current tenant slug as its only prefix; - `home`, `custom`, and `landing` content comes from a schema-validated serializable View in the publication snapshot. Raw React, TSX, scripts, and HTML markup are not accepted by the publisher. Successful binding and publication operations evict affected hostnames from the current resolver process. Other edge isolates converge within the resolver's 60-second TTL; a shared invalidation channel is a follow-up rather than a second cache system in this slice. ## Foundation boundary This phase does not generate content, activate real domains, create per-site repositories/deployments, route leads, or define canonical-domain/alias redirects. Those remain later work after exact host isolation and publication safety are proven.