Skip to content
Keel
All docs

Notifications

Keel has an in-app notification center at /app/notifications, with an unread badge in the sidebar. Rows live in the notifications table (lib/db/schema/notifications.ts), one per recipient.

What sends one today

| Event | Who gets it | Sent from | |---|---|---| | Invited to a team (if the address already has an account) | The invitee, linked to the accept page | app/api/teams/[teamId]/invites/route.ts | | Someone accepts an invite | The team's owners and admins | app/api/invites/[token]/accept/route.ts | | Your role changes | The member | app/api/teams/[teamId]/members/route.ts | | You're removed from a team | The removed member | same route | | Subscription starts, payment fails, subscription ends | The team's owners | lib/billing/sync.ts |

Sending your own

Call notifyUsers(userIds, input) or notifyTeam(teamId, roles, input, { exceptUserId }) from lib/notifications.ts after the change it describes has committed:

ts
import { notifyTeam } from "@/lib/notifications";

await notifyTeam(teamId, ["owner", "admin"], {
title: "Export finished",
body: "Your CSV is ready to download.",
href: "/app/exports",
});

Both are best-effort: a failed insert is logged and swallowed, so a notification can never fail or roll back the real action. Keep the copy in lib/notification-messages.ts next to the built-in messages so it stays consistent.

Read state

PATCH /api/notifications marks the caller's notifications read, all of them with an empty body, or specific ones with { "ids": ["..."] }. Every update is scoped to the signed-in user's own rows. Clicking a notification marks it read, and "Mark all as read" clears the badge.