Skip to content

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: true

Import 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.

BlockPlaneWhat it declares
identityNames the application, its route namespace, icon, and navigation order.
entities[].schemaDataAn entity type and its fields. A schema-bearing entry owns the type; a schema-less entry references an existing one.
actionsExecutionOperations on your data — parameters, create/edit effects, eligibility criteria, side effects, and where the action surfaces in the UI.
notification_event / template / ruleExecutionEvents your app emits, the messages they render, and the rules that decide who's notified.
viewsData / AnalyticsAggregate definitions — counts, group-bys, joins — that power dashboards and public tallies.
layersSurfaceGeographic overlays for the map, sourced from an entity, a view, or an external feed.
public_surfacesSurfaceThe single source of truth for what a citizen can read — which entity or view, which states, which fields.
portal / portal_pagesSurfaceThe public website and its route bindings.
presentation / dashboardSurfaceHow the staff admin renders each entity's list, detail, and dashboard.
reportsSurfaceDocument 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:

  1. Parse — read the sheet and confirm it's well-formed.
  2. Validate — check every block against the live schema, so errors are caught before anything is written.
  3. Plan — diff the sheet against what already exists, producing a precise set of create/update operations.
  4. 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.code is 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 a schema provisions a new type; without one it references an existing type.
  • Add a public read surface — a public_surfaces row keyed by an entity or a view, with the states you want published.
  • Add analytics — declare a view with 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.

Atelier — declare your application, generate the product.