Skip to main content

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:slug as their natural ID
  • Transfer: Ownership can be changed to another team (requires write permission and team eligibility)

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:

ParameterTypeDescription
idstringApp UUID or natural ID

Response:

Returns a Principal with embedded User or Team.

{
"_links": {
"self": { "href": "/api/principals/analytics" },
"inf:user": { "href": "/api/users/john.doe" },
"inf:team": { "href": "/api/teams/analytics" }
},
"id": "analytics",
"userId": null,
"teamId": "analytics",
"_embedded": {
"inf:team": {
"id": "analytics",
"name": "Analytics Team",
"materialIcon": "analytics",
"color": "blue",
"createdAt": "2023-01-15T10:00:00.000Z"
}
}
}

For User-Owned Apps:

{
"_links": {
"self": { "href": "/api/principals/john.doe" },
"inf:user": { "href": "/api/users/john.doe" }
},
"id": "john.doe",
"userId": "john.doe",
"teamId": null,
"_embedded": {
"inf:user": {
"username": "john.doe",
"displayName": "John Doe",
"email": "john@example.com"
}
}
}

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 team.

Authentication: Required

Permissions Required:

  • Member+ role (write permission on the app)
  • Permission to transfer ownership to the target team

Path Parameters:

ParameterTypeDescription
idstringApp UUID or natural ID

Payload:

Specify the new owner by providing Principal lookup fields:

{
"teamId": "marketing"
}

Or for user ownership:

{
"userId": "jane.doe"
}

Response:

Returns the updated app with a Location header pointing to the new natural ID (if slug changed).

{
"id": "d4f8a2b1-1234-5678-90ab-cdef12345678",
"type": "report",
"name": "Sales Dashboard",
"slug": "sales-dashboard",
"ownerId": "marketing",
"naturalId": "marketing:sales-dashboard",
"updatedAt": "2024-02-13T10:30:00.000Z"
}

Important Notes:

  • Changing ownership updates the naturalId if the app has a slug
  • The app's URL changes from /api/apps/analytics:sales-dashboard to /api/apps/marketing:sales-dashboard
  • All shares, tags, and other associations remain intact
  • The old natural ID becomes invalid

Error Responses:

  • 400 Bad Request - Invalid principal lookup fields
  • 404 Not Found - App doesn't exist or user lacks access
  • 403 Forbidden - User lacks write permission or cannot transfer to 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:

ParameterTypeDescription
idstringApp UUID or natural ID

Response:

Returns an array of share records with principal details.

{
"_links": {
"self": { "href": "/api/apps/analytics:sales-dashboard/shares" }
},
"_embedded": {
"inf:app-share": [
{
"_links": {
"self": { "href": "/api/apps/analytics:sales-dashboard/shares/marketing" }
},
"appId": "d4f8a2b1-1234-5678-90ab-cdef12345678",
"principalId": "marketing",
"accessLevel": 1,
"type": "Team",
"name": "Marketing Team",
"materialIcon": "campaign",
"color": "purple"
},
{
"_links": {
"self": { "href": "/api/apps/analytics:sales-dashboard/shares/john.doe" }
},
"appId": "d4f8a2b1-1234-5678-90ab-cdef12345678",
"principalId": "john.doe",
"accessLevel": 2,
"type": "User",
"name": "John Doe",
"username": "john.doe",
"displayName": "John Doe",
"email": "john@example.com",
"avatarUrl": "/api/users/john.doe/avatar?t=1707825000000"
}
]
},
"items": [...]
}

Key Fields:

FieldDescription
appIdApp UUID
principalIdID of shared user or team
accessLevelAccess level integer (≥1, higher = more access)
type"User" or "Team"
nameDisplay name
avatarUrlAvatar URL (users only, if avatar exists)

Access Levels:

LevelMeaning
0No access (not returned in results)
1Read/Run access
2+Extended access (interpretation varies by app type)

Error Responses:

  • 404 Not Found - App doesn't exist or user lacks access

GET /api/apps/{id}/shares/{principalId}

Get a specific share record.

Authentication: Required

Permissions Required: None (any user who can read the app)

Path Parameters:

ParameterTypeDescription
idstringApp UUID or natural ID
principalIdstringPrincipal ID (username or team ID)

Response:

{
"_links": {
"self": { "href": "/api/apps/analytics:sales-dashboard/shares/marketing" }
},
"id": "share-uuid-1234",
"appId": "d4f8a2b1-1234-5678-90ab-cdef12345678",
"principalId": "marketing",
"accessLevel": 1,
"createdAt": "2024-02-10T14:00:00.000Z",
"updatedAt": "2024-02-10T14:00:00.000Z"
}

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: Publisher role or Superuser (share permission)

Path Parameters:

ParameterTypeDescription
idstringApp UUID or natural ID
principalIdstringPrincipal ID (username or team ID)

Payload:

{
"accessLevel": 1,
"roles": ["viewer", "approver"]
}

Payload Fields:

FieldTypeRequiredDefaultDescription
accessLevelintegerNo1Access level (must be ≥1, use DELETE to revoke)
rolesstring[]No[]Role IDs to assign (must match informer.yaml definitions). See Roles.

Response:

Returns the created/updated share record with status 200 OK or 201 Created.

{
"id": "share-uuid-1234",
"appId": "d4f8a2b1-1234-5678-90ab-cdef12345678",
"principalId": "marketing",
"accessLevel": 1,
"roles": ["viewer", "approver"],
"createdAt": "2024-02-13T10:30:00.000Z",
"updatedAt": "2024-02-13T10:30:00.000Z"
}

Upsert Behavior:

  • If a share already exists, updates the accessLevel
  • If no share exists, creates a new one
  • Setting accessLevel: 0 via 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 access
  • 403 Forbidden - User lacks share permission

DELETE /api/apps/{id}/shares/{principalId}

Revoke access (delete a share).

Authentication: Required

Permissions Required: Publisher role or Superuser (share permission)

Path Parameters:

ParameterTypeDescription
idstringApp UUID or natural ID
principalIdstringPrincipal ID (username or team ID)

Response:

Returns 204 No Content on success.

Behavior:

  • Removes the share record
  • Principal loses access (unless they're the owner or have another path to access)
  • If the principal was the last share, the app becomes unshared

Error Responses:

  • 404 Not Found - App or share doesn't exist
  • 403 Forbidden - User lacks share permission

GET /api/apps/{id}/data-access

Get a summary of the app's data access requirements for the sharing dialog.

Authentication: Required

Permissions Required: None (any user who can read the app)

Path Parameters:

ParameterTypeDescription
idstringApp UUID or natural ID

Response:

Returns information about datasets the app accesses and their ownership.

{
"datasets": [
{
"id": "dataset-uuid-1",
"name": "Sales Data",
"ownerId": "analytics",
"ownerName": "Analytics Team"
}
]
}

Use Case:

This endpoint helps users understand what data the app can access, which is important when sharing the app with others. Users being granted access to the app may also need access to the underlying datasets.

Error Responses:

  • 404 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.items.map(s => s.name));

Transfer Ownership

// Transfer app to Marketing team
const updatedApp = await PUT('/api/apps/analytics:sales-dashboard/owner', {
teamId: 'marketing'
});

console.log('New natural ID:', updatedApp.naturalId);
// "marketing:sales-dashboard"

Revoke Access

// Remove Marketing team's access
await DELETE('/api/apps/analytics:sales-dashboard/shares/marketing');

Check Data Access Requirements

// Before sharing, check what datasets the app uses
const dataAccess = await GET('/api/apps/analytics:sales-dashboard/data-access');

console.log('App accesses these datasets:', dataAccess.datasets);

// Share the app
await PUT('/api/apps/analytics:sales-dashboard/shares/marketing', {
accessLevel: 1
});

// Optionally, also share the datasets
for (const dataset of dataAccess.datasets) {
await PUT(`/api/datasets/${dataset.id}/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']
});

List All Shares with Details

const response = await GET('/api/apps/analytics:sales-dashboard/shares');

for (const share of response.items) {
console.log(`${share.type}: ${share.name} (level ${share.accessLevel})`);
if (share.type === 'User' && share.avatarUrl) {
console.log(` Avatar: ${share.avatarUrl}`);
}
}

Permission Levels Summary

PermissionRequired RoleAllows
Read/RunMemberView app, execute queries
WriteMember+Edit content, modify settings
SharePublisherGrant access to others
Change OwnerAdminTransfer ownership to another team