Appearance
The Data Model
The data model is Atelier's durable answer to what exists. It's a runtime schema engine: you define entity types, their fields, the relationships between them, and the analytical views that aggregate them — all declaratively, at request time. There are no hand-written migrations, no application code to compile, and no redeploy. You declare a type, and the storage to hold it is ready immediately.
This is the foundation the rest of the platform builds on. Anything you declare here can be rendered in any widget by the surface layer, driven by the action engine, and forked into a new tenant by the provisioning system — all without changing the engine itself. The data model stays deliberately focused on one job: storing and serving your entities, reliably and in isolation.
What you can declare
| Concept | What it gives you |
|---|---|
| Entity type | A typed record schema — the header that owns its own storage. Types support single inheritance, so a specialized type can extend a base type and inherit its fields. Types are versioned. |
| Field | A typed column on a type, with required and indexed flags, a display name, and an optional default value. Fields are inherited down the type hierarchy. |
| Relationship | A real edge between two types, with a defined cardinality and a named reverse direction so you can traverse it either way. |
| Link | A lightweight, key-based join between two types — a virtual relationship you can traverse and aggregate over without a foreign key. |
| View | A stored analytical query over a type — aggregate, group-by, filter, and join — that you read like any other collection. |
| Tenant | An isolation boundary. Every tenant-scoped record belongs to exactly one tenant and never bleeds across the line. |
Each type gets its own dedicated storage, and every read — whether of a type, a view, or a saved query — flows through one consistent, cursor-paginated query surface. You learn the read shape once and it works everywhere.
A closed, predictable type system
Fields draw from a curated set of field types, each mapped to the right underlying storage and the right set of query operators:
- Text & identifiers —
string,slug,uuid - Numbers —
integer,number,boolean - Time —
datetime,date - Structured —
json,file - Choice —
enum, with an append-only list of allowed values - Spatial —
geometry, with a geometry type and coordinate reference (defaulting to standard WGS 84 / SRID 4326)
Because the set is closed and well-defined, every field has predictable storage, predictable operators, and predictable indexing behavior. Geometry fields are first-class: spatial data is stored natively, so you can filter by within and near directly in your queries.
How you declare it
Everything is authored declaratively. In day-to-day use you describe your data model in a provisioning sheet — a readable, version-controllable description of your types, fields, relationships, links, and views — and Atelier compiles and applies it for you. New types get their storage provisioned on the spot, and you can start writing and reading records immediately.
A small, illustrative sheet might look like this:
yaml
entities:
- entity_type: report
schema:
display_name: Report
tenant_scoped: true
fields:
- field_key: title
field_type: { type: string }
required: true
indexed: true
- field_key: status
field_type:
type: enum
choices:
- { value: open, label: Open }
- { value: in_progress, label: In progress }
- { value: resolved, label: Resolved }
indexed: true
- field_key: location
field_type: { type: geometry }
- field_key: reported_at
field_type: { type: datetime }
relationships:
- rel_code: assigned_to
target_type_code: staff_member
cardinality: many_to_one
required: false
reverse_code: assigned_reports
views:
open_by_status:
display_name: Reports by status
kind: plain
source_type_code: report
group_by:
- { path: tenant_id, alias: tenant_id }
- { path: status, alias: status }
aggregate:
- { kind: count, alias: total }That's the whole loop: declare it, apply it, query it.
One read surface for everything
Types, views, and saved queries all share a single read interface, so once you know how to query one, you know how to query them all. Every collection supports:
- Filtering by any field with type-appropriate operators
- Ordering by one or more fields
- Expansion of related records inline
- Spatial queries —
withinandnearover geometry fields - Time bucketing for time-series rollups
- Cursor pagination — fast, stable, keyset-based paging that holds up as your data grows
Responses come back in a consistent envelope — the page of items plus a cursor to fetch the next page — so client code stays uniform across every kind of read.
Evolve without breaking consumers
Schemas change. Atelier makes change safe by versioning every type. When you add or retire a field, relationship, link, or action, you publish a new version of the type — and existing consumers can keep reading the version they were built against. You move the model forward without forcing every reader to move in lockstep.
Tenancy and isolation
Tenant isolation is built into the storage layer, not bolted on. Mark a type as tenant-scoped and every record belongs to exactly one tenant, enforced at the data layer:
- A tenant must exist before any record can reference it.
- Tenant identity is supplied by the request context, never trusted from a record body — so a write can't claim to belong to a tenant it has no right to.
- Tenant identity is folded into uniqueness automatically, so two tenants can safely share the same natural key (two municipalities can each have a "Report #1") without collision.
- Once a type is declared tenant-scoped, that decision is locked in — isolation can't be silently weakened later.
This is the same model that powers Atelier's template-and-fork tenancy: you build a model once on a template application, then fork it into each new tenant as ordinary, fully isolated records.
Relationships and traversal
You model connections two ways, and both behave the same when you query them:
- Relationships are true edges with explicit cardinality and a named reverse direction.
- Links are key-based joins that connect two types without a foreign key — ideal for relating records that already share a natural key.
Either way, you can traverse across the connection in your filters and pull related types into your views as join targets. Relationships are always bidirectional: declaring the forward edge gives you the reverse traversal for free.
Analytics as declared views
Every aggregate, KPI, or rollup in Atelier is a view — a stored query over a type that supports aggregation, grouping, filtering, and joining across relationships and links. Views are the single, sanctioned way to compute aggregates: you declare the shape of the metric once, and it becomes a first-class collection you read like any other.
Views carry their access scope with them. The data a view returns is fixed at creation time against its author's read scope and the scope of every type it joins, so a view computes a consistent, well-defined result set every time it's read. This makes views ideal for tenant-wide KPIs and dashboards: define "open reports by status" or "average resolution time by team" once, and surface it to staff dashboards and citizen-facing tallies alike.
Isolation guarantees, by construction
The data model is built so that isolation and least-privilege are the default, not an afterthought:
- Tenant-scoped by enforcement. Scoped records require a tenant context and reject any attempt to assert a tenant in the record body.
- No existence leakage. A record or collection you can't see is indistinguishable from one that doesn't exist — the same empty result either way — so the model never reveals what's hidden from you.
- Stable, well-defined query semantics. Pagination, defaults, and enum evolution all follow predictable, documented rules: cursor-based paging only, defaults that apply only when a field is omitted on create, and enum choices that only ever grow.
Where it fits
The data model is the durable core. On top of it, the action engine drives state changes, the surface layer renders any entity through the shared widget catalog, notifications and durable workflows react to changes, and the provisioning system forks your whole model into new tenants. Declare your entities once here, and the rest of the platform can render, drive, analyze, and replicate them — without ever reaching back into the engine.