Build MCP
Build cookbook: Viewer-scoped customer portal
Golden example `viewer-scoped-customer-portal` from the Build cookbook: what it builds, the template it starts from and the boundary it keeps.
Viewer-scoped customer portal
Example user request
"Give each signed-in tenant customer a small portal showing only their own current account status."
Fit and template
This is the narrow viewer-scoped, read-only binding path: the data is already exposed as customers, every returned row can be matched to the signed-in viewer's email, and every descriptor field returned by that resource is safe for that viewer. Start from scripts/artifacts/templates/viewer-scoped-dashboard.
Do not use this specimen for staff-wide search, arbitrary row-level rules, joins, writes, or a resource containing fields that must not reach the matched viewer. Those require a bounded dataset, an approved backend, runtime projection enforcement, or platform-service work.
Topology and contract decisions
/is tenant-viewable for signed-in tenant users;viewerPolicy: "tenant"admits the page before row filtering.- The live
viewerbinding requirescustomers.emailto equal the signed-in viewer email. Never accept an email or customer ID parameter as an authorization substitute. - The current connector path returns its descriptor row after filtering. Manifest
projectionis not a field-level security boundary, so this example omits it and requires the entire descriptor to be safe. Render an explicit UI allowlist anyway. - Viewer bindings are served
no-store. Display returned source freshness/warnings rather than claiming a TTL guarantee. - This is read-only and has no app-owned state, datasets, schedules, secrets, or background work.
Token bootstrap and relative fetch
Preserve the blessed template's token bootstrap. The platform proxy supplies x-artifact-binding-tokens to the server handler; the handler parses the customers token and injects only that serialized value into the page. The browser uses it on the relative request:
function customerFetch(customersToken) {
return fetch("./_artifact/data/customers", {
headers: { "x-artifact-binding-token": customersToken }
});
}
Do not invent a client token endpoint or log the token. Map 403 viewer_policy_no_authorized_rows for a signed-in nonmatching viewer to No customer record is linked to this email; never broaden the query or substitute example rows.
Validation and controls
- Confirm current source metadata has
customers.email, identity semantics match the platform viewer email, and every returned descriptor field is approved for viewer exposure. - Test a matching viewer receives
200with only their rows, a nonmatching viewer receives403 viewer_policy_no_authorized_rows, and no session receives401. - Run the template tests and
node scripts/artifacts/validate-local --dir <appDir>from the repository root. - Complete commit, source sync, draft deployment, rendered smoke, validation preparation, and validation request. Prove the token bootstrap without logging tokens, no sample rows, and useful loading, no-record, freshness, and unavailable states.
Limitations and graduation boundary
Manifest v2 supports this narrow customers/email viewer filter only. Field minimization, multi-resource joins, other ownership rules, writes, invitations, or public access require an explicitly reviewed backend or platform change.