Platform Authoring¶
This document defines what should be authored in an app workspace and the key file contracts.
Core Rule¶
The canonical target is a self-contained app workspace rooted at app/. The same app-root shape should appear inside hosted product workspaces as well.
It should contain declaratives, assets, and explicit logic stubs. It should not contain framework compiler logic.
The bundle should stay focused on the product-facing pieces:
- app manifest
- pages
- workflows
- modules
Modules exist as support bundles, but they should not dominate the authoring model.
Active Root vs Workspace Wrapper¶
The runtime reads an active app root. Canonically, that root is:
app/in a generated/customer app workspacefactory_app/app/for the first-party Studio app bundle served by the Studio host in this repo
Current repo note:
factory_app/app/is the repo-local first-party app bundle inside this repo- hosted product workspaces should follow the same self-contained app-root pattern
Primary Families¶
Module loading binds the Python services package to the active app root. Reloading a workspace must not resolve service clients from another app left on the process import path. Python package markers remain optional.
Studio composes the packaged factory_app/app/modules/ as module defaults through the same AppLoader and ModuleLoader used for app modules. Active workspace module folders override defaults by id; a broken override fails validation rather than silently using the default. This makes shared Studio modules such as security_readiness callable through the normal authenticated module executor without copying their implementation into each workspace. The platform-only host loads only its active app modules. Config, services, data contracts, and app identity continue to come from the active workspace; module defaults do not replace those app families or grant permissions. HTTP module execution uses the active bundle's resolved app identity. The host uses its data contract or appId/app_id, then the established default identity when an authored bundle omits them. AppGenerator assembly binds the target appId before validation and preview; export reasserts that same ID. A token app claim, query app_id, or explicit execution context must agree with that identity. A POST action may separately declare app_id as a business input for a target app; that input stays in action params and does not change the execution app. Hosts that compose the module router must set their loaded app identity before accepting module requests.
Module defaults bring their own persistence declarations. The collections the Studio modules own are declared in factory_app/app/data/contract.json, in the same contract shape an app uses. AppLoader composes the declarations of the defaults it actually mounted, and each module dispatch gets one allow-list:
- an app module keeps the workspace's
app/data/contract.jsonunchanged and cannot reach a collection a mounted default declares, even in a workspace with no contract; - a mounted default reaches only the collections declared for it: its own, those another mounted default shares with it through
shared_collections, and those the workspace declares for it under its surface or shares with it; - an undeclared collection is refused for both.
A workspace that overrides a default by id owns that module's data in its own contract; the default's declarations, and shares made to or by it, no longer apply. A workspace contract that redeclares a mounted default's collection fails app load. When the factory bundle is itself the active root (source-checkout Studio), its contract is the workspace contract.
| Family | Purpose | Path |
|---|---|---|
| App manifest | Small app identity and target manifest | app/app.json |
| Pages | Normal app screens | app/ui/pages/* |
| Workflows | Agentic execution | workflows/* |
| Workflow triggers | App event to workflow rules | workflows/*/orchestrator.yaml |
Support Family¶
| Family | Purpose | Path |
|---|---|---|
| Modules | Shared backing logic and helpers | app/modules/* |
What Users Should Mostly Author¶
For most apps, the user or generator should mainly author:
- app manifest
- pages
- workflows
- modules (when shared support logic appears)
Workflow triggers are declared inline in orchestrator.yaml, not as separate files.
Generator Decision Rules¶
When the generator needs to add a new capability:
- visible routeable screen -> create a
page - shared backend helper -> create a
module - agentic execution -> create a
workflow - event-triggered workflow handoff -> add
triggersinorchestrator.yaml
Generator outputs must not be written directly into the active app root. The builder writes to MOZAIKS_GENERATED_ARTIFACTS_PATH first:
An explicit promotion step copies validated artifacts into the active root.
Framework-Owned vs Generator-Owned¶
Framework owns:¶
- manifest resolvers
- config compilers
- registry projections
- runtime loaders
- defaulting logic
Those do not belong in an app workspace.
Example:
chat-ui/src/platform/appManifest.jsis framework-ownedapp/app.jsonis generator-owned
File Contracts¶
app/app.json¶
Small authoring manifest only.
Expected default shape:
{
"appName": "My App",
"targets": {
"web": true,
"mobile": false
},
"startup": {
"landing_spot": "/dashboard"
},
"authRequired": true,
"admins": [
"owner@example.com"
]
}
The generator should not author low-level runtime plumbing here unless the user explicitly requests advanced overrides.
app/app.json is for app identity, startup route, and product auth intent.
The shared shell takes its default app scope from the active host's /api/shell-config response through NavigationProvider. ChatUIProvider waits for that response and uses its appId for chat, workflow launches, and usage scope. It must not invent a demo-app identity. Standalone embedders without a navigation provider may supply their own uiConfig; a missing identity in the app shell is a configuration error, not permission to launch under a placeholder. Server-side authentication and scope checks remain authoritative.
It is not the place for shell colors, login theme files, footer links, or header chrome.
It is also not the place for local development shortcuts such as auto-login.
app/ui/pages/{page}.yaml¶
Required:
- a declarative page schema using the App UI primitive contract
Optional:
- folder form:
app/ui/pages/{page}/page.yaml - custom UI extension files when a page intentionally leaves the primitive schema path
Shared page-level UI should live under app/ui/pages/_shared/, not inside a module by default.
Schema routes reference the exact discovered file key: customers for customers.yaml, or the directory name for customers/page.yaml. The shell uses the same discovery function as the page loader. A page's name or title must not replace that file key in a SchemaPage route; this matters on case-sensitive deployment filesystems.
workflows/{workflow}/¶
Required:
orchestrator.yamlagents.yamltransition_graph.yamlcontext_variables.yamlstructured_outputs.yamltools.yamlui_config.yamlmiddleware.yaml
Optional but common:
tools/*.pyui/index.jsui/*.{js,jsx}extended_orchestration/task_batches.yaml
Event routing and workflow triggers are configured via: - workflows/{workflow}/orchestrator.yaml - triggers declare which app events start or resume a workflow - app hosts/backends emit domain events through the runtime ingress boundary; there is no separate app-owned automations catalog file
app/modules/{module}/¶
Required:
module.yamlbackend/handler.py
Optional contracts:
contracts/events.yamlcontracts/reactions.yamlcontracts/notifications.yamlcontracts/settings.yamlcontracts/admin.yamlcontracts/profile.yaml
Recommended for any module with database access:
backend/service.pybackend/repo.pybackend/policy.pybackend/schemas.py
Optional helper files:
backend/{helper_files}.pyui/index.jsfor explicit frontend/admin extension registration- additional
ui/*.{js,jsx}
Backend layer contract¶
Each file in backend/ has a single responsibility. Do not mix concerns.
handler.py — dispatch layer only.
class {Name}Handler:
def __init__(self):
self.service = {Name}Service()
async def action_name(self, ctx, *, param: str) -> dict:
return await self.service.action_name(ctx, param=param)
Rules: never access persistence directly, never contain conditional business logic, never call ctx.emit(). One method per action declared in module.yaml.
service.py — all business logic lives here.
class {Name}Service:
def __init__(self, repo=None):
self.repo = repo or {Name}Repo()
async def action_name(self, ctx, *, param: str) -> dict:
# validate → repo calls → ctx.emit() → return
record = await self.repo.get(ctx, query={...})
await ctx.emit("domain.{name}.{event}", {...})
return {"success": True}
Rules: never access persistence directly, delegates all DB operations to repo, calls ctx.emit() only after state is committed.
repo.py — database access only.
class {Name}Repo:
async def _collection(self, ctx):
persistence = getattr(ctx, "persistence", None)
if persistence is None:
raise RuntimeError("Persistence is not available for this app context.")
return persistence.collection("{module_id}", "{collection_name}")
async def get(self, ctx, *, query: dict) -> dict | None: ...
async def insert(self, ctx, *, record: dict) -> None: ...
async def update(self, ctx, *, query: dict, update: dict) -> int: ...
async def list(self, ctx, *, query: dict, limit: int) -> list[dict]: ...
async def count(self, ctx, *, query: dict) -> int: ...
Rules: no business logic, no event emission, no validation — pure data access. Generated repo code uses ctx.persistence.collection(module_id, collection_name) with values aligned to data/contract.json. It must not use ctx.db, call get_mongo_client(), or hardcode database names.
policy.py — optional ownership preflight.
Code renders this file from collection tenancy and owner_field. Its helpers use the immutable ctx.persistence.principal; request parameters and mutable context identity are not authorization. Runtime persistence enforces ownership on every operation even when the repo passes domain filters directly and never calls these helpers. Inserts receive the declared owner field automatically; conflicting supplied values are rejected. Keep domain permission and transition checks in service.py.
schemas.py — typed document definitions and pure helpers.
from typing import TypedDict
class {Name}Record(TypedDict):
record_id: str
owner_id: str
status: str
created_at: str
updated_at: str
def timestamp_now() -> str:
from datetime import UTC, datetime
return datetime.now(UTC).isoformat()
def coerce_limit(value, default: int = 20, maximum: int = 100) -> int:
try:
return max(1, min(int(value), maximum))
except Exception:
return default
Rules: TypedDicts for MongoDB document shapes, no Pydantic (framework handles dispatch validation), no I/O, no imports from service or repo.
Modules are backing capability bundles. The generator should not create module UI unless the module truly exports reusable UI.
Modules do not own page routing by default. Routeable surfaces should be pages.
app/data/contract.json¶
Optional for non-persistent apps. Required when generated modules own business collections such as projects, tasks, audit_logs, or notifications.
The generator emits data_contract and stages it as app/data/contract.json. The platform runtime loads it with the app, indexes entities by (module_id, entity_name), and applies declared indexes idempotently. Invalid JSON or invalid shape fails app load.
Example collection intent:
{
"version": "1",
"surfaces": [
{
"surface_id": "tasks",
"surface_kind": "module",
"collections": [
{
"module_id": "tasks",
"name": "tasks",
"entity_name": "tasks",
"indexes": [
{
"name": "task_project_status",
"keys": [["project_id", 1], ["status", 1]]
}
]
}
]
}
]
}
app/data/migrations/{migration_id}.json¶
Optional additive migration files. The runtime loads files under app/data/migrations/*.json, applies them in deterministic filename order, and records migration state in mozaiksai.AppDatabaseMigrations.
Supported operations:
ensure_collectionensure_index
The runtime does not support destructive migrations, document rewrites, or arbitrary migration code.
For production persistent generated apps, set MOZAIKS_DATABASE_STARTUP_POLICY=required so index or migration failures fail startup. The default is best_effort, which logs failures and continues for local/dev iteration. App business data uses MOZAIKS_APP_DATABASE_NAME, then MOZAIKS_APPS_DATABASE, then the fallback database name mozaiks_apps unless the adapter is constructed with an explicit database name.
Migration history states are in_progress, applied, and failed. Failed migrations include operation-level error details and are not retried automatically; an operator must clear or repair the failed history record before retrying.
Active app-root frontend extension barrel¶
When a generated app needs bounded frontend customization, the active app root may also contain:
app/ui/index.jsin app workspaces
This file is the deterministic registration barrel loaded by @platform/extensions. Generators may create it only to register contract-declared UI stubs such as custom admin components declared through modules/{module}/contracts/admin.yaml and materialized from module_contract.js_stubs.
Do not treat ui/index.js as a second page system. It is only a bounded extension entrypoint for declared customization stubs.
app/brand/¶
Generator-owned when the app needs shell branding.
Use:
app/brand/assets/for logos and iconsapp/brand/fonts/for local fontsapp/brand/login-theme/only when auth login theme assets change
Do not use app/brand/ for page route logic.
What Mozaiks Should Default¶
Mozaiks should default anything that most apps share, including:
- shell behavior
- auth plumbing
- backend URLs
- mobile defaults
- version defaults
- dev defaults
- registry projections
Those should not be the main declarative burden on the user.
Simple Compiler View¶
The platform should think in this order:
app idea
-> small app manifest
-> pages
-> workflows (with inline triggers)
-> app events
-> support modules if needed
That is enough to generate useful apps without turning the config layer into a second programming language.
Derived or Generated Artifacts¶
These should not be generator-authored:
- derived module catalog projections
- manifest resolvers
- generated realm exports
- system support files such as silent SSO helpers
Current examples:
factory_app/app/brand/realm-export.jsonis a repo-local Keycloak compose seed consumed by OSS self-hosting infra; generated apps should not author realm exports directlyapp/brand/_system/silent-check-sso.htmlis framework support
Anti-Drift Rule¶
If a file exists only to repeat information already declared elsewhere, the generator should not author it unless the runtime still requires it as a true input.
That means:
- do not duplicate module metadata into a separate catalog file
- do not duplicate app defaults into authored manifests
- do not add compiler helpers to the app workspace