The mental model
The core mental model — declare, don't code; generate, don't build
Atelier has exactly four authored primitives and three generated experiences.
The four primitives an operator declares (as ontology rows):
- Entity — a type with a schema: fields, relationships, FK-less declarative links, states. "What exists." (data plane)
- Action — an
action_typerow plus six child classes (parameters, criteria, edits, creates, side-effects, placements): a gated, audited mutation with fan-out. "What can happen." (execution plane) - Surface — a
page_template/portal_page/public_entity_surface/action_placement/dashboard_configthat projects entities and actions into widgets, layouts, and bindings. "What is experienced." (surface plane) - Identity & policy —
acl_rulerows,application_entity_membership, organization tree, and the static role-action matrix that the BFF compiles into row-level filters. "Who may do what." (governs all three planes)
Around those, three more declared classes ride along: Notifications (notification_event/rule/template), Durable execution (submission_contract + execution_mode), and Reports/Analytics (report_template, source_kind:view).
The three generated experiences the platform produces from the declaration, with zero per-vertical code:
- the staff admin console (SvelteKit, port 13002) — list/detail/dashboards driven by
admin_entity_configand the shared renderer; - the citizen portal (SvelteKit) — generated from
portal_pagerows and the strict public reader; - the APIs — entity proxy, action submit, public reader, all keyed by the caller's resolved tenant.
The two declarations that matter most:
- Declare-not-code. A new entity, action, surface, or notification is a row, not a Python branch. The rules reference entity fields and templates (
{{caller.organization_memberships}}); a compiler turns them into SQL. When tempted to writeif/elsein the BFF, declare a rule instead. - Generated, not bespoke. The renderer is presentation-only (
@aiaiai-pt/design-system, v0.38.0): it dispatches a list of declarative Block descriptors to widgets via a tester/priority registry, and both admin and portal hosts drive it through their own security-scopedDataProvider. Authoring field-schema lives in the host, never in the DS.
If you remember one sentence: the rows ARE the runtime contract — the admin_entity_config.actions JSONB cache is dead (#292 S2); the action engine compiles action_type rows per request; nothing is generated from a derived cache.