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 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¶
| Family | Purpose | Path |
|---|---|---|
| App manifest | Small app identity and target manifest | app/app.json |
| Pages | Normal app screens | app/ui/pages/* |
| Workflows | Agentic execution | app/workflows/* |
| Workflow triggers | App event to workflow rules | app/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.
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.
app/workflows/{workflow}/¶
Required:
orchestrator.yamlagents.yamlhandoffs.yamlcontext_variables.yamlstructured_outputs.yamltools.yamlui_config.yamlhooks.yaml
Optional but common:
tools/*.pyui/index.jsui/*.{js,jsx}extended_orchestration/mfj_extension.json
Event routing and workflow triggers are configured via: - app/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.yamlevents.yamlsubscriptions.yamlnotifications.yamlsettings.yamladmin.yamlbackend/__init__.pybackend/handler.py
Recommended for any module with database access:
backend/service.pybackend/repo.pybackend/policy.pybackend/schemas.py
Optional hooks:
backend/settings.pybackend/subscriptions.pybackend/notifications.pybackend/admin.pyui/index.js- 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 ctx.db 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("hosted.{name}.{event}", {...})
return {"success": True}
Rules: never access ctx.db directly, delegates all DB operations to repo, calls ctx.emit() only after state is committed.
repo.py — database access only.
COLLECTION = "hosted_{name}_records"
class {Name}Repo:
async def _collection(self, ctx):
db = getattr(ctx, "db", None)
if db is not None:
return db[COLLECTION]
from mozaiksai.core.core_config import get_mongo_client
return get_mongo_client()["mozaiks"][COLLECTION]
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.
policy.py — multi-tenancy query scoping.
def owner_id_from_context(ctx, user_id=None) -> str:
return user_id or getattr(ctx, "user_id", None) or ""
def scoped_owner_query(ctx) -> dict:
owner_id = owner_id_from_context(ctx)
return {"owner_id": owner_id} if owner_id else {}
def scoped_record_query(ctx, *, record_id: str) -> dict:
query = {"record_id": record_id}
owner_id = owner_id_from_context(ctx)
if owner_id:
query["owner_id"] = owner_id
return query
Rules: pure functions only, no DB access, no side effects.
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.
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}/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:
app/brand/realm-export.jsonis a generated deployment artifactapp/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