Appearance
Surfaces & Widgets
The surface plane is Atelier's "what people experience" layer. It projects your data (entities) and your logic (actions) into running staff-admin and citizen-portal experiences — without writing UI code.
You describe an experience as an ordered list of blocks. Each block is a declarative pointer: a widget intent, a data binding, and a placement. A single, presentation-only renderer takes those blocks and dispatches each one to the right widget. Both the admin app and the citizen portal import the same renderer, so an experience composed once behaves identically everywhere.
This is the compound-software idea made concrete:
- Any entity renders in any widget.
- Any action triggers from any surface.
- The widget catalog grows without breaking existing compositions.
How an experience is composed
A surface is an ordered list of blocks. A block stores only a pointer — never baked-in data. Rows are fetched at render time through the host's own secure data lane, so a page is always current and never ships stale snapshots.
Each block carries:
| Part | What it does |
|---|---|
| Widget intent | Which kind of widget to render (list, map, KPI, form, chart…) |
| Binding | What to render and how to scope it — entity or action, plus filter, limit, order, and relation expansion |
| Placement | Which slot the block occupies, and optional grid coordinates |
| Presentation props | Per-widget options, importance, and locale |
Because every widget receives the same uniform contract — data, schema, action definition, props, and locale — one dispatcher can mount any widget, and new widgets slot in without touching the ones already shipped.
The widget catalog
Atelier ships a governed catalog of widget kinds. Each one is a self-contained, reusable component that knows how to present a particular shape of data or interaction:
- Data display — list, detail, feed, lookup, owned-list
- Spatial & temporal — map, calendar
- Input & action — form, multi-step forms, actions, vote, consent, subscriptions
- Metrics — KPI cards, status, aggregate charts
- Civic & delivery — deliveries, status trackers
- Marketing & content — hero, feature grid, call-to-action, media + text, steps, testimonial, FAQ, logo strip, media gallery
The catalog is the growth vector. Adding a new widget is an additive operation — register it once and it becomes available to every surface, on every host, without changing the renderer or any existing composition.
Page templates
Page templates are a shared catalog of reusable page shapes — landing, service flow, browse, detail, tracker, results, content, account, and error. Each template defines named, ordered slots with built-in accessibility and structural guarantees baked in (for example, exactly one primary heading per page).
Templates are shared platform vocabulary: every application can reference any template immediately, and structural rules are validated centrally so a malformed page can't ship. A tenant binds a route to a template and supplies a sparse set of content overrides — or, when it needs more freedom, an embedded custom slot tree for that one page.
Dashboards
Dashboards are authored, not coded. From the application settings, you assemble a dashboard out of KPI cards, charts, and analytics embeds — adding, reordering, and removing blocks visually.
Chart types come from the live widget registry, so the options you can pick are always exactly the chart widgets the platform actually ships — no free-text, no broken references. A live preview renders each draft through the very same path as the published dashboard, so what you see while authoring is what your users get.
One shared renderer
The defining property of the surface plane is that there is exactly one renderer, and it is deliberately presentation-only.
- Both hosts use it. The admin app and the citizen portal import the same engine and the same widget catalog.
- Presentation stays separate from authoring. The renderer carries no editing metadata. Per-widget authoring configuration lives in a separate host layer, keyed to the same registry, following the same pattern modern visual builders use. The result: the rendering library stays clean and reusable, and authoring evolves independently.
- Preview equals live by construction. Authoring previews and published pages flow through one shared builder, so they can't drift apart.
A new host can join the model by implementing a data provider over its own security-scoped lane and reusing the renderer unchanged.
Security & isolation
Surfaces are safe by construction:
- Trusted identifiers only. The widget that reaches the page is always resolved from the platform's known-good registry — never directly from authored input. Operators compose surfaces; they can't inject arbitrary components.
- Fail-closed rendering. A binding that can't be resolved never fetches. A structural slot that can't load surfaces a clear error with the correct HTTP status; an optional slot simply empties. A page never renders a broken shell as if it were healthy.
- Per-host data boundaries. Each host owns its own data provider. The public, auth-optional lane and the authenticated operator lane are never shared — the admin provider refuses to operate over citizen lanes, and vice versa. Tenant identity is always resolved from the authenticated caller.
The template-and-fork model
Surfaces inherit Atelier's tenancy model cleanly:
- Shared vocabulary — page templates, action surfaces, and the widget registry are global. Every tenant reads the same catalog; there's nothing to copy.
- A template application — dashboards and portal pages are authored once on a template application, the canonical starting point for new tenants.
- Per-tenant copies — when a tenant is created, the template's dashboards and portal pages are forked into their own space and re-wired automatically. From then on, a tenant admin edits their copy with the exact same editors — no special-case code, because every authoring endpoint is keyed to the caller's resolved tenant.
The portal serves every page through a single dynamic route: match the path to a tenant's page, load the shared template, resolve each slot's data through the public provider, and apply the page-level fail-closed status gate.
Layout
Surfaces stack vertically by default. When you want a richer arrangement, give blocks grid coordinates and the renderer switches to a twelve-column grid — fully backward-compatible with the stacked default. The same layout data drives both rendering and the authoring experience, so what you arrange is what ships.
Extending the surface plane
Every part of the surface plane is designed to grow additively:
| You want to… | You do… | And it… |
|---|---|---|
| Add a widget | Implement the component, register it once | Becomes available on every surface and host, no renderer change |
| Add a chart type | Register the chart widget | Auto-appears in the dashboard authoring picker |
| Add a page template | Append a template to the shared catalog | Is referenceable by every tenant immediately |
| Add a portal page | Append a page row for the tenant | Is served by the single dynamic route automatically |
| Add a host | Implement a data provider over its secure lane | Reuses the renderer and catalog unchanged |
You declare experiences. Atelier renders them — consistently, safely, and on every surface at once.
Example: a portal page in a sheet
A portal page binds a route to a page template and overrides its widget slots with bindings to your entities. Here a public map+list of proposals, with a filter bar — all composed from declarations and rendered by the shared renderer.
yaml
portal_pages:
/budget/map:
portal: default
template: browse
match_order: 1
is_active: true
overrides:
feed:
feed:
binding: { kind: feed, entity: proposal }
props:
views: [map, list]
marker_href_template: /budget/proposals/:id
filters:
filters:
binding: { kind: filters, entity: proposal }
props:
search_placeholder: Search proposals