Valcraven Docs
Features

Notifications

In-app notifications with a live NotificationBell, plus Expo push notifications to registered mobile devices.

Notifications

Valcraven has a built-in per-user notification system: create a notification from any server code and it appears instantly in the user's NotificationBell (the header dropdown in the /app shell) over a WebSocket, and — if they have the mobile app installed — as an Expo push notification. Notifications persist to the database so they survive reloads and offline periods.

Where it lives

PiecePath
Notification helpersapps/web/lib/notifications.ts
Header bell (UI)apps/web/components/notification-bell.tsx
List / count APIapps/web/app/api/notifications/route.ts
Mark read / read-all APIapps/web/app/api/notifications/[id]/read, .../read-all
Push token registrationapps/web/app/api/push-tokens/route.ts
Tablesnotifications, push_tokens

Creating a notification

The one function you call is createNotification():

import { createNotification } from "@/lib/notifications";

await createNotification(
  userId,
  "success",              // "info" | "success" | "warning" | "error"
  "Payment received",     // title
  "Your invoice is paid.", // message (optional)
);

A single call does three things:

  1. Persists a row to the notifications table (unread).
  2. Publishes a new-notification event to the realtime channel notifications:<userId> so any open browser tab updates instantly.
  3. Sends Expo push to every registered device token for that user (fire-and-forget).

The realtime publish is best-effort and never throws — if the realtime Worker is unavailable the notification still persists and the bell picks it up on its next load. See Realtime.

Don't confuse these with toast notifications. useToast() (@/lib/use-toast) shows transient in-page toasts and does not persist. createNotification() is for durable, per-user notifications that belong in the bell.

The NotificationBell

components/notification-bell.tsx renders in the app-shell header. It:

  • Loads the user's notifications and unread count on mount from GET /api/notifications.
  • Subscribes to notifications:<userId> via the useRealtime hook; on a new-notification event it prepends the item and bumps the unread badge — no polling.
  • Shows an unread count badge (capped at 9+).
  • Lets the user mark a single item read or Mark all read.

Because it uses the typed @valcraven/core API client, the same list/mark-read calls work from the mobile app.

API

All routes require an authenticated session and are scoped to the current user.

Method + pathPurpose
GET /api/notifications?limit=&offset=List notifications (newest first) + unreadCount
POST /api/notifications/[id]/readMark one notification read
POST /api/notifications/read-allMark all read; returns the count updated

limit is clamped to 1–100 (default 50). The list response shape is { notifications, unreadCount }.

Push notifications (mobile)

Push targets the mobile app via Expo's push service. When a device registers, the app calls:

POST /api/push-tokens   { token, platform: "ios" | "android" }
DELETE /api/push-tokens { token }

Tokens are stored in push_tokens, keyed uniquely on the token (upsert on conflict), scoped to user_id. When you create a notification, lib/notifications.ts looks up all of the user's tokens and POSTs a batch to https://exp.host/--/api/v2/push/send with the title, body, and { type: "notification", notificationId } payload. This is fire-and-forget — a push failure is logged but never propagated, and users with no registered device simply get nothing.

No configuration is required for this to be a safe no-op: with zero registered tokens, the push step returns early. See the mobile app for the client side of token registration.

Data model

notifications columns of note: type (info/success/warning/error, default info), title, message, read (0/1), createdAt (unix epoch). push_tokens holds token, platform (default ios), and timestamps, with a unique index on token.

Extending

  • New notification type styling — the four types map to bell styling; add server-side triggers by calling createNotification() wherever the event happens (payment succeeded, item shared, etc.).
  • Broadcast to many users — loop over user ids and call createNotification() for each; there's no built-in fan-out to "all users".
  • Realtime — the WebSocket channel that delivers live notifications
  • Toasts: useToast() from @/lib/use-toast for transient in-page messages

On this page