Skip to content

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:

  1. Declare your application as a sheet.
  2. Import it — Atelier generates the admin console, citizen portal, and APIs.
  3. 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: 10

Declare 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_requests

That'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/:id

The 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 declaredAtelier generates
entitiesThe data model and storage, tenant-isolated
actionsGoverned action endpoints with audit + side-effect fan-out
public_surfacesCitizen-facing read APIs with disclosure projection
portal_pagesPublic portal pages, rendered by the shared component library
presentation / dashboardA staff admin console — lists, detail pages, dashboards
viewsAnalytics aggregates that power dashboards and the portal alike
notificationsEvent-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 copy

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

Atelier — declare your application, generate the product.