Skip to content

Inbox

Try the interactive example ↓

Persistent per-user notifications: messages sent on admin-defined channels land in each recipient's inbox and stay there - with read state, priority, thread grouping, interactive actions, receipts, and delivery controls (scheduled delivery, expiry, self-destruct) - until acted on, expired, or self-destructed.

When to reach for it: in-app notification centers, agent-to-human handoff messages, approval prompts, job status updates - anything a user must be able to find later and mark as handled.

When not to: ephemeral realtime fan-out with no persistence or read state belongs to Pub/Sub; service-to-service work dispatch belongs to Message Queue; an append-only activity feed belongs to Timeline. Email is out of scope entirely.

Concepts

  • Channels are named categories (docs4.alerts) created by a platform tenant administrator or live service principal, with creation authority in its workspace. A channel carries policy that every message on it inherits: default priority, who may send, whether actions and scheduled delivery are allowed (a retention_days value is stored and echoed but nothing sweeps on it; expiry is per message). See Channels and delivery.
  • Fan-out at send - one send to N recipients creates N independent messages, each with its own message_id and its own read state.
  • Read state is per message - read/unread, dismissed, archived - and aggregate unread counts come broken down by channel, priority, and thread.
  • Threads group related messages under a caller-chosen thread_id (a job id, a conversation id) and get their own summary endpoint.
  • Actions are buttons attached to a message (approve, link, retry, ...). Executing one records an auditable result; the server never calls the action's target itself - kinds and targets are hints for the client rendering the inbox.
  • Priority is one of low, normal, high, urgent; omitted on send, it falls back to the channel's default (verified: a plain send on a default channel arrived as normal).

Sending

Sending requires channel editor/manager or workspace creation authority. An empty producer allowlist adds no further restriction; a nonempty allowlist rejects other producers with 403 producer_not_allowed, except channel managers. A JWT role string alone does not grant channel authority.

snug inbox send --to user_123 --channel 0199e100-0000-7000-8000-00000000000b \
  --title "Deploy finished" --msg "Build 421 is live"
snug inbox send --to user_123 --to user_456 --channel 0199e100-0000-7000-8000-00000000000b \
  --msg "Ready" --priority high --thread deploy-421
snug inbox batch-send --file messages.json   # JSON array of send requests

Re-sends are made safe with an idempotency key, scoped per channel and recipient - verified: four sends with --idempotency-key docs4-idem-1 all returned the same message_id, and exactly one message existed.

Over HTTP, POST /api/v1/inbox/messages returns 202 with the created ids (captured live; actions attached here because the CLI send command does not take actions):

curl -X POST -H "Authorization: Bearer $SNUG_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"channel_id": "0199e100-0000-7000-8000-00000000000b",
       "recipient_user_ids": ["docs-wave"],
       "title": "Approve rollout", "body": "Promote canary to 100%?",
       "priority": "high",
       "actions": [{"id": "approve", "label": "Promote", "kind": "approve"},
                   {"id": "docs", "label": "Runbook", "kind": "link",
                    "target": "https://example.com/runbook"}]}' \
  http://localhost:4000/api/v1/inbox/messages
{
  "status": 202,
  "msg": "Accepted",
  "data": {
    "message_ids": ["RqehZAdRfEmNDmnjUSHR"],
    "status": "delivered",
    "scheduled": false
  }
}

Reading and read state

snug inbox list --unread
snug inbox list --filter 'channel_id[eq]=0199e100-0000-7000-8000-00000000000b' --filter 'priority[eq]=high' --limit 20
snug inbox get --message-id RqehZAdRfEmNDmnjUSHR --mark-read
snug inbox read --message-id RqehZAdRfEmNDmnjUSHR     # returns unread_total
snug inbox unread --message-id RqehZAdRfEmNDmnjUSHR
snug inbox counts

counts returns the total plus by_channel, by_priority, and by_thread maps. Messages, counts, thread summaries, receipts and action results belong to the recipient. Other users, including channel and workspace managers, receive 404 for that private message. Administration and producer grants do not permit impersonating a recipient. Live membership and enabled principal state are required; token claims alone are insufficient.

list speaks the standard list contract, newest first, 50 per page by default: q over title and body, and filters on channel_id, thread_id, priority, lifecycle, read, dismissed, archived, created_at, and updated_at (e.g. filter[lifecycle][eq]=read). It only ever lists the caller's own delivered messages; --unread (unread=true) keeps the unread ones. Archived and dismissed messages are left out unless --include-archived / --include-dismissed is passed or the request filters on archived or dismissed itself.

Threads

snug inbox threads
snug inbox threads --filter 'unread_count[gt]=0'

Each summary carries message_count, unread_count, highest_priority, and the latest message's id and title - verified with a six-message docs4-job-88 thread. Threads are a list, newest activity first; they filter on channel_id, highest_priority, unread_count, message_count, and latest_at, and sort by unread_count or message_count.

Dismiss, archive, bulk

Dismissed and archived messages keep their data but drop out of default lists until requested back with --include-dismissed / --include-archived (verified: list total went 10 -> 8 and back).

snug inbox dismiss --message-id msg_id            # dismiss
snug inbox dismiss --message-id msg_id --archive  # archive instead
snug inbox bulk --operation mark_read --unread-only --channel 0199e100-0000-7000-8000-00000000000b
snug inbox bulk --operation delete --message-id id1 --message-id id2

Bulk operations are mark_read, mark_unread, dismiss, archive, delete, selected by explicit ids or by filter flags, and return affected plus the new unread_total.

Actions and receipts

snug inbox action --message-id RqehZAdRfEmNDmnjUSHR --action-id approve \
  --idempotency-key act-1
snug inbox receipt --message-id msg_id --kind read --device "ios-app"

Executing an action records a completed MessageActionResult (default result {"acknowledged": true}, or the payload you pass); repeating with the same idempotency key returns the same result_id instead of a new record. An unknown action id is 404 action_not_found. A read receipt also marks the message read (verified) - delivered receipts only append to the receipt log.

Channels and delivery controls

Channel creation (current tenant administrator or live service principal, with workspace creation access), producer allowlists, required technical notes, scheduled delivery, expiry, and self-destruct are on their own page: Channels and delivery.

Limits and configuration

Message bodies up to 32 KiB, 1000 recipients per send, 8 actions per message, 500 items per bulk operation, and 100 messages per batch-send by default - all tunable via the INBOX_* variables.

Reference

Identifiers

Channel, message, receipt and action-result IDs are opaque UUIDv7 strings. Thread names, action names and idempotency keys remain business strings.

Interactive example

Give each person a persistent inbox with unread counts and read state. Notifications can stay available after the live event has passed.

An illustrative example that runs in your browser. No account or API calls required.