Appearance
Quickstart
Atelier turns a single declarative file into a complete, running application — a staff admin console, a citizen portal, and a full set of APIs — and then forks that application onto each tenant that needs it. You don't build these surfaces. You declare the application once, and Atelier generates them.
This guide walks you from an empty file to a live, multi-tenant app in three moves:
- Declare your application as a sheet.
- Import it — Atelier generates the admin console, citizen portal, and APIs.
- Fork it onto a tenant, who then evolves their own copy.
By the end you'll understand the core loop every Atelier application follows: declare → generate → fork → evolve.
The idea in one minute
A sheet is one YAML file that expresses your whole application across three planes:
- Data — the entity types you store and their fields.
- Execution — the actions users can take and what those actions do.
- Surface — what staff see in the admin console and what citizens see in the public portal.
You write those declarations once. Atelier reads them and provisions everything: the data model, the admin screens, the public pages, the read APIs, the action endpoints, notifications, analytics views, and more — all rendered through one shared component library, so every surface stays consistent.
Because the same file describes the app on every plane, there's a single source of truth. Edit the file and re-import, or edit live in the authoring hub — both round-trip to exactly the same place.
Step 1 — Declare your application as a sheet
Let's build a small Service Requests application: citizens report a problem, staff triage it.
Every sheet opens with an identity block. The code you choose here is your application's identity — everything the app provisions belongs to it.
yaml
identity:
code: service-requests
name: Service Requests
route_namespace: service-requests
icon: clipboard
nav_order: 10Declare the data (entities)
An entity declares a type and its schema. tenant_scoped: true keeps each tenant's rows isolated from every other tenant's — a guarantee enforced by construction.
yaml
entities:
- entity_type: service_request
schema:
display_name: Service Request
tenant_scoped: true
fields:
- field_key: title
field_type: { type: string }
required: true
indexed: true
- field_key: description
field_type: { type: string }
- field_key: category
field_type:
type: enum
choices:
- { value: road, label: Road }
- { value: lighting, label: Lighting }
- { value: waste, label: Waste }
- { value: other, label: Other }
- field_key: location
field_type: { type: geometry }
- field_key: status
field_type:
type: enum
choices:
- { value: new, label: New }
- { value: in_progress, label: In progress }
- { value: resolved, label: Resolved }
relationships:
- rel_code: organization
target_type_code: organization
cardinality: many_to_one
required: true
reverse_code: service_requestsThat's the whole data plane for this app. Atelier provisions the type, its fields, and the storage behind it.
Declare the execution (actions)
An action is a named, governed operation against an entity. Here, staff can mark a request resolved.
yaml
actions:
- key: resolve_request
label: Resolve
target_model: service_request
execution_mode: engine
status: published
is_active: true
permissions_policy:
view: ["admin", "staff"]
apply: ["admin", "staff"]
edit: ["admin"]
submission_criteria:
- key: must-be-in-progress
criteria_type: field
config: { field: status, operator: eq, value: in_progress }
message: Only in-progress requests can be resolved.
order: 1
is_active: true
edits:
- { key: set-resolved, field: status, value: resolved }
placements:
- { key: resolve-on-detail, surface: admin_detail, target_model: service_request }Actions run through a uniform pipeline — validate, authorize, mutate, audit, and fan out side effects — so every action in your app behaves consistently and leaves an audit trail without you wiring any of it.
Declare the surface (portal + presentation)
A public surface decides what a citizen can read and in which states. A portal page binds a route under your route_namespace.
yaml
public_surfaces:
service_request:
published_states: [in_progress, resolved]
status_field: status
public_fields: [title, category, status, location]
filterable_fields: [status, category]
searchable_fields: [title]
is_active: true
portal_pages:
/service-requests/map:
portal: default
template: browse
match_order: 1
is_active: true
overrides:
feed:
feed:
binding: { kind: feed, entity: service_request }
props:
views: [map, list]
marker_href_template: /service-requests/requests/:idThe public surface is your read-exposure source of truth: only the fields and states you name here ever leave the system, projected for disclosure. Everything else stays private by default.
That's the entire application. One file, three planes. You haven't written a screen, an endpoint, or a query.
Step 2 — Import, and watch Atelier generate the app
Importing the sheet runs Atelier's provisioning pipeline: parse → validate → diff → apply. It validates your declarations against the live schema, computes the minimal set of changes, and applies them in dependency order. Re-importing is safe and idempotent — it converges to the same result every time, so you can iterate freely.
The moment the import lands, these come to life with no additional code:
| You declared | Atelier generates |
|---|---|
entities | The data model and storage, tenant-isolated |
actions | Governed action endpoints with audit + side-effect fan-out |
public_surfaces | Citizen-facing read APIs with disclosure projection |
portal_pages | Public portal pages, rendered by the shared component library |
| presentation / dashboard | A staff admin console — lists, detail pages, dashboards |
views | Analytics aggregates that power dashboards and the portal alike |
| notifications | Event-driven, multi-channel notifications |
Authorization is data, not code. Access rules are declared and compiled into every query, and the platform is fail-closed by construction: if a rule doesn't grant access, the row simply isn't returned. You express who can see what as policy — including membership-aware policy templates that scope rows to a user's organizations — and the platform enforces it everywhere uniformly.
Edit live, too
You don't have to hand-edit YAML to change anything. The authoring hub gives you visual editors for actions, presentation, dashboards, notifications, and portal pages. Because the hub round-trips through the exact same grammar, what you click you can declare, and what you declare you can click — they're two doors into one model.
Step 3 — Fork it onto a tenant
A sheet you import lands on a template plane — a reference copy of the application. To stand up a real customer, you fork that template onto a new tenant.
A fork is a one-shot operation that copies the application's configuration and rewires it into the new tenant's own isolated plane. Forking is copy-on-create: the tenant gets their own copy of every entity type, action, surface, and page — there's no shared live state between tenants.
After the fork:
- The tenant's first staff admin is provisioned.
- Tenant-wide configuration is materialized.
- The app is live for that tenant — admin console, portal, and APIs, all keyed to the tenant's identity.
Tenants evolve their own copy
This is the payoff of copy-on-create. A tenant admin opens the same editors and changes their copy — rename a status, add a notification rule, tweak the dashboard — with zero special-case code, because every configuration endpoint is keyed to the caller's resolved tenant. One tenant's edits never touch another's.
Meanwhile, a platform author maintains the template for the fleet. Edits to the template publish with preview == live and compile-on-save, so the authoring view and the running app always agree.
The loop you just learned
Declare a sheet → Import (Atelier generates admin + portal + APIs) → Fork onto a tenant → Tenant evolves their copyEvery Atelier application — from a two-field feedback form to a full smart-city vertical — follows this same loop. You stay in the language of declarations: entities, actions, surfaces, rules. Atelier handles the generation, the multi-tenancy, the authorization, the audit trail, and the rendering.
Where to go next
- Vertical Sheets — the full grammar: every block you can declare, from analytics views and geo layers to notifications and document templates.
- Lifecycle — the end-to-end journey of an application: author, import, fork, seed, run, diagnose, evolve.
- The Action Engine — how actions validate, authorize, mutate, and fan out side effects.
- Public Surfaces & the Citizen Portal — how citizen-facing reads and submissions work.
Start by declaring the smallest app that's useful, import it, and grow it one block at a time. The first running version is only a few lines away.