Constructive Sites
A site is a routable web property: a hostname, a content origin (a bucket, an authored page set, or an installed service), and the serving rules in between. This skill covers the whole site surface from the application layer, through the generated SDK ORM: provisioning a bucket-backed static site in one call, deploying it as an immutable release with named previews and rollback, authoring Merkle-versioned pages, installing the platform's Mantra auth pages and content presets, and associating a mobile app with a host.
It intentionally does not cover the SQL, trigger, static-gateway or reconciler internals — those live in the sites-*, mantra-auth-pages and testing-static-gateway-planes skills in constructive-db. Every symbol below is taken from the generated @constructive-db/constructive-sdk ORM (the api target) or, where noted, the infra target.
When to Apply
Use this skill when:
- Provisioning a static site (docs, marketing, SPA) with a bucket origin and a hostname in one call
- Deploying a build as an immutable release, publishing it atomically, and rolling back by moving a pointer
- Minting a named preview URL (
<name>--<site>.<apex>) per branch and moving/retiring it
- Reading any historical release without publishing it (time travel)
- Authoring pages whose every write is versioned, and publishing a chosen version
- Installing the Mantra page set (sign-in/up/out, reset, 2FA, OAuth callback, legal, robots/sitemap/webmanifest) onto a site
- Seeding a site with a content preset (
pages:legal, robots:no-ai) without overwriting tenant edits
- Associating a mobile app with a host (AASA / assetlinks) and adding
/l/<slug> deep links
- Deciding between the tenant (
db.site) and platform (db.platformSite) surfaces
Not this skill: bucket and upload mechanics (constructive-storage), API-route/service deployment and DNS (constructive-platform), the login flows the Mantra pages drive (constructive-auth).
Two lanes, one surface
The ORM exposes the same site surface twice. The tenant (database) lane is the default: unprefixed models and operations whose rows carry a databaseId. The platform lane — on-prem / self-hosted, and the platform's own sites — is the identical surface with a platform prefix and no scope key.
| Tenant lane |
Platform lane |
db.site, db.siteRelease, db.page, db.route, db.domain, db.managedDomain |
db.platformSite, db.platformSiteRelease, db.platformPage, db.platformDomain, db.platformManagedDomain |
db.siteWebConfig, db.siteErrorPage, db.siteMetadatum, db.siteAppLink, db.siteDeepLink |
db.platformSiteWebConfig, db.platformSiteErrorPage, db.platformSiteMetadatum, db.platformSiteAppLink, db.platformSiteDeepLink |
db.mutation.sitesProvisionStaticSite |
db.mutation.platformSitesProvisionStaticSite |
db.mutation.provisionSitePreview / setSitePreview / deleteSitePreview |
db.mutation.platformProvisionSitePreview / platformSetSitePreview / platformDeleteSitePreview |
db.query.getSitePreviewCommit / getSiteReleaseManifest / pagePublished |
db.query.platformGetSitePreviewCommit / platformGetSiteReleaseManifest / platformPagePublished |
db.mutation.sitesInstallMantra / sitesInstallContentPreset |
db.mutation.platformSitesInstallMantra / platformSitesInstallContentPreset |
db.query.sitesDeepLinkUrl / sitesSiteOrigin / resolveSiteAppLinks |
db.query.platformSitesDeepLinkUrl / platformSitesSiteOrigin |
Rules of thumb:
- If an operation "doesn't exist", you are on the wrong lane — check for the
platform prefix.
- Tenant
create calls pass databaseId in data; platform calls do not.
- The platform lane checks the
manage_sites capability (NOT_AUTHORIZED otherwise); the tenant lane confines every read and write to the session's own database.
- Every model call takes a required
select. Prefer .unwrap() (returns data, throws on GraphQL error) over .execute() (returns { ok, data, errors }) — swallowing errors is how a failed deploy looks successful.
Client setup
import { createClient } from '@constructive-db/constructive-sdk';
const db = createClient({
endpoint: 'https://api.example.com/graphql',
headers: { Authorization: `Bearer ${token}` },
});
Quick start: a static site, deployed as a release
// 1. One call: public bucket + site + web config + hostname + route.
const { sitesProvisionStaticSite } = await db.mutation
.sitesProvisionStaticSite(
{
input: {
name: 'docs',
label: 'docs', // subdomain label under a published apex
siteConfig: { spa_fallback: true, not_found_path: '404.html' },
},
},
{ select: { result: { select: { id: true, path: true, targetSiteId: true, domainId: true } } } },
)
.unwrap();
const siteId = sitesProvisionStaticSite?.result?.targetSiteId!;
// 2. Upload each file to cas/sha256/<sha256> in the site's bucket
// (presigned upload — see constructive-storage), then commit one manifest.
const { createSiteRelease } = await db.siteRelease
.create({
data: {
siteId,
databaseId,
manifest: {
files: {
'index.html': { hash: '9a3f…', content_type: 'text/html', size: 2841 },
'assets/app.4f2c.js': { hash: '1be5…', content_type: 'application/javascript', size: 91043 },
},
file_count: 2,
total_bytes: 93884,
},
},
select: { id: true, commitId: true, storeId: true },
})
.unwrap();
const release = createSiteRelease.siteRelease;
if (!release.commitId || !release.storeId) throw new Error('release was not versioned');
// 3. Publish: one pointer move, atomic for every in-flight visitor.
await db.site
.update({
where: { id: siteId },
data: { activeCommitId: release.commitId },
select: { id: true, activeCommitId: true },
})
.unwrap();
Rollback is step 3 with an older commitId. A site whose activeCommitId is null serves straight from the bucket root (no release). Details, previews and time travel: static-sites.md, releases-and-previews.md.
Core models (tenant lane)
| Model |
Purpose |
Key fields |
db.site |
The web property |
id, name, title, description, bucketId, resourceId, installationId, activeCommitId, isPublished, databaseId |
db.siteWebConfig |
1:1 serving rules |
siteId, indexDocument, cleanUrls, spaFallback, metadata |
db.siteErrorPage |
Custom error documents |
siteId, statusCode, objectPath |
db.siteMetadatum |
Head/SEO facts |
siteId, title, description, canonicalUrl, favicon, logo, ogImage, appleTouchIcon, robots, robotsSeededFrom |
db.siteRelease |
One manifest row per site (unique on siteId); every write is a new commit |
siteId, manifest, commitId, storeId |
db.page |
Merkle-versioned authored content |
siteId, slug, content, commitId, storeId, seededFrom |
db.commit |
Version history of a store |
storeId, treeId, parentIds, message, date |
db.route |
Hostname + path → target |
domainId, path, method, priority, isActive, anonymous, targetSiteId, servingSiteId, targetServiceId, targetFunctionId, targetBucketId, targetApiId, targetRedirectId, previewRef |
db.domain |
A claimed hostname |
hostname, parentHostname, isPublished, isWildcard, managed, verificationStatus, tlsStatus, tlsReadyAt |
db.managedDomain |
A platform-managed apex under which labels are assigned |
domain, isWildcard, allowPublicUsage, verificationStatus, certStatus, tlsStatus |
db.siteAppLink |
Host-owned mobile association |
siteId, appStoreIdentityId, pathComponents, webcredentials |
db.siteDeepLink |
Named /l/<slug> link |
siteId, slug, webPath, fallbackUrl, appPath, pageId, metadata |
commitId / storeId are string | null in the generated types because a trigger stamps them; assert once after the write.
Custom operations
| Operation |
Input |
Returns |
Notes |
db.mutation.sitesProvisionStaticSite |
name, label?, apex?, hostname?, routePath?, siteConfig? |
Route |
Bucket + site + web config + hostname + route in one transaction |
db.mutation.domainsAssignSubdomain |
apex?, label?, maxAttempts? |
Domain |
Claim <label>.<apex> under a published apex; auto-generates a label when omitted |
db.mutation.provisionSitePreview |
siteId, name, commitId?, apex? |
Route |
Set the ref, claim <name>--<site>.<apex>, create the route; re-run moves the ref |
db.mutation.setSitePreview |
targetSiteId, targetName, targetCommitId? |
commit UUID |
Move a ref without touching routing; omitted commit pins the current head |
db.mutation.deleteSitePreview |
targetSiteId, targetName |
— |
Idempotent; the hostname keeps resolving and now 404s |
db.query.getSitePreviewCommit |
targetSiteId, targetName |
commit UUID or null |
Null, not an error, when the ref is gone |
db.query.getSiteReleaseManifest |
targetSiteId, targetCommitId |
manifest JSON |
Read any release as it was at that commit |
db.query.pagePublished |
targetSiteId, pageSlug |
page content |
The page as of the site's activeCommitId |
db.mutation.sitesInstallMantra |
siteId, routePreset? (default 'mantra'), entityId? |
Site |
Idempotent per (hostname, path) |
db.mutation.sitesInstallContentPreset |
siteId, presetKind, presetSlug, entityId? |
report JSON |
Insert-only; { seeded[], present[] } |
db.query.resolveSiteAppLinks |
targetSiteId |
JSON array |
Exactly what the .well-known documents will render |
db.query.sitesDeepLinkUrl |
targetSiteId, linkSlug |
URL |
The resolved destination of /l/<slug> |
db.query.sitesSiteOrigin |
targetSiteId |
origin string |
The site's public origin |
db.mutation.mintSitePreviewToken |
siteId, target, targetKind?, ttlSeconds? |
{ token, expiresAt } |
Signed bearer for a gated preview; the gateway accepts it as a query param or cookie |
db.query.verifySitePreviewToken |
siteId, token |
string or null |
Null when the token is invalid or expired |
All input fields are optional in the generated types; the server raises a structured error when a required one is missing.
Pages, Mantra, presets, app links — at a glance
// A page: every write commits; nothing is published until the site pointer moves.
const { createPage } = await db.page
.create({
data: { siteId, databaseId, slug: 'home', content: { title: 'v1' } },
select: { id: true, commitId: true, storeId: true },
})
.unwrap();
// Mantra auth/legal pages onto an already-routed site.
await db.mutation
.sitesInstallMantra({ input: { siteId } }, { select: { result: { select: { id: true, name: true } } } })
.unwrap();
// Seed legal pages and a no-AI robots policy — never overwrites what the site already has.
await db.mutation
.sitesInstallContentPreset({ input: { siteId, presetKind: 'pages', presetSlug: 'legal' } }, { select: { result: true } })
.unwrap();
await db.mutation
.sitesInstallContentPreset({ input: { siteId, presetKind: 'robots', presetSlug: 'no-ai' } }, { select: { result: true } })
.unwrap();
// A deep link under the reserved /l/ prefix.
await db.siteDeepLink
.create({
data: { siteId, databaseId, slug: 'welcome', webPath: '/getting-started', fallbackUrl: 'https://acme.com/welcome' },
select: { id: true, slug: true },
})
.unwrap();
Details: pages-and-content-presets.md, mantra.md, app-links.md.
Error codes
| Code |
Meaning |
SUBDOMAIN_APEX_NOT_PUBLISHED |
Label requested under an apex that is not a published wildcard managed domain (isWildcard && allowPublicUsage) |
SUBDOMAIN_LABEL_INVALID / SUBDOMAIN_LABEL_EXHAUSTED |
Label is not ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$ / no free label found within maxAttempts |
DOMAIN_ALREADY_CLAIMED |
Explicit hostname is already routed by another owner |
STATIC_SITES_LIMIT |
Plan cap on static sites reached (see constructive-billing) |
SITE_NOT_FOUND |
No such site in this scope |
SITE_PREVIEW_COMMIT_SITE_MISMATCH |
The commit belongs to another site's store — refs are store-local |
ROUTE_BINDINGS_SITE_NOT_ROUTED |
Mantra install on a site that serves no hostname yet — route it first |
ROUTE_BINDINGS_ENTITY_REQUIRED |
Entity-keyed scope, entityId omitted |
MANTRA_BINDINGS_INVALID / FUNCTION_DEFINITION_NOT_FOUND |
Route preset malformed / a mantra:* task is not published at this scope |
CONTENT_PRESET_KIND_UNSUPPORTED |
presetKind is not one the installer dispatches (pages, robots) |
NOT_AUTHORIZED |
Platform or entity lane without manage_sites |
Known SDK gaps
Document these as gaps — do not work around them with SQL:
- Listing a site's previews.
getSitePreviews(targetSiteId) exists in the GraphQL schema but the generated db.getSitePreviewsRecord.findMany cannot pass targetSiteId. Use getSitePreviewCommit for single-ref reads; list with a raw GraphQL query if you must. The create/update/delete methods on that record model are meaningless (it is a function result, not a table).
- App store identities. The app-owned half of mobile app links (
appStoreIdentity: platform, bundle/package id, team id, certificate fingerprints, store URL) has no model in the generated api ORM; only the host-owned siteAppLink does. Create the identity through whichever surface owns your app, then reference its id.
- Service-backed (SSR) sites. A site with
resourceId / installationId resolves to a running service at reconcile time, but the install_app verb that creates the definition → installation → service chain is not exposed by the generated ORM. Route a hostname at an existing service with db.route.create({ data: { domainId, path, targetServiceId } }) instead; see the sites-ssr-apps skill in constructive-db for the internals.
- Historical page reads. Reading a page's content at an arbitrary past commit (as opposed to the published one via
pagePublished) is a merkle-store read that the api ORM does not front; db.commit.findMany gives you the history, not the content.
Reference Files
| File |
Read when |
| static-sites.md |
Provisioning a bucket-backed site, siteConfig keys, apex publishing and subdomain assignment, custom hostnames, serving behaviour |
| releases-and-previews.md |
Release manifests, publish/rollback, deploy history, named previews, time travel |
| pages-and-content-presets.md |
Versioned pages, publishing a page version, pages:legal / robots:no-ai presets, provenance |
| mantra.md |
Installing the Mantra page set, the route_bindings catalog, mixing one hostname across targets, entity scope |
| app-links.md |
siteAppLink, the derived AASA / assetlinks documents, siteDeepLink and /l/<slug> |
Related Skills
1---2name: constructive-sites3description: Sites — provision and serve web properties through the SDK ORM: one-call bucket-backed static sites (sitesProvisionStaticSite), immutable release manifests and rollback (siteRelease, activeCommitId, getSiteReleaseManifest), named preview hostnames (provisionSitePreview, setSitePreview, getSitePreviewCommit, deleteSitePreview), Merkle-versioned pages (db.page, pagePublished), Mantra auth/legal pages (sitesInstallMantra), content presets (sitesInstallContentPreset — pages:legal, robots:no-ai), mobile app links and /l/ deep links (siteAppLink, siteDeepLink, resolveSiteAppLinks, sitesDeepLinkUrl), and the tenant-vs-platform naming rule (db.site vs db.platformSite). Use when asked to 'provision a static site', 'deploy a static build', 'release manifest', 'roll back a site', 'preview URL', 'provisionSitePreview', 'setSitePreview', 'time travel a release', 'publish a page version', 'install mantra', 'add login pages to a site', 'install legal pages', 'robots preset', 'content preset', 'universal links', 'assetlinks'4---56# Constructive Sites78A **site** is a routable web property: a hostname, a content origin (a bucket, an authored page set, or an installed service), and the serving rules in between. This skill covers the whole site surface from the application layer, through the generated **SDK ORM**: provisioning a bucket-backed static site in one call, deploying it as an immutable release with named previews and rollback, authoring Merkle-versioned pages, installing the platform's Mantra auth pages and content presets, and associating a mobile app with a host.910It intentionally does not cover the SQL, trigger, static-gateway or reconciler internals — those live in the `sites-*`, `mantra-auth-pages` and `testing-static-gateway-planes` skills in `constructive-db`. Every symbol below is taken from the generated `@constructive-db/constructive-sdk` ORM (the `api` target) or, where noted, the `infra` target.1112## When to Apply1314Use this skill when:15- Provisioning a **static site** (docs, marketing, SPA) with a bucket origin and a hostname in one call16- Deploying a build as an **immutable release**, publishing it atomically, and **rolling back** by moving a pointer17- Minting a **named preview URL** (`<name>--<site>.<apex>`) per branch and moving/retiring it18- Reading **any historical release** without publishing it (time travel)19- Authoring **pages** whose every write is versioned, and publishing a chosen version20- Installing the **Mantra** page set (sign-in/up/out, reset, 2FA, OAuth callback, legal, robots/sitemap/webmanifest) onto a site21- Seeding a site with a **content preset** (`pages:legal`, `robots:no-ai`) without overwriting tenant edits22- Associating a **mobile app** with a host (AASA / assetlinks) and adding `/l/<slug>` **deep links**23- Deciding between the **tenant** (`db.site`) and **platform** (`db.platformSite`) surfaces2425Not this skill: bucket and upload mechanics ([`constructive-storage`](../constructive-storage/SKILL.md)), API-route/service deployment and DNS ([`constructive-platform`](../constructive-platform/SKILL.md)), the login flows the Mantra pages drive ([`constructive-auth`](../constructive-auth/SKILL.md)).2627## Two lanes, one surface2829The ORM exposes the same site surface twice. The **tenant (database) lane** is the default: unprefixed models and operations whose rows carry a `databaseId`. The **platform lane** — on-prem / self-hosted, and the platform's own sites — is the identical surface with a `platform` prefix and no scope key.3031| Tenant lane | Platform lane |32|---|---|33| `db.site`, `db.siteRelease`, `db.page`, `db.route`, `db.domain`, `db.managedDomain` | `db.platformSite`, `db.platformSiteRelease`, `db.platformPage`, `db.platformDomain`, `db.platformManagedDomain` |34| `db.siteWebConfig`, `db.siteErrorPage`, `db.siteMetadatum`, `db.siteAppLink`, `db.siteDeepLink` | `db.platformSiteWebConfig`, `db.platformSiteErrorPage`, `db.platformSiteMetadatum`, `db.platformSiteAppLink`, `db.platformSiteDeepLink` |35| `db.mutation.sitesProvisionStaticSite` | `db.mutation.platformSitesProvisionStaticSite` |36| `db.mutation.provisionSitePreview` / `setSitePreview` / `deleteSitePreview` | `db.mutation.platformProvisionSitePreview` / `platformSetSitePreview` / `platformDeleteSitePreview` |37| `db.query.getSitePreviewCommit` / `getSiteReleaseManifest` / `pagePublished` | `db.query.platformGetSitePreviewCommit` / `platformGetSiteReleaseManifest` / `platformPagePublished` |38| `db.mutation.sitesInstallMantra` / `sitesInstallContentPreset` | `db.mutation.platformSitesInstallMantra` / `platformSitesInstallContentPreset` |39| `db.query.sitesDeepLinkUrl` / `sitesSiteOrigin` / `resolveSiteAppLinks` | `db.query.platformSitesDeepLinkUrl` / `platformSitesSiteOrigin` |4041Rules of thumb:42- If an operation "doesn't exist", you are on the wrong lane — check for the `platform` prefix.43- Tenant `create` calls pass `databaseId` in `data`; platform calls do not.44- The platform lane checks the `manage_sites` capability (`NOT_AUTHORIZED` otherwise); the tenant lane confines every read and write to the session's own database.45- Every model call takes a required `select`. Prefer `.unwrap()` (returns data, throws on GraphQL error) over `.execute()` (returns `{ ok, data, errors }`) — swallowing `errors` is how a failed deploy looks successful.4647## Client setup4849```typescript50import { createClient } from '@constructive-db/constructive-sdk';5152const db = createClient({53 endpoint: 'https://api.example.com/graphql',54 headers: { Authorization: `Bearer ${token}` },55});56```5758## Quick start: a static site, deployed as a release5960```typescript61// 1. One call: public bucket + site + web config + hostname + route.62const { sitesProvisionStaticSite } = await db.mutation63 .sitesProvisionStaticSite(64 {65 input: {66 name: 'docs',67 label: 'docs', // subdomain label under a published apex68 siteConfig: { spa_fallback: true, not_found_path: '404.html' },69 },70 },71 { select: { result: { select: { id: true, path: true, targetSiteId: true, domainId: true } } } },72 )73 .unwrap();74const siteId = sitesProvisionStaticSite?.result?.targetSiteId!;7576// 2. Upload each file to cas/sha256/<sha256> in the site's bucket77// (presigned upload — see constructive-storage), then commit one manifest.78const { createSiteRelease } = await db.siteRelease79 .create({80 data: {81 siteId,82 databaseId,83 manifest: {84 files: {85 'index.html': { hash: '9a3f…', content_type: 'text/html', size: 2841 },86 'assets/app.4f2c.js': { hash: '1be5…', content_type: 'application/javascript', size: 91043 },87 },88 file_count: 2,89 total_bytes: 93884,90 },91 },92 select: { id: true, commitId: true, storeId: true },93 })94 .unwrap();95const release = createSiteRelease.siteRelease;96if (!release.commitId || !release.storeId) throw new Error('release was not versioned');9798// 3. Publish: one pointer move, atomic for every in-flight visitor.99await db.site100 .update({101 where: { id: siteId },102 data: { activeCommitId: release.commitId },103 select: { id: true, activeCommitId: true },104 })105 .unwrap();106```107108Rollback is step 3 with an older `commitId`. A site whose `activeCommitId` is null serves straight from the bucket root (no release). Details, previews and time travel: [static-sites.md](./references/static-sites.md), [releases-and-previews.md](./references/releases-and-previews.md).109110## Core models (tenant lane)111112| Model | Purpose | Key fields |113|---|---|---|114| `db.site` | The web property | `id`, `name`, `title`, `description`, `bucketId`, `resourceId`, `installationId`, `activeCommitId`, `isPublished`, `databaseId` |115| `db.siteWebConfig` | 1:1 serving rules | `siteId`, `indexDocument`, `cleanUrls`, `spaFallback`, `metadata` |116| `db.siteErrorPage` | Custom error documents | `siteId`, `statusCode`, `objectPath` |117| `db.siteMetadatum` | Head/SEO facts | `siteId`, `title`, `description`, `canonicalUrl`, `favicon`, `logo`, `ogImage`, `appleTouchIcon`, `robots`, `robotsSeededFrom` |118| `db.siteRelease` | One manifest row per site (unique on `siteId`); every write is a new commit | `siteId`, `manifest`, `commitId`, `storeId` |119| `db.page` | Merkle-versioned authored content | `siteId`, `slug`, `content`, `commitId`, `storeId`, `seededFrom` |120| `db.commit` | Version history of a store | `storeId`, `treeId`, `parentIds`, `message`, `date` |121| `db.route` | Hostname + path → target | `domainId`, `path`, `method`, `priority`, `isActive`, `anonymous`, `targetSiteId`, `servingSiteId`, `targetServiceId`, `targetFunctionId`, `targetBucketId`, `targetApiId`, `targetRedirectId`, `previewRef` |122| `db.domain` | A claimed hostname | `hostname`, `parentHostname`, `isPublished`, `isWildcard`, `managed`, `verificationStatus`, `tlsStatus`, `tlsReadyAt` |123| `db.managedDomain` | A platform-managed apex under which labels are assigned | `domain`, `isWildcard`, `allowPublicUsage`, `verificationStatus`, `certStatus`, `tlsStatus` |124| `db.siteAppLink` | Host-owned mobile association | `siteId`, `appStoreIdentityId`, `pathComponents`, `webcredentials` |125| `db.siteDeepLink` | Named `/l/<slug>` link | `siteId`, `slug`, `webPath`, `fallbackUrl`, `appPath`, `pageId`, `metadata` |126127`commitId` / `storeId` are `string | null` in the generated types because a trigger stamps them; assert once after the write.128129## Custom operations130131| Operation | Input | Returns | Notes |132|---|---|---|---|133| `db.mutation.sitesProvisionStaticSite` | `name`, `label?`, `apex?`, `hostname?`, `routePath?`, `siteConfig?` | `Route` | Bucket + site + web config + hostname + route in one transaction |134| `db.mutation.domainsAssignSubdomain` | `apex?`, `label?`, `maxAttempts?` | `Domain` | Claim `<label>.<apex>` under a published apex; auto-generates a label when omitted |135| `db.mutation.provisionSitePreview` | `siteId`, `name`, `commitId?`, `apex?` | `Route` | Set the ref, claim `<name>--<site>.<apex>`, create the route; re-run moves the ref |136| `db.mutation.setSitePreview` | `targetSiteId`, `targetName`, `targetCommitId?` | commit UUID | Move a ref without touching routing; omitted commit pins the current head |137| `db.mutation.deleteSitePreview` | `targetSiteId`, `targetName` | — | Idempotent; the hostname keeps resolving and now 404s |138| `db.query.getSitePreviewCommit` | `targetSiteId`, `targetName` | commit UUID or null | Null, not an error, when the ref is gone |139| `db.query.getSiteReleaseManifest` | `targetSiteId`, `targetCommitId` | manifest JSON | Read any release as it was at that commit |140| `db.query.pagePublished` | `targetSiteId`, `pageSlug` | page content | The page as of the site's `activeCommitId` |141| `db.mutation.sitesInstallMantra` | `siteId`, `routePreset?` (default `'mantra'`), `entityId?` | `Site` | Idempotent per (hostname, path) |142| `db.mutation.sitesInstallContentPreset` | `siteId`, `presetKind`, `presetSlug`, `entityId?` | report JSON | Insert-only; `{ seeded[], present[] }` |143| `db.query.resolveSiteAppLinks` | `targetSiteId` | JSON array | Exactly what the `.well-known` documents will render |144| `db.query.sitesDeepLinkUrl` | `targetSiteId`, `linkSlug` | URL | The resolved destination of `/l/<slug>` |145| `db.query.sitesSiteOrigin` | `targetSiteId` | origin string | The site's public origin |146| `db.mutation.mintSitePreviewToken` | `siteId`, `target`, `targetKind?`, `ttlSeconds?` | `{ token, expiresAt }` | Signed bearer for a gated preview; the gateway accepts it as a query param or cookie |147| `db.query.verifySitePreviewToken` | `siteId`, `token` | string or null | Null when the token is invalid or expired |148149All `input` fields are optional in the generated types; the server raises a structured error when a required one is missing.150151## Pages, Mantra, presets, app links — at a glance152153```typescript154// A page: every write commits; nothing is published until the site pointer moves.155const { createPage } = await db.page156 .create({157 data: { siteId, databaseId, slug: 'home', content: { title: 'v1' } },158 select: { id: true, commitId: true, storeId: true },159 })160 .unwrap();161162// Mantra auth/legal pages onto an already-routed site.163await db.mutation164 .sitesInstallMantra({ input: { siteId } }, { select: { result: { select: { id: true, name: true } } } })165 .unwrap();166167// Seed legal pages and a no-AI robots policy — never overwrites what the site already has.168await db.mutation169 .sitesInstallContentPreset({ input: { siteId, presetKind: 'pages', presetSlug: 'legal' } }, { select: { result: true } })170 .unwrap();171await db.mutation172 .sitesInstallContentPreset({ input: { siteId, presetKind: 'robots', presetSlug: 'no-ai' } }, { select: { result: true } })173 .unwrap();174175// A deep link under the reserved /l/ prefix.176await db.siteDeepLink177 .create({178 data: { siteId, databaseId, slug: 'welcome', webPath: '/getting-started', fallbackUrl: 'https://acme.com/welcome' },179 select: { id: true, slug: true },180 })181 .unwrap();182```183184Details: [pages-and-content-presets.md](./references/pages-and-content-presets.md), [mantra.md](./references/mantra.md), [app-links.md](./references/app-links.md).185186## Error codes187188| Code | Meaning |189|---|---|190| `SUBDOMAIN_APEX_NOT_PUBLISHED` | Label requested under an apex that is not a published wildcard managed domain (`isWildcard && allowPublicUsage`) |191| `SUBDOMAIN_LABEL_INVALID` / `SUBDOMAIN_LABEL_EXHAUSTED` | Label is not `^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$` / no free label found within `maxAttempts` |192| `DOMAIN_ALREADY_CLAIMED` | Explicit `hostname` is already routed by another owner |193| `STATIC_SITES_LIMIT` | Plan cap on static sites reached (see [`constructive-billing`](../constructive-billing/SKILL.md)) |194| `SITE_NOT_FOUND` | No such site in this scope |195| `SITE_PREVIEW_COMMIT_SITE_MISMATCH` | The commit belongs to another site's store — refs are store-local |196| `ROUTE_BINDINGS_SITE_NOT_ROUTED` | Mantra install on a site that serves no hostname yet — route it first |197| `ROUTE_BINDINGS_ENTITY_REQUIRED` | Entity-keyed scope, `entityId` omitted |198| `MANTRA_BINDINGS_INVALID` / `FUNCTION_DEFINITION_NOT_FOUND` | Route preset malformed / a `mantra:*` task is not published at this scope |199| `CONTENT_PRESET_KIND_UNSUPPORTED` | `presetKind` is not one the installer dispatches (`pages`, `robots`) |200| `NOT_AUTHORIZED` | Platform or entity lane without `manage_sites` |201202## Known SDK gaps203204Document these as gaps — do not work around them with SQL:205206- **Listing a site's previews.** `getSitePreviews(targetSiteId)` exists in the GraphQL schema but the generated `db.getSitePreviewsRecord.findMany` cannot pass `targetSiteId`. Use `getSitePreviewCommit` for single-ref reads; list with a raw GraphQL query if you must. The `create`/`update`/`delete` methods on that record model are meaningless (it is a function result, not a table).207- **App store identities.** The app-owned half of mobile app links (`appStoreIdentity`: platform, bundle/package id, team id, certificate fingerprints, store URL) has no model in the generated `api` ORM; only the host-owned `siteAppLink` does. Create the identity through whichever surface owns your app, then reference its id.208- **Service-backed (SSR) sites.** A site with `resourceId` / `installationId` resolves to a running service at reconcile time, but the `install_app` verb that creates the definition → installation → service chain is not exposed by the generated ORM. Route a hostname at an existing service with `db.route.create({ data: { domainId, path, targetServiceId } })` instead; see the `sites-ssr-apps` skill in `constructive-db` for the internals.209- **Historical page reads.** Reading a page's content at an arbitrary past commit (as opposed to the published one via `pagePublished`) is a merkle-store read that the `api` ORM does not front; `db.commit.findMany` gives you the history, not the content.210211## Reference Files212213| File | Read when |214|---|---|215| [static-sites.md](./references/static-sites.md) | Provisioning a bucket-backed site, `siteConfig` keys, apex publishing and subdomain assignment, custom hostnames, serving behaviour |216| [releases-and-previews.md](./references/releases-and-previews.md) | Release manifests, publish/rollback, deploy history, named previews, time travel |217| [pages-and-content-presets.md](./references/pages-and-content-presets.md) | Versioned pages, publishing a page version, `pages:legal` / `robots:no-ai` presets, provenance |218| [mantra.md](./references/mantra.md) | Installing the Mantra page set, the `route_bindings` catalog, mixing one hostname across targets, entity scope |219| [app-links.md](./references/app-links.md) | `siteAppLink`, the derived AASA / assetlinks documents, `siteDeepLink` and `/l/<slug>` |220221## Related Skills222223- [`constructive-storage`](../constructive-storage/SKILL.md) — presigned uploads into the site's bucket224- [`constructive-platform`](../constructive-platform/SKILL.md) — services, API routing, deployment, domains225- [`constructive-auth`](../constructive-auth/SKILL.md) — the sign-in/2FA/reset behaviour the Mantra pages drive226- [`constructive-flow-graphs`](../constructive-flow-graphs/SKILL.md) — the merkle store that versions releases and pages227- [`constructive-billing`](../constructive-billing/SKILL.md) — `STATIC_SITES_LIMIT` and other plan caps