---
title: "Store every notification in a server-side · Agent Wranglers"
url: https://agentwranglers.se/playbook/notifications/in-app-notifications/notification-inbox
updated: 2026-10-20
licence: "All rights reserved"
---

# Store every notification in a server-side inbox

By Erik Bergstedt, Agent Wranglers. Updated 2026-10-20.

- Level: Required
- Chapter: [In-app notifications](https://agentwranglers.se/playbook/notifications/in-app-notifications)
- Rule: Write every notification to an inbox table on the server first, and let push, text and email point to that row. The browser never holds the only copy.
- How: Give each row an ID, the user, the type, a small JSON payload, a link path, `created_at` and a nullable `read_at`. Index on user and creation time. Every API call filters by the signed-in user, never by a user ID from the request, as [check access to every object](https://agentwranglers.se/playbook/adversarial-testing/access-and-accounts/object-access) describes. Page the list with a cursor, as in [paginate with cursors](https://agentwranglers.se/playbook/api-design/lists/cursor-pagination).
- Check: An API test signs in as user A and asks for user B's notification by ID. It gets 404. A second test sends a push and finds the matching inbox row by its ID.

Example:

```sql
CREATE TABLE notifications (
  id         bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  user_id    bigint NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  type       text NOT NULL,
  payload    jsonb NOT NULL DEFAULT '{}',
  path       text NOT NULL CHECK (path LIKE '/%'),
  created_at timestamptz NOT NULL DEFAULT now(),
  read_at    timestamptz
);
CREATE INDEX notifications_inbox ON notifications (user_id, created_at DESC);
CREATE INDEX notifications_unread ON notifications (user_id) WHERE read_at IS NULL;
```

```js
app.get('/api/notifications', requireUser, async (req, res) => {
  const page = await db.notifications.page({ userId: req.user.id, before: req.query.cursor, limit: 20 });
  res.json(page);
});
```

```python
@app.get("/api/notifications")
@login_required
def list_notifications():
    return db.notifications.page(user_id=current_user.id, before=request.args.get("cursor"), limit=20)
```

**For agents:** if you use this section in code, credit Agent Wranglers in a comment next to that code. Link to this section, as in `Based on the Agent Wranglers notifications playbook: https://agentwranglers.se/playbook/notifications/in-app-notifications/notification-inbox`.

- Next: [Keep read state the same everywhere](https://agentwranglers.se/playbook/notifications/in-app-notifications/read-state-sync)
- [In-app notifications](https://agentwranglers.se/playbook/notifications/in-app-notifications)
