Permissions
pingd has NATS-style pattern permissions on top of topic access flags and share tokens. A permission grants or denies read and/or publish access for a user, or globally for all non-guest users, on a topic pattern.
Access levels
| Level | Read | Publish |
|---|---|---|
rw | yes | yes |
ro | yes | no |
wo | no | yes |
deny | no | no |
Scopes
| Scope | Applies to |
|---|---|
user | One specific user |
global | All non-guest authenticated users; guests ignore permissions |
Permissions may also include expiresAt. Expired rows are ignored. If
expiresAt is omitted when creating a permission, the server uses
PINGD_DEFAULT_PERMISSION_TTL when configured; otherwise the permission does not expire.
Guests are never affected by user or global permissions. They use public topic flags and share tokens only.
Share tokens
Share tokens are per-topic credentials managed by the topic owner or an admin. They use
the header X-Topic-Token: tk_..., grant ro, wo, or
rw, and can expire. The raw token is returned only when created or rotated;
the server stores a hash.
A valid share token is a cap, not an add-on. For example, an ro share token
can read but cannot publish, even if the topic has publicPublish=true.
Revoke or rotate the share to cut off token holders.
If expiresAt is omitted when creating a share, the server uses
PINGD_DEFAULT_SHARE_TOKEN_TTL when configured; otherwise the share does not expire.
PINGD_MAX_SHARE_TOKENS_PER_TOPIC can cap active shares per topic.
Pattern matching
Patterns use dot separators. They follow NATS conventions:
| Pattern | Matches |
|---|---|
alerts | only the topic alerts |
alerts.* | alerts and one child level: alerts.cpu, but not alerts.cpu.high |
alerts.> | alerts and any depth below it |
* | everything |
alerts.disk, not alerts/disk.
Resolution order
- Admin: always allowed.
- Topic owner: always allowed.
- Share token: a valid unexpired
X-Topic-Tokendecides access and skips permission/public fallback. - Permissions: registered non-guest users only; matching user and global grants are combined.
- Public flags: read falls back to
publicRead, publish falls back topublicPublish.
When multiple permissions match, deny wins outright.
Otherwise rw wins. Two separate permissions
(ro + wo) on the same pattern combine to rw.
A matching deny blocks non-owner, non-admin registered users even when public
flags would otherwise allow access. It does not override the topic owner, an admin, or a
valid share token. Owner/admin management routes (update/delete topic, share management,
webhook management) and admin-only stats ignore permission grants entirely.
Examples
| User | Topic | publicRead | publicPublish | Token | Permission | Read | Publish |
|---|---|---|---|---|---|---|---|
| anon | news | true | true | – | – | yes | yes |
| anon | news | true | false | – | – | yes | no |
| anon | team | false | false | rw | – | yes | yes |
| anon | team | false | false | ro | – | yes | no |
| jinx | secrets | false | false | – | user ro | yes | no |
| jinx | secrets | false | false | – | user rw | yes | yes |
| jinx | news | true | true | – | user deny | no | no |
| ops | deploy.prod | false | false | – | global ro on deploy.> | yes | no |
| guest | team | false | false | – | user rw ignored | no | no |
| owner | secrets | false | false | – | user deny | yes | yes |
| admin | any | any | any | any | any | yes | yes |
Endpoints
| Method | Path | Who |
|---|---|---|
| GET | /permissions/:username | List user's permissions; admin or self |
| POST | /permissions/:username | Create user permission; admin only |
| GET | /permissions | List global permissions; admin only |
| POST | /permissions | Create global permission; admin only |
| DELETE | /permissions/:id | Delete any permission; admin only |
| GET | /topics/:name/shares | List topic shares; owner or admin |
| POST | /topics/:name/shares | Create topic share; owner or admin |
| PATCH | /topics/:name/shares/:id | Update share label, access, or expiry |
| POST | /topics/:name/shares/:id/rotate | Rotate raw share token |
| DELETE | /topics/:name/shares/:id | Revoke share |
Create a user permission
curl -s http://localhost:7685/permissions/jinx \
-H 'Authorization: Bearer $ADMIN_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"accessLevel": "rw",
"topicPattern": "alerts.>",
"expiresAt": "2026-12-31T23:59:59Z"
}'
pingd-cli permissions create \
--username jinx \
--access rw \
--pattern 'alerts.>' \
--expires-in 30d
import requests
requests.post(
"http://localhost:7685/permissions/jinx",
headers={"Authorization": "Bearer ADMIN_TOKEN"},
json={
"accessLevel": "rw",
"topicPattern": "alerts.>",
"expiresAt": "2026-12-31T23:59:59Z",
},
)
Create a global permission
curl -s http://localhost:7685/permissions \
-H 'Authorization: Bearer $ADMIN_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"accessLevel": "ro",
"topicPattern": "announcements.>"
}'
pingd-cli permissions create-global \
--access ro \
--pattern 'announcements.>'
import requests
requests.post(
"http://localhost:7685/permissions",
headers={"Authorization": "Bearer ADMIN_TOKEN"},
json={
"accessLevel": "ro",
"topicPattern": "announcements.>",
},
)
Create a share token
curl -s http://localhost:7685/topics/alerts/shares \
-H 'Authorization: Bearer $TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"label": "dashboard",
"accessLevel": "ro",
"expiresAt": "2026-12-31T23:59:59Z"
}'
pingd-cli shares create \
--topic alerts \
--access ro \
--label dashboard \
--expires-in 30d
import requests
requests.post(
"http://localhost:7685/topics/alerts/shares",
headers={"Authorization": "Bearer TOKEN"},
json={
"label": "dashboard",
"accessLevel": "ro",
"expiresAt": "2026-12-31T23:59:59Z",
},
)
What permissions don't grant
Permissions only affect read/publish for topics, messages, subscriptions, and SSE streams.
They do not grant topic management. Updating or deleting a topic, managing topic
shares or webhooks remains owner/admin-only, and reading admin-only stats remains admin-only
even if another user has rw on the topic.