The Platform BFF layer exposes a renderer-agnostic UI contract (bff.v1).
Every user-facing surface must be renderable from a BFF model. Renderers do not
own business rules, persistence, permissions, or resource-specific mutations.
Default architecture is BFF contract + renderer adapter, not a specific frontend stack. Templ, React/Vite, Next, Vue, MPA, SSR, SSG, and islands are adapter choices; Platform core stays stack-neutral.
module -> panel/toolset -> BFF runtime -> PageModel -> renderer
BFF owns permissions, validation, navigation, session projection, action descriptors, and hydrated data shape before any renderer runs. Renderers map the model to HTML or client UI only.
Two deliveries of the same model:
| Delivery | Route | Consumer |
|---|---|---|
| In-process HTML | GET /go-admin/{path...} |
Templ reference renderer (bff.PageModel Go structs) |
| HTTP JSON | GET /bff/screens/{path...} |
React/Vite, Next, Vue, PHP, mobile, automation clients |
Templ is the primary reference renderer, not a privileged path: it must not
bypass BFF for capability checks, validation, action paths, or alternate screen
shape. Vite, Next, Vue, PHP, and other clients consume the same bff.v1
fields over /bff/*.
Use hydration precisely:
| Kind | Owner | Meaning |
|---|---|---|
| Data/model hydration | BFF (Hydrator) |
Fill ScreenModel.Rows, Pagination, FormRecord, validation state, action metadata |
| UI hydration | Renderer adapter | React hydrate, islands boot, partial client behavior, SSG enhancement |
BFF owns model hydration only. UI hydration belongs to renderer adapters, not
pkg/bff.
BFF is not admin-only. The same bff.v1 contract applies across surfaces;
profiles and product apps choose which surfaces are mounted and which route bases
they use.
| Surface | Purpose | Example path (product-owned) |
|---|---|---|
admin |
Operator / control-plane UI | /go-admin/posts, /go-admin/crm/leads |
public |
Public-facing pages | /, /posts/{slug} (AppCMS public) |
portal |
Authenticated external-user UI | /portal/account, CRM client dashboard |
embedded |
Widget or iframe-like UI | status widget, lead capture form |
mobile |
JSON-first app model | mobile task list via /bff/screens/... |
automation |
CLI, LLM, bot clients | action catalog, screen data, validation envelopes |
Rules:
- Platform defines the shared model contract (
PageModel,ScreenModel, actions). - Profiles own route bases (
AdminBase,APIBase,PublicBaseinprofile.Profile). - Product apps/modules own surface policy, hydration, executors, and special screens.
- Public and portal are not CMS-only; CRM, monitoring, support, and SaaS apps use the same pattern.
- Mobile and automation clients may consume BFF JSON without a browser renderer.
Optional ScreenModel.metadata may carry surface hints (surface, visibility,
list_api, form_action). Platform does not hardcode public/portal domain behavior.
See API surfaces for REST vs BFF vs HTML routing.
PageModel is the unified entry contract for user-facing UI:
| Part | Type | Role |
|---|---|---|
title |
string | Page title |
shell |
ShellModel |
Product chrome: title, workspace, admin/api bases |
navigation |
NavigationModel |
Capability-filtered nav items |
screen |
render.ScreenModel |
View type, columns, fields, rows, actions, metadata |
ShellModel fields: title, product, workspace, admin_base, api_base,
description.
NavigationModel contains items[] with href, label, and optional
capability.
SessionModel (via /bff/session) exposes a safe subset: contract version,
authenticated state, principal_id, profile_id, and sorted capability ids.
No secrets or tokens.
Product apps mount these routes (AppCMS, AppCRM):
GET /bff/screens/{path...} -> ScreenModel (hydrated when hydrator is active)
GET /bff/nav -> NavigationModel
GET /bff/session -> SessionModel
POST /bff/actions/{actionId} -> ActionResult | ValidationModel (Stage 6, AppCMS posts proof)
Path encoding: logical screen path without leading slash in the URL segment
(for example go-admin/posts, or a product-defined public/portal path when wired).
| Surface | Example screen | BFF JSON path |
|---|---|---|
| Admin (proof) | AppCMS posts list | GET /bff/screens/go-admin/posts |
| Admin (proof) | AppCRM leads list | GET /bff/screens/go-admin/crm/leads |
| Public / portal / embedded | Product-defined | GET /bff/screens/{product-path...} when mounted |
- Unauthenticated:
401JSON ({"error":"unauthorized"}), not a login redirect. - Missing admin/CRM capability:
403JSON ({"error":"forbidden"}). - Same principal resolution as
/go-admin/*(session cookie, AppCMS Bearer token).
For proof screens, the JSON body of /bff/screens/... must match the
ScreenModel produced in-process by registry.Page(...) — the same model Templ
renders. Contract tests in AppCMS and AppCRM enforce this.
github.com/fastygo/platform/pkg/bff:
| Component | Role |
|---|---|
Resolver |
Path -> ScreenModel from bindings + special screens |
ScreenHydrator |
Product-owned data load into rows, pagination, form values |
Navigator |
Capability-filtered navigation from panel resources |
PageRuntime |
Composes resolver + hydrator + navigator into PageModel |
ActionRegistry / ExecutorRegistry |
Descriptor registry + product dispatch |
ActionResult / ValidationModel |
Mutation response envelope |
Contract version: bff.v1 (bff.Version).
For bff.v1, render.ScreenModel is the screen ABI only at the renderer-visible
JSON boundary. The frozen contract is the combination of JSON Schema, TypeScript
types, and golden fixtures. In particular, panel.Column and panel.Field payloads
keep their Go-derived PascalCase keys (ID, Label, Options, Relation, ...).
Any future DTO split must preserve v1 JSON compatibility or ship as bff.v2.
Public API freeze: see Compatibility policy and schema/bff/v1/README.md.
Apps keep a thin internal/appschema registry that:
- builds
bff.PageRuntimefrom module descriptors; - registers product-specific special screens (settings, kanban, dashboard);
- exposes
Page(ctx, path, principal)andDashboardPage(ctx, principal).
pkg/app admin HTML routes call registry.Page and render Templ.
pkg/app/bff.go exposes the same runtime over JSON.
web/react is a compatibility proof, not the default Platform renderer.
It demonstrates JSON BFF parity with the Templ reference renderer for proof
screens. See web/react/README.md for dev setup (Vite proxy -> Go /bff/*).
POST /bff/actions/{actionId} dispatches by ActionDescriptor metadata.
Product apps register executors; Platform owns contract types only.
AppCMS proof actions:
| Action | Path | Capability | Token scope |
|---|---|---|---|
| Create post | POST /bff/actions/post.create |
content.write |
admin.content.write |
| Update post | POST /bff/actions/post.update?id={id} |
content.write |
admin.content.write |
| Trash post | POST /bff/actions/post.trash?id={id} |
content.write |
admin.content.write |
Responses:
200+ActionResult(ok, optionalredirect,data)422+ValidationModelfor invalid input401/403for auth, capability, or token failures404for unknown action ids
Post form screens set metadata.form_action to the BFF action path and
metadata.action_scope for scoped action_token injection. Codex REST
(/go-json/*) remains unchanged for integrations.
Dashboard screens use view: "dashboard" with optional sections[] and
widgets[] on ScreenModel. Platform promotes only shared primitives; product
domain stays in metadata until reuse is proven.
| Primitive | JSON field | Role |
|---|---|---|
SectionModel |
sections[] |
Layout container with titled widget grid |
WidgetModel |
sections[].widgets[] or widgets[] |
Typed widget slot |
MetricModel |
widget.metric |
KPI tile (label, value, optional unit, trend) |
StatusModel |
widget.status |
Health/state badge (status, message) |
ChartModel |
widget.chart |
Chart summary (chart_type, series[]) |
ActivityFeedModel |
widget.activity |
Timeline/incident list |
Widget types: metric, status, chart, activity.
Proof dashboard fixtures (no Platform import of product modules):
| Fixture | Product flavor |
|---|---|
monitoring-sites-dashboard.json |
Monitoring sites fleet overview |
appcrm-pipeline-dashboard.json |
CRM pipeline metrics |
appcms-content-health-dashboard.json |
CMS content health |
Templ (DashboardScreen) and React (DashboardView) render metric and status
widgets from these fixtures. Platform must not import monitoring/CMS/CRM app
packages to prove the contract.
Canonical proof-screen JSON lives in pkg/conformance/testdata/bff/v1/:
appcms-posts-table.jsonappcrm-leads-table.jsonappcms-post-edit-form.json
pkg/conformance/bffparity loads fixtures, normalizes volatile metadata
(action_token is stripped), and asserts strict ScreenModel roundtrip stability.
AppCMS and AppCRM HTTP tests compare /bff/screens/* output to the same fixtures.
Run the regression gate:
./scripts/bff-parity.shCI workflow: .github/workflows/bff-parity.yml.
Stable renderer-facing exports for the bff.v1 baseline:
| Artifact | Path |
|---|---|
| Go page/shell/nav/session | pkg/bff |
| Go screen model | pkg/render |
| JSON Schema | schema/bff/v1/ (screen, page, navigation, session, actions) |
| TypeScript contract | web/contract (@fastygo/bff-contract) |
| Golden fixtures | pkg/conformance/testdata/bff/v1/*.json |
| Adapter proofs | examples/renderer-adapters |
Proof screens: AppCMS posts table, AppCMS post edit form, AppCRM leads table.
Go conformance validates golden fixtures against screen.schema.json
(pkg/conformance/schema_test.go). Third-party renderers can implement proof
screens from schema + fixtures alone. See
renderer-adapter-authoring.md.
- Not codex REST — use
/go-json/*for integrations and headless data. - Not a renderer — adapters map
PageModelto HTML or client UI only.
- API surfaces —
/go-json/*vs/bff/*vs/go-admin/* - Frontend adapters — renderer adapter guide (Vite/Next examples)
- Custom screens — app/module-owned views
- Renderers — renderer-agnostic runtime and parity
- Templ renderer — reference in-process renderer
- Contract spec:
.project/specs/bff-v1-screen-schema.md - Strategic roadmap:
.project/roadmab-fullstack-bff.md - Implementation roadmap:
.project/roadmap-bff-model.md