Sharing
Endpoints for managing a Template's shares: who can see and work with the Template, and at what access level.
A share binds a Principal (a User or a Team) to a Template at an access level.
The principalId in the share routes is the Principal id itself: for a User
that is the username (for example annie.larson), and for a Team it is the team
id (for example order-desk). There is no separate UUID principal key.
Access level is an integer:
| Access Level | Meaning |
|---|---|
0 | None (effectively removes the share; the collection hides level-0 rows) |
1 | Read |
2 | Write |
3 | Share |
Listing and reading shares need only an authenticated user. Granting (PUT) and
revoking (DELETE) a share require permission.template.share. The Template
owner always keeps full access, independent of any share row.
GET /api/templates/{id}/shares
List a Template's shares as a HAL collection. Rows are filtered to
accessLevel > 0, so a level-0 (removed) share is not returned.
Authentication: Required (session)
Pre-blocks: template.lookup(params.id)
Response:
A HAL collection under _embedded["inf:template-share"]. Each row is mapped
from the Principal and carries templateId, principalId, accessLevel, and a
derived type ("User" or "Team") plus name. User rows add
username/displayName/email and avatar fields; Team rows add
materialIcon/icon/color. The underlying query has no ORDER BY, so row order is
not guaranteed.
[
{
"templateId": "00000000-0000-4000-8000-000000000001",
"principalId": "order-desk",
"accessLevel": 2,
"id": "order-desk",
"teamId": "order-desk",
"name": "Order Desk",
"materialIcon": "storefront",
"icon": null,
"color": "blue",
"username": null,
"displayName": null,
"email": null,
"avatarUpdatedAt": null,
"type": "Team"
},
{
"templateId": "00000000-0000-4000-8000-000000000001",
"principalId": "annie.larson",
"accessLevel": 1,
"id": "annie.larson",
"teamId": null,
"name": "Annie Larson",
"materialIcon": null,
"icon": null,
"color": null,
"username": "annie.larson",
"displayName": "Annie Larson",
"email": "annie.larson@example.com",
"avatarUpdatedAt": null,
"type": "User",
"avatarColor": {
"background": "#81C784",
"text": "#1B5E20"
},
"colorIndex": 4,
"initials": "AL"
}
]
GET /api/templates/{id}/shares/{principalId}
Get a single share row for one Principal.
Authentication: Required (session)
Pre-blocks: template.lookup(params.id)
Response:
A db.lookup on the TemplateShare model: the raw row, with tenant,
templateId, principalId, accessLevel, createdAt, and updatedAt.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/shares/order-desk"
}
},
"id": "00000000-0000-4000-8000-000000000002",
"tenant": "acme",
"templateId": "00000000-0000-4000-8000-000000000001",
"principalId": "order-desk",
"accessLevel": 2,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z"
}
A Principal with no share on this Template answers 404.
{
"statusCode": 404,
"error": "Not Found",
"message": "Not Found"
}
PUT /api/templates/{id}/shares/{principalId}
Create or update (upsert) a Principal's share on a Template.
Authentication: Required (session)
Pre-blocks: template.lookup(params.id), permission.template.share(pre.template)
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
accessLevel | integer | Yes | 1 read, 2 write, 3 share. 0 removes the share from the list. |
This route has no Joi payload schema, so accessLevel is not validated at the
route level. The body is written straight onto the share row. Send a valid
integer access level.
Example Request:
{
"accessLevel": 1
}
Response:
Responds 200 with the upserted share for both create and update (the handler
does not call .created(), so the status is 200, not 201).
{
"accessLevel": 1
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/shares/annie.larson"
}
},
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000002",
"templateId": "00000000-0000-4000-8000-000000000001",
"principalId": "annie.larson",
"accessLevel": 1,
"updatedAt": "2026-01-15T16:20:00.000Z",
"createdAt": "2026-01-15T16:20:00.000Z"
}
DELETE /api/templates/{id}/shares/{principalId}
Revoke a Principal's share, removing their access.
Authentication: Required (session)
Pre-blocks: template.lookup(params.id), permission.template.share(pre.template)
Response:
Responds 200 with an empty body (Informer convention), not 204. The Template
owner's own access is unaffected by removing a share.
Permission Mapping
Share access levels grant the matching abilities. A higher level includes the lower ones:
| Access Level | Read | Write | Share |
|---|---|---|---|
0 - None | no | no | no |
1 - Read | yes | no | no |
2 - Write | yes | yes | no |
3 - Share | yes | yes | yes |
Template ownership:
- The Template owner always has full (share-level) access, regardless of share rows.
- Change the owner via
PUT /api/templates/{id}/owner. - Granting or revoking a share requires
permission.template.share.
Global sharing:
- Set
shared: trueviaPUT /api/templates/{id}to make the Template accessible to all users in the tenant. - Global sharing requires
permission.template.write.