Vault

Inbox

Manage in-app notifications with read/unread tracking, pagination, metadata, and action URLs.

Herald's inbox subsystem provides a persistent store for in-app notifications. When a notification is sent on the inapp channel, Herald automatically creates an inbox entry for the target user. The inbox supports read/unread management, pagination, metadata, expiration, and action URLs.

Notification Entity

The inbox.Notification struct represents a single in-app notification:

type Notification struct {
    ID        id.InboxID        `json:"id"`
    AppID     string            `json:"app_id"`
    EnvID     string            `json:"env_id,omitempty"`
    UserID    string            `json:"user_id"`
    Type      string            `json:"type"`              // template slug
    Title     string            `json:"title"`
    Body      string            `json:"body,omitempty"`
    ActionURL string            `json:"action_url,omitempty"`
    ImageURL  string            `json:"image_url,omitempty"`
    Read      bool              `json:"read"`
    ReadAt    *time.Time        `json:"read_at,omitempty"`
    Metadata  map[string]string `json:"metadata,omitempty"`
    ExpiresAt *time.Time        `json:"expires_at,omitempty"`
    CreatedAt time.Time         `json:"created_at"`
}

Notification IDs use the hinb prefix (e.g., hinb_01j8x7k2...).

Automatic Creation

Inbox notifications are created automatically during the delivery pipeline when the channel is inapp and a UserID is provided:

// Inside Herald.Send(), after the inapp driver returns success:
if channel == string(ChannelInApp) && req.UserID != "" {
    _ = h.store.CreateNotification(ctx, &inbox.Notification{
        ID:        id.NewInboxID(),
        AppID:     req.AppID,
        EnvID:     req.EnvID,
        UserID:    req.UserID,
        Type:      req.Template,
        Title:     rendered.Title,
        Body:      rendered.Text,
        Metadata:  req.Metadata,
        CreatedAt: time.Now().UTC(),
    })
}

The Type field is set to the template slug, making it easy to filter and identify notification types in the inbox.

Read/Unread Management

Notifications start in an unread state (Read: false). Herald provides operations to mark individual or all notifications as read.

Mark Single Notification as Read

err := store.MarkRead(ctx, notifID)

This sets Read to true and records the current time in ReadAt.

Mark All Read

err := store.MarkAllRead(ctx, appID, userID)

Marks every unread notification for the given user and application as read in a single operation.

Unread Count

count, err := store.UnreadCount(ctx, appID, userID)

Returns the number of unread notifications. This is useful for displaying a badge count in your application UI.

Pagination

Inbox notifications are listed with limit/offset pagination:

notifications, err := store.ListNotifications(ctx, appID, userID, limit, offset)

Notifications are returned ordered by creation time (newest first). The default page size when using the API is 50.

Metadata and Action URLs

Metadata

The Metadata field carries arbitrary key-value pairs passed from the send request. Use this for application-specific context like linking to specific resources:

result, err := h.Send(ctx, &herald.SendRequest{
    AppID:    "app_01abc...",
    Channel:  "inapp",
    Template: "auth.invitation",
    UserID:   "user_01xyz...",
    To:       []string{"user_01xyz..."},
    Data: map[string]any{
        "inviter_name": "Bob",
        "org_name":     "Acme Corp",
        "role":         "admin",
    },
    Metadata: map[string]string{
        "org_id":        "org_01abc...",
        "invitation_id": "inv_01def...",
    },
})

Action URL

The ActionURL field provides a deep link or URL that the user can follow when interacting with the notification (e.g., clicking "Accept Invitation"):

notification := &inbox.Notification{
    // ...
    ActionURL: "/invitations/inv_01def.../accept",
}

Image URL

The ImageURL field optionally provides an avatar or icon image to display alongside the notification.

Expiration

The optional ExpiresAt field allows notifications to carry an expiry time. Your application can filter out expired notifications at read time.

Store Interface

The inbox.Store interface defines all inbox persistence operations:

type Store interface {
    CreateNotification(ctx context.Context, n *Notification) error
    GetNotification(ctx context.Context, notifID id.InboxID) (*Notification, error)
    DeleteNotification(ctx context.Context, notifID id.InboxID) error
    MarkRead(ctx context.Context, notifID id.InboxID) error
    MarkAllRead(ctx context.Context, appID string, userID string) error
    UnreadCount(ctx context.Context, appID string, userID string) (int, error)
    ListNotifications(ctx context.Context, appID string, userID string, limit, offset int) ([]*Notification, error)
}

API Endpoints

The inbox is managed through the /inbox route group:

MethodPathOperation
GET/inboxList inbox notifications (query: app_id, user_id, limit, offset)
GET/inbox/unread/countGet unread count (query: app_id, user_id)
PUT/inbox/:id/readMark a single notification as read
PUT/inbox/read-allMark all notifications as read (query: app_id, user_id)
DELETE/inbox/:idDelete a notification

Unread Count Response

{
  "count": 3
}

List Inbox Example

GET /herald/inbox?app_id=app_01abc...&user_id=user_01xyz...&limit=20&offset=0

Returns an array of Notification objects ordered by created_at descending.

Using the Inbox with Notify

The most common pattern is to include "inapp" in the channels list of a Notify call so the user receives both an external notification and an inbox entry:

results, err := h.Notify(ctx, &herald.NotifyRequest{
    AppID:    "app_01abc...",
    Template: "auth.new-device-login",
    Channels: []string{"email", "inapp"},
    UserID:   "user_01xyz...",
    To:       []string{"alice@example.com"},
    Data: map[string]any{
        "user_name":   "Alice",
        "app_name":    "MyApp",
        "device_info": "Chrome on macOS",
        "location":    "San Francisco, CA",
    },
})

This sends the email through the configured email provider and simultaneously creates an inbox notification for the user.

On this page