pingd docs

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

LevelReadPublish
rwyesyes
royesno
wonoyes
denynono

Scopes

ScopeApplies to
userOne specific user
globalAll 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:

PatternMatches
alertsonly the topic alerts
alerts.*alerts and one child level: alerts.cpu, but not alerts.cpu.high
alerts.>alerts and any depth below it
*everything
Slashes are not supported. Use dots: alerts.disk, not alerts/disk.

Resolution order

  1. Admin: always allowed.
  2. Topic owner: always allowed.
  3. Share token: a valid unexpired X-Topic-Token decides access and skips permission/public fallback.
  4. Permissions: registered non-guest users only; matching user and global grants are combined.
  5. Public flags: read falls back to publicRead, publish falls back to publicPublish.

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

UserTopicpublicReadpublicPublishTokenPermissionReadPublish
anonnewstruetrueyesyes
anonnewstruefalseyesno
anonteamfalsefalserwyesyes
anonteamfalsefalseroyesno
jinxsecretsfalsefalseuser royesno
jinxsecretsfalsefalseuser rwyesyes
jinxnewstruetrueuser denynono
opsdeploy.prodfalsefalseglobal ro on deploy.>yesno
guestteamfalsefalseuser rw ignorednono
ownersecretsfalsefalseuser denyyesyes
adminanyanyanyanyanyyesyes

Endpoints

MethodPathWho
GET/permissions/:usernameList user's permissions; admin or self
POST/permissions/:usernameCreate user permission; admin only
GET/permissionsList global permissions; admin only
POST/permissionsCreate global permission; admin only
DELETE/permissions/:idDelete any permission; admin only
GET/topics/:name/sharesList topic shares; owner or admin
POST/topics/:name/sharesCreate topic share; owner or admin
PATCH/topics/:name/shares/:idUpdate share label, access, or expiry
POST/topics/:name/shares/:id/rotateRotate raw share token
DELETE/topics/:name/shares/:idRevoke 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.