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:
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.