Appearance
Vertical Sheets
A vertical sheet is the heart of Atelier. It's a single YAML file that fully describes a running application across all three planes of the platform:
- Data — the entities your app stores and how they're shaped.
- Execution — the actions and business logic that operate on that data.
- Surface — the staff admin screens, citizen portal pages, map layers, dashboards, and public APIs that expose it.
You declare an application as data, and Atelier generates everything else: the schema, the staff back office, the citizen-facing portal, the analytics views, and the REST surface. There's no application code to write, build, or deploy — the sheet is the app.
Declare, don't code. Everything else in Atelier — the action engine, public surfaces, notifications, analytics — is ultimately a block in this one grammar. Learn the sheet and you've learned the platform.
A small example
Here's a minimal sheet that declares one entity, one action, and one public read surface:
yaml
identity:
code: street-lighting
name: Street Lighting
description: Citizens report broken streetlights; crews schedule and close repairs.
route_namespace: street-lighting
icon: lightbulb
nav_order: 40
is_active: true
entities:
- entity_type: light_fault
schema:
display_name: Light Fault Report
tenant_scoped: true
fields:
- field_key: description
field_type: { type: string }
required: true
- field_key: status
field_type:
type: enum
choices:
- { value: open, label: Open }
- { value: scheduled, label: Scheduled }
- { value: fixed, label: Fixed }
indexed: true
- field_key: location
field_type: { type: geometry }
- field_key: reported_at
field_type: { type: datetime }
relationships:
- rel_code: organization
target_type_code: organization
cardinality: many_to_one
required: true
reverse_code: light_faults
actions:
- key: mark_fixed
label: Mark fixed
target_model: light_fault
execution_mode: engine
status: published
is_active: true
permissions_policy:
view: ["admin", "staff"]
apply: ["admin", "staff"]
edit: ["admin"]
edits:
- { key: set-fixed, field: status, value: fixed }
placements:
- { key: mark-fixed-detail, surface: admin_detail, target_model: light_fault }
public_surfaces:
light_fault:
published_states: [open, scheduled, fixed]
status_field: status
public_fields: [description, status, location, reported_at]
filterable_fields: [status]
is_active: trueImport that, and you have: a light_fault entity with a generated admin list and detail view, a "Mark fixed" action wired into the back office, and a read-only public endpoint that citizens can query for open faults. No additional wiring required.
The building blocks
A sheet is composed of declarative blocks, each targeting one of the three planes.
| Block | Plane | What it declares |
|---|---|---|
identity | — | Names the application, its route namespace, icon, and navigation order. |
entities[].schema | Data | An entity type and its fields. A schema-bearing entry owns the type; a schema-less entry references an existing one. |
actions | Execution | Operations on your data — parameters, create/edit effects, eligibility criteria, side effects, and where the action surfaces in the UI. |
notification_event / template / rule | Execution | Events your app emits, the messages they render, and the rules that decide who's notified. |
views | Data / Analytics | Aggregate definitions — counts, group-bys, joins — that power dashboards and public tallies. |
layers | Surface | Geographic overlays for the map, sourced from an entity, a view, or an external feed. |
public_surfaces | Surface | The single source of truth for what a citizen can read — which entity or view, which states, which fields. |
portal / portal_pages | Surface | The public website and its route bindings. |
presentation / dashboard | Surface | How the staff admin renders each entity's list, detail, and dashboard. |
reports | Surface | Document templates (PDFs, letters) generated from entity data. |
Two ways to author
There's one canonical destination — the application definition the rest of the platform reads — reachable two ways, and they stay perfectly in sync.
Write the YAML. Author a sheet by hand and import it. This is ideal for version-controlled, reviewable application definitions.
Use the authoring hub. Atelier ships a visual authoring surface where operators build the same applications through forms and editors. Because Atelier can export any live application back into sheet form, the two directions round-trip cleanly: what you click, you can declare, and what you declare, you can edit visually. Export then re-import always produces a zero diff.
How provisioning works
Both authoring paths run through the same provisioning pipeline:
- Parse — read the sheet and confirm it's well-formed.
- Validate — check every block against the live schema, so errors are caught before anything is written.
- Plan — diff the sheet against what already exists, producing a precise set of create/update operations.
- Apply — provision the entity types first, then walk the rest of the blocks in strict dependency order: application, entities, actions, notifications, reports, views, layers, public surfaces, and portal pages.
Schema changes are additive by construction — provisioning evolves a type by adding to it, never by dropping or rewriting existing data. Re-importing a sheet is idempotent: running it twice converges on the same state rather than duplicating rows.
Tenancy: template and fork
Atelier's multi-tenant model is built directly on sheets.
- A template application is provisioned from the sheets once. It carries the full vocabulary, presentation, actions, and surfaces for a vertical.
- Onboarding a new tenant forks the template — every configurable row is copied and re-pointed onto the new tenant in a single operation.
After the fork, that tenant's administrators edit their own copy using the exact same editors, with no special-case code. Every configuration endpoint is keyed to the caller's resolved tenant, so each tenant's changes stay isolated to that tenant. Tenant data never crosses the boundary.
Invariants you can rely on
These guarantees hold for every sheet Atelier provisions:
identity.codeis the application's identity. Everything the sheet declares is provisioned under it.- Tenant scoping is explicit. Every entity schema states whether it's tenant-scoped; the choice is made up front and is fixed once the type exists, keeping tenant isolation unambiguous.
- Ownership is clear. An entity entry with a schema owns its type; without one, it references a type that already exists.
- Evolution is additive. Schema changes only ever add — a non-additive change surfaces as a warning in the plan, never as a destructive write.
- Actions stay in scope. An action targets an entity declared in the same sheet.
- Public surfaces are governed. Any view exposed to citizens is aggregated per tenant, so only tenant-isolated, row-free results ever leave the platform.
- Round-trips are lossless. Export-then-import always diffs to zero.
- Portal routes are namespaced. Public routes live under the application's route namespace, keeping every vertical's pages cleanly separated.
Extending a sheet
The grammar grows with your needs:
- Add a vertical — author a new sheet with an
identity.code, declare your entities (referenced types before the entries that point at them), actions, surfaces, views, and portal pages, then import. - Add an entity type — an
entities[]entry with aschemaprovisions a new type; without one it references an existing type. - Add a public read surface — a
public_surfacesrow keyed by an entity or a view, with the states you want published. - Add analytics — declare a
viewwith the group-bys and aggregates you need; surface it on a dashboard or expose its tally publicly.
Because the sheet drives data, logic, and surface together, extending one block is usually all it takes to ship a new capability end to end.