ServicesInbox

Inbox

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 or swept by retention.

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 admin or service principal. A channel carries policy that every message on it inherits: default priority, retention, who may send, whether actions and scheduled delivery are allowed. 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

Anyone authenticated can send on a channel with an empty producer allowlist; restricted channels reject other senders with 403 producer_not_allowed.

snug inbox send --to user_123 --channel KunCSRQzLPryQwbHxSfp \
  --title "Deploy finished" --msg "Build 421 is live"
snug inbox send --to user_123 --to user_456 --channel KunCSRQzLPryQwbHxSfp \
  --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": "KunCSRQzLPryQwbHxSfp",
       "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 --channel KunCSRQzLPryQwbHxSfp --priority high --page-size 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. Reading another user's message fails with 403 insufficient_permissions ("cannot access another user's message"), not a silent 404 - verified with a second identity.

The HTTP list endpoint also takes filter expressions in the shared query grammar (filter=lifecycle:eq:read) over the tagged fields - channel, recipient, thread, priority, lifecycle, and the read/dismissed/archived/scheduled flags.

Threads

snug inbox threads

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.

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 KunCSRQzLPryQwbHxSfp
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 administration (admin or service principal only), 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 in the Inbox CONFIG reference.

Reference

On this page