Ownership & Sharing
Apps support team-based ownership and fine-grained sharing with access levels. The owner can change, and apps can be shared with multiple teams or users.
Ownership Model
- Owner: A Principal (user or team) identified by
ownerId - Natural ID: Apps with a slug use
ownerId:slugas their natural ID - Transfer: Ownership can be changed to another team (requires write permission plus eligibility on the target team)
GET /api/apps/{id}/owner
Get the app's current owner (Principal with embedded User or Team).
Authentication: Required
Permissions Required: None (any user who can read the app)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | App UUID or natural ID |
{
"_links": {
"self": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/owner"
}
},
"id": "admin",
"userId": "admin",
"teamId": null,
"tenant": "acme",
"_embedded": {
"inf:user": {
"_links": {
"self": {
"href": "https://informer.example.com/api/users/admin"
},
"inf:memberships": {
"href": "https://informer.example.com/api/users/admin/memberships"
},
"inf:avatar-upload": {
"href": "https://informer.example.com/api/users/admin/_upload/{uploadId}/avatar-upload",
"templated": true
},
"inf:password": {
"href": "https://informer.example.com/api/users/admin/password"
},
"inf:feed": {
"href": "https://informer.example.com/api/users/admin/feed"
},
"inf:viewed-feed": {
"href": "https://informer.example.com/api/users/admin/_markFeedViewed"
},
"inf:superuser": {
"href": "https://informer.example.com/api/users/admin/superuser"
}
},
"permissions": {
"changeDomain": false,
"changeProfile": true,
"changePassword": true,
"delete": false,
"edit": true,
"reassign": true,
"superuser": true,
"enable": true,
"changeTheme": true,
"lock": true,
"setGlobalPermissions": true,
"manageLogin": true
},
"domain": "local",
"username": "admin",
"displayName": "acme Administrator",
"familyName": "Administrator",
"givenName": "acme",
"middleName": null,
"email": null,
"data": null,
"superuser": true,
"timezone": "America/New_York",
"settings": {
"log": {
"log": false,
"info": false,
"warn": false,
"debug": false,
"error": true,
"remote": false
}
},
"enabled": true,
"source": null,
"sourceId": null,
"lastViewedFeed": null,
"tenant": "acme",
"globalPermissions": {
"ai": false,
"jobCreation": true,
"viewAllTeams": true,
"viewAllUsers": true,
"painlessScriptCreation": true
},
"userFields": {},
"locked": false,
"lockedAt": null,
"loginAttempts": 0,
"passwordSetAt": "2026-01-15T16:20:00.000Z",
"passwordExpiresAt": "2026-01-15T16:20:00.000Z",
"forgotPasswordRequestTime": null,
"mfa": null,
"mfaConfig": {},
"aiMessage": null,
"aiConsent": false,
"hasSharedDatasources": false,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"initials": "EA",
"colorIndex": 4,
"avatarColor": {
"background": "#81C784",
"text": "#1B5E20"
},
"managesPassword": true
}
}
}
For team-owned apps, teamId is set and the team is embedded under
inf:team instead.
Error Responses:
404 Not Found- App doesn't exist or user lacks access
PUT /api/apps/{id}/owner
Change the app's owner to a different principal (typically a team).
Authentication: Required
Permissions Required:
app:writeon the appapp:changeOwnerToTeamfor the target: the tenant must have the AI capability and the caller needsteam:assignOwnership(Designer or higher) on the target team
Payload:
The payload is the new owner's principal id; all other fields are stripped:
{
"id": "marketing"
}
An unknown principal id is rejected with 400 Bad Request.
{
"id": "marketing"
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk"
},
"inf:owner": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/owner"
},
"inf:draft": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/draft"
},
"inf:app-shares": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/shares"
},
"inf:app-tags": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/tags"
},
"inf:favorite": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/favorite"
},
"inf:app-tag": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/tags/{tagId}",
"templated": true
},
"inf:export-bundle": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/_export"
},
"inf:cloud-publish": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/_cloud-publish"
},
"inf:copy": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/_copy"
},
"inf:print": [
{
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/_print"
},
{
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/_print",
"title": "Print to PDF"
}
],
"inf:migrate": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/_migrate"
},
"inf:deploy": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/_deploy"
},
"inf:app-tokens": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/tokens"
},
"inf:app-roles": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/roles"
},
"inf:app-dependencies": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/dependencies"
},
"inf:app-dependency": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/dependencies/{name}",
"templated": true
},
"inf:app-environment": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/environment"
},
"inf:app-environment-var": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/environment/{key}",
"templated": true
},
"inf:app-suggest-icon": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/_suggest-icon"
},
"inf:app-modernize-manifest": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/_modernize-manifest"
},
"inf:app-draft-diff": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/draft/_diff"
},
"edit": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/_edit"
},
"inf:app-share": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/shares/{principalId}",
"templated": true
},
"inf:entrypoint": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/view/",
"title": "index.html"
},
"inf:library": {
"href": "https://informer.example.com/api/libraries/admin%3Aorder-desk-files",
"title": "App Files"
},
"inf:library-contents": {
"href": "https://informer.example.com/api/libraries/admin%3Aorder-desk-files/contents/index.html",
"title": "Raw Contents"
},
"share": {
"href": "https://informer.example.com/api/apps/marketing%3Aorder-desk/view/?token=%7Btoken%7D",
"title": "Share Page"
}
},
"naturalId": "marketing:order-desk",
"isManaged": false,
"permissions": {
"assignTags": true,
"bind": true,
"changeOwner": true,
"copy": true,
"delete": true,
"edit": true,
"manage": true,
"purge": true,
"restore": true,
"run": true,
"share": true,
"switchRoles": true,
"write": true
},
"id": "00000000-0000-4000-8000-000000000001",
"type": "app",
"name": "Order Desk",
"tenant": "acme",
"ownerId": "marketing",
"slug": "order-desk",
"description": "Track and fulfill open orders",
"defn": {
"entryPoint": "index.html"
},
"settings": {
"sandbox": true,
"passDatasets": true
},
"origin": "ui",
"source": null,
"sourceId": null,
"shared": false,
"defnUpdatedAt": null,
"libraryId": "00000000-0000-4000-8000-000000000002",
"webhookSecret": "encrypted:iv:0000000000000000",
"dbRolePassword": null,
"deletedAt": null,
"editingId": null,
"deployedAt": null,
"editingSession": null,
"editingHeartbeatAt": null,
"editingLockedBy": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"folderId": null,
"datasets": [],
"library": {
"naturalId": "admin:order-desk-files",
"connectionOwner": "admin",
"isLocal": true,
"permissions": {
"assignTags": true,
"changeOwner": true,
"configure": true,
"connect": true,
"edit": true,
"delete": true,
"write": true,
"sync": true,
"rename": true,
"share": true,
"editConnection": true
},
"id": "00000000-0000-4000-8000-000000000002",
"type": "local",
"name": "Order Desk Files",
"slug": "order-desk-files",
"description": null,
"ownerId": "admin",
"data": null,
"folderId": null,
"shared": false,
"assistantAccess": false,
"embedded": true,
"syncedAt": null,
"syncInterval": null,
"snapshotOfId": null,
"chatId": null,
"source": null,
"sourceId": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme",
"integration": null
},
"editing": null,
"contentModifiedAt": "2026-01-15T16:20:00.000Z"
}
Important Notes:
- The response is the full updated app with a
Locationheader pointing at the new natural ID - Changing ownership updates the
naturalId(theownerId:slugpair), so the app's URL changes and the old natural ID stops resolving - All shares, tags, and other associations remain intact
Error Responses:
400 Bad Request- Payload is not a known principal id404 Not Found- App doesn't exist or user lacks access403 Forbidden- User lacks write permission or cannot transfer to the target team
GET /api/apps/{id}/shares
Get all shares for an app (users and teams with access).
Authentication: Required
Permissions Required: None (any user who can read the app)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | App UUID or natural ID |
The response is a plain array (not a HAL collection). Each row joins the share with its principal's display details:
[
{
"appId": "00000000-0000-4000-8000-000000000001",
"principalId": "marketing",
"accessLevel": 1,
"roles": [
"viewer"
],
"id": "marketing",
"teamId": "marketing",
"name": "Marketing",
"materialIcon": "campaign",
"icon": null,
"color": "purple",
"username": null,
"displayName": null,
"email": null,
"avatarUpdatedAt": null,
"type": "Team"
}
]
Key Fields:
| Field | Description |
|---|---|
principalId | ID of the shared user or team |
accessLevel | Access level integer (≥1; level-0 rows are filtered out) |
roles | App role ids assigned to this share (see Roles) |
type | "User" or "Team" |
name | Display name (team name, user display name, or username) |
materialIcon / icon / color | Team chip styling (teams) |
avatarUrl / colorIndex / initials | Avatar rendering data (users) |
Error Responses:
404 Not Found- App doesn't exist or user lacks access
GET /api/apps/{id}/shares/{principalId}
Get a specific share record (the raw AppShare row, same shape as the PUT response below).
Error Responses:
404 Not Found- App, principal, or share doesn't exist
PUT /api/apps/{id}/shares/{principalId}
Create or update a share (grant access to a user or team).
Authentication: Required
Permissions Required: app:share (Publisher on the app's team, or
superuser)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | App UUID or natural ID |
principalId | string | Principal ID (username or team ID) |
Payload Fields:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
accessLevel | integer | No | 1 | Access level (must be ≥1, use DELETE to revoke) |
roles | string[] | No | [] | Role IDs to assign (must match informer.yaml definitions). See Roles. |
Unknown payload fields are stripped.
{
"accessLevel": 1,
"roles": [
"viewer"
]
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/shares/marketing"
}
},
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000002",
"appId": "00000000-0000-4000-8000-000000000001",
"principalId": "marketing",
"accessLevel": 1,
"roles": [
"viewer"
],
"updatedAt": "2026-01-15T16:20:00.000Z",
"createdAt": "2026-01-15T16:20:00.000Z"
}
Upsert Behavior:
- If a share already exists, updates
accessLevelandroles - If no share exists, creates a new one
- Setting
accessLevel: 0via PUT is invalid (use DELETE instead)
Error Responses:
400 Bad Request- Invalid access level (must be ≥1)404 Not Found- App doesn't exist or user lacks access403 Forbidden- User lacks share permission
DELETE /api/apps/{id}/shares/{principalId}
Revoke access (delete a share).
Authentication: Required
Permissions Required: app:share (Publisher on the app's team, or
superuser)
Behavior:
- Removes the share record
- The principal loses access (unless they're on the owning team or have another path to access)
Error Responses:
404 Not Found- App doesn't exist403 Forbidden- User lacks share permission
GET /api/apps/{id}/data-access
Summarize the data access the app's manifest declares, for display in the sharing dialog ("people you share this with will also exercise this access").
Authentication: Required
Permissions Required: None (any user who can read the app)
{
"_links": {
"self": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/data-access"
}
},
"datasets": [],
"queries": [],
"datasources": [],
"integrations": [],
"libraries": [],
"apis": [
{
"raw": "/api/jobs/*"
}
],
"hasAccess": true
}
Response Shape:
| Field | Description |
|---|---|
datasets / queries / datasources / integrations / libraries | Arrays of { id, name } for each resource the manifest's access: block whitelists. Entries gain notFound: true when the referenced resource no longer exists, and dataset entries gain hasFilter: true when the manifest pins a filter. |
apis | Raw API whitelist entries: { raw } for plain strings, { method, url } for object form |
hasAccess | Whether anything at all is whitelisted |
Returns null (empty body) when the app has no library or its manifest
declares no access: block. Note this summarizes the legacy/raw access:
whitelist; dependency slots (dependencies:) are surfaced through
GET /api/apps/{id}/dependencies instead.
Error Responses:
400 Bad Request- The manifest exists but could not be loaded/parsed404 Not Found- App doesn't exist or user lacks access
Common Sharing Patterns
Share with a Team
// Grant read access to Marketing team
await PUT('/api/apps/analytics:sales-dashboard/shares/marketing', {
accessLevel: 1
});
// Verify the share
const shares = await GET('/api/apps/analytics:sales-dashboard/shares');
console.log('Shared with:', shares.map(s => s.name));
Transfer Ownership
// Transfer app to Marketing team
const updatedApp = await PUT('/api/apps/analytics:sales-dashboard/owner', {
id: 'marketing'
});
console.log('New natural ID:', updatedApp.naturalId);
// "marketing:sales-dashboard"
Check Data Access Before Sharing
const dataAccess = await GET('/api/apps/analytics:sales-dashboard/data-access');
if (dataAccess?.hasAccess) {
console.log('Datasets:', dataAccess.datasets.map(d => d.name));
console.log('Raw APIs:', dataAccess.apis);
}
await PUT('/api/apps/analytics:sales-dashboard/shares/marketing', {
accessLevel: 1
});
Share with Roles
// First, check what roles the app defines
const rolesResp = await GET('/api/apps/analytics:sales-dashboard/roles');
console.log('Available roles:', rolesResp.roles.map(r => r.id));
// Share with specific roles
await PUT('/api/apps/analytics:sales-dashboard/shares/john.doe', {
accessLevel: 1,
roles: ['viewer', 'approver']
});
Permission Levels Summary
| Operation | Required role on the app's team |
|---|---|
| Read/Run | Read access via ownership or a share |
Update metadata (PUT /apps/{id}) | Designer (Magic Report) / Admin (full App) |
| Share / revoke shares | Publisher |
| Transfer ownership | app:write plus Designer on the target team |