Profile Panel And Tab Contract¶
The profile contract lets modules declare account-scoped panels and profile tabs they contribute to the user profile page. Profile is the signed-in person's surface: identity, personal preferences, personal relationship inventory, safe personal summaries, and social sections such as friends, posts, activity, and messages. It is not an app/workspace management surface and it is not the org/workspace home.
Billing, subscriptions, entitlements, app access, collaborators, deployments, governance, build runs, and revenue participation belong in Admin Portal or Studio / Workspace Shell. Do not use profile panels or tabs to continue app builds or manage app/workspace operations.
Design Goals¶
| Goal | How it is met |
|---|---|
| Not cookie-cutter across apps | Apps with user-owned module data can expose personal summaries automatically |
| App-agnostic runtime | ProfilePage never imports app-specific modules — it renders what the API returns |
| Deterministic | Panels are declared contracts, not arbitrary React |
| Scalable | Simple apps: identity only. Complex apps: account-scoped module panels only |
| Consistent | Same discovery pattern as contracts/admin.yaml |
Contract File — contracts/profile.yaml¶
Any module can create contracts/profile.yaml inside its module directory:
Schema¶
schema_version: mozaiks.profile.v1
panels:
- id: notification-summary # unique id within this module
title: Notifications # section heading shown in the UI
description: Personal delivery preferences and unread counts. # optional subtitle
order: 20 # sort position (identity=0, preferences=999)
kind: metrics # metrics | list | form | component
action: get_notification_profile_summary # module action name to hydrate the panel
fields: # used by metrics and list kinds
- id: unread_count
label: Unread
type: number # string | number | currency | date | boolean | status
- id: digest_enabled
label: Digest
type: boolean
tabs:
- id: friends
label: Friends
order: 10
action: list_friends_of
component: FriendListTab
kind values¶
| Kind | Behaviour |
|---|---|
metrics | Renders a grid of labelled metric tiles from fields |
list | Renders a key/value list from fields |
form | Reserved — not yet implemented. Do not emit kind: form in profile.yaml. The validator rejects it at load time. |
component | Renders an app-registered React component by component name |
type values for fields¶
| Type | Rendered as |
|---|---|
string | Plain text |
number | Locale-formatted number |
currency | $0.00 formatted |
date | toLocaleDateString() |
boolean | Yes / No |
status | StatusPill with tone derived from value |
Component panels¶
For sections that can't be expressed as metrics/list, set kind: component and declare the registered component name:
- id: invitations
title: Invitations
kind: component
component: InvitationsProfilePanel # registered via registerComponent()
order: 30
The component receives { panel, data } props. data is the result of calling action if one is declared; otherwise it is null.
Profile Tabs¶
Use tabs[] for first-class profile sections that users expect to live on the person profile rather than in shell navigation. This is the default for friends, direct messages, user posts, and activity feed.
schema_version: mozaiks.profile.v1
tabs:
- id: messages
label: Messages
order: 40
action: list_threads
component: MessagingProfileTab
The tab component receives { tab, data } props. data is the result of calling action if one is declared; otherwise it is null.
Actions that render the viewed profile subject should declare an optional user_id input in module.yaml. The platform passes the target profile user id only to actions whose input schema declares user_id. Viewer-scoped surfaces, such as a signed-in user's message inbox, should omit user_id and read ctx.user_id.
Runtime Discovery¶
GET /api/me/profile-panels¶
The platform walks app_root/modules/*/contracts/profile.yaml at request time (same pattern as admin panel discovery in mozaiksai/core/admin/router.py).
For each panel that declares an action, the platform calls the module executor and attaches the result as data on the panel. Panels without an action are returned with data: null.
Response shape:
{
"panels": [
{
"id": "notification-summary",
"title": "Notifications",
"description": "Personal delivery preferences and unread counts.",
"order": 20,
"kind": "metrics",
"module_id": "notifications",
"fields": [
{ "id": "unread_count", "label": "Unread", "type": "number" },
{ "id": "digest_enabled", "label": "Digest", "type": "boolean" }
],
"data": { "unread_count": 3, "digest_enabled": true },
"error": null
}
]
}
If the action call fails, data is null and error contains the error message. The panel is still included so the UI can render a graceful empty state rather than silently hiding it.
GET /api/me/profile-tabs¶
The platform discovers tabs[] from the same contracts/profile.yaml files. For /me, actions hydrate with the signed-in user as the profile subject. For a public profile route such as /u/:username, the shell calls /api/me/profile-tabs?username=<username> so profile-subject actions can render that user's public social sections while viewer-scoped actions continue to use the signed-in viewer.
Response shape:
{
"tabs": [
{
"id": "friends",
"label": "Friends",
"order": 10,
"module_id": "friends",
"action": "list_friends_of",
"component": "FriendListTab",
"data": { "friends": [] },
"error": null
}
]
}
Built-in Sections¶
The framework-owned identity and preferences sections are rendered directly by ProfilePage.jsx and always appear regardless of module panels or tabs. They are not declared in profile.yaml — they are platform guarantees.
| Section | Order | Editable |
|---|---|---|
| Identity (avatar, display_name, email, roles) | 0 | display_name, avatar_url |
| Account-scoped module panels and tabs inject here (order 1-998) | — | — |
| App Preferences | 999 | settings dict |
Where Code Lives¶
| Concern | File |
|---|---|
| Contract models | mozaiksai/core/runtime/app/module_loader.py — ModuleProfilePanel, ModuleProfileTab, ModuleProfileManifest |
| Discovery | mozaiksai/core/profile/discovery.py — load_profile_panels(app_root), load_profile_tabs(app_root) |
| API endpoint | mozaiksai/hosts/platform.py — GET /api/me/profile-panels, GET /api/me/profile-tabs, GET /api/users/{username} |
| UI renderer | chat-ui/src/pages/ProfilePage.jsx — profile tab renderer |
Allowed Panel Scope¶
Use contracts/profile.yaml only when the panel answers a question about the signed-in person.
Allowed examples:
- personal notification preferences
- personal invitations
- personal community memberships
- friends, direct messages, user posts, and activity feed as profile tabs
- personal votes or delegations
- personal usage summaries that do not manage an app/workspace
Forbidden examples:
- app build history or continue-build controls
- app access, roles, collaborators, or team settings
- billing plans, subscriptions, entitlement assignment, or revenue participation
- deployments, domains, hosting, health, incidents, or audit logs
- app/workspace integrations or provider configuration
Those belong in Admin Portal or Studio / Workspace Shell.
Do not add profile-owned social surfaces to config/shell.json shortcuts or global navigation by default. Generate a separate /feed, /friends, /posts, or global /messages route only when the product explicitly needs an app-wide discovery or conversation surface outside the profile.
Example — notifications module¶
# app/modules/notifications/contracts/profile.yaml
schema_version: mozaiks.profile.v1
panels:
- id: notification-summary
title: Notifications
description: Personal notification state.
order: 20
kind: metrics
action: get_notification_profile_summary
fields:
- { id: unread_count, label: Unread, type: number }
- { id: digest_enabled, label: Digest, type: boolean }
- { id: last_delivery_at, label: Last Delivery, type: date }
The notifications module's handler must implement a get_notification_profile_summary action that returns a dict matching those field ids. No changes anywhere else are needed — the profile page picks it up automatically on next load.