Overview
The notification system consists of five database tables, a Supabase Edge Function for dispatching reminders, Web Push support in the admin dashboard, and native push in the mobile app. This guide covers the architecture, database schema, and extensibility patterns.
Architecture
┌──────────────────┐
│ Reminder Engine │
│ (Edge Function) │
└────────┬─────────┘
│ evaluates schedules
▼
┌──────────────────────────────────┐
│ reminder_schedules │
│ + reminder_schedule_day_overrides│
└──────────────┬───────────────────┘
│ creates
▼
┌─────────────────────┐
│ notifications │◄── read/manage by users
└──────┬──────────────┘
│ dispatches
┌──────────┴──────────┐
▼ ▼
┌───────────────┐ ┌─────────────────────┐
│ Web Push │ │ Mobile Push / Email │
│ (browser SW) │ │ (Expo + Resend) │
└───────────────┘ └─────────────────────┘
Flow
- The
dispatch-reminders Edge Function runs on a cron schedule
- It evaluates
reminder_schedules + reminder_schedule_day_overrides for the current time and day
- For each schedule due, it checks if a recent submission exists (smart skip)
- If a reminder is needed, it inserts a row into
notifications
- It also dispatches to external channels: Web Push via
web_push_subscriptions, mobile push via Expo, and email via Resend (if reminder_emails_enabled is true on the user)
Database Schema
notifications
Stores all in-app notifications for users.
| Column | Type | Description |
|---|
id | UUID | Primary key |
user_id | UUID | References auth.users |
auction_id | UUID | Optional — scopes to an auction |
type | TEXT | Notification type (e.g., submission_reminder, system) |
title | TEXT | Display title |
body | TEXT | Optional body text |
data | JSONB | Arbitrary metadata (URLs, IDs, etc.) |
is_read | BOOLEAN | Read status |
read_at | TIMESTAMPTZ | When marked as read |
created_at | TIMESTAMPTZ | Creation timestamp |
RLS policies: Users can read, update, and delete their own notifications. Service role can insert and delete any.
web_push_subscriptions
Stores Web Push API subscription objects for the admin dashboard.
| Column | Type | Description |
|---|
id | UUID | Primary key |
user_id | UUID | References auth.users |
endpoint | TEXT | Push service endpoint URL |
p256dh | TEXT | Client public key |
auth | TEXT | Auth secret |
created_at | TIMESTAMPTZ | Subscription creation time |
RLS policies: Users can manage their own subscriptions. Service role reads all for dispatch.
reminder_schedules
Defines reminder schedule configurations with a three-level hierarchy.
| Column | Type | Description |
|---|
id | UUID | Primary key |
auction_id | UUID | NULL = global default |
location_id | UUID | NULL = auction-wide |
mode | TEXT | interval or deadline |
is_enabled | BOOLEAN | Whether this schedule is active |
start_time | TIME | Start of the reminder window (interval mode) |
end_time | TIME | End of the reminder window (interval mode) |
interval_minutes | INTEGER | Minutes between reminders (interval mode) |
deadlines | JSONB | Array of deadline times (deadline mode) |
advance_reminder_minutes | JSONB | Array of advance warning times (deadline mode) |
smart_skip_enabled | BOOLEAN | Skip if submission already exists |
timezone | TEXT | IANA timezone for schedule evaluation |
created_at | TIMESTAMPTZ | Creation timestamp |
updated_at | TIMESTAMPTZ | Last update timestamp |
Partial unique indexes enforce the hierarchy:
- One global default (where
auction_id IS NULL AND location_id IS NULL)
- One per auction (where
location_id IS NULL)
- One per location
reminder_schedule_day_overrides
Per-day-of-week overrides for a reminder schedule.
| Column | Type | Description |
|---|
id | UUID | Primary key |
schedule_id | UUID | References reminder_schedules |
day_of_week | INTEGER | 0 = Sunday through 6 = Saturday |
is_enabled | BOOLEAN | Whether reminders fire on this day |
start_time | TIME | Override start time (NULL = inherit) |
end_time | TIME | Override end time (NULL = inherit) |
interval_minutes | INTEGER | Override interval (NULL = inherit) |
deadlines | JSONB | Override deadlines (NULL = inherit) |
advance_reminder_minutes | JSONB | Override advance times (NULL = inherit) |
Unique constraint: (schedule_id, day_of_week) — one override per day per schedule.
reminder_logs
Tracks dispatched reminders for auditing and smart-skip logic.
| Column | Type | Description |
|---|
id | UUID | Primary key |
schedule_id | UUID | Which schedule triggered this |
location_id | UUID | The affected location |
dispatched_at | TIMESTAMPTZ | When the reminder was sent |
skipped | BOOLEAN | Whether smart skip prevented delivery |
recipient_count | INTEGER | Number of users notified |
Edge Function: dispatch-reminders
The dispatch-reminders Edge Function is the core engine that evaluates schedules and sends reminders.
Invocation
The function is designed to be called on a cron schedule (e.g., every 5 minutes) via Supabase’s pg_cron or an external scheduler.
Logic
1. Fetch all enabled reminder_schedules
2. For each schedule:
a. Resolve the effective config (with day overrides for today)
b. Determine if a reminder is due now based on mode:
- Interval: check if current time is within window and interval elapsed
- Deadline: check if any deadline advance time matches
c. If due, check smart_skip:
- Query recent submissions for the schedule's location(s)
- If submission exists in current period, skip
d. If not skipped:
- Insert notification rows for each assigned user
- Dispatch Web Push to web_push_subscriptions
- Send email via Resend (if user.reminder_emails_enabled)
- Send mobile push via Expo push API
e. Log to reminder_logs
Environment Variables
| Variable | Description |
|---|
SUPABASE_URL | Supabase project URL |
SUPABASE_SERVICE_ROLE_KEY | Service role key for DB access |
RESEND_API_KEY | Resend API key for email delivery |
VAPID_PUBLIC_KEY | VAPID public key for Web Push |
VAPID_PRIVATE_KEY | VAPID private key for Web Push |
VAPID Key Configuration
Web Push requires VAPID (Voluntary Application Server Identification) keys for authentication between the server and push services.
Generating VAPID Keys
Generate a VAPID key pair using the web-push npm package:
npx web-push generate-vapid-keys
This outputs a public and private key. Store them as environment variables:
NEXT_PUBLIC_VAPID_PUBLIC_KEY — Used by the admin dashboard client
VAPID_PRIVATE_KEY — Used by the Edge Function (server-side only)
Never expose the VAPID private key to the client. Only the public key is used in the browser.
Adding New Notification Types
The notification system is extensible. To add a new notification type:
Define the Type
Choose a type string (e.g., inspection_complete) and document it
Insert Notifications
From your server action or Edge Function, insert rows into the notifications table with the new type:await serviceClient.from('notifications').insert({
user_id: userId,
auction_id: auctionId,
type: 'inspection_complete',
title: 'Inspection Completed',
body: `Vehicle ${vin} passed inspection`,
data: { inspectionId, vin },
});
Update UI (Optional)
Add an icon mapping for the new type in the notification components:
- Admin:
NotificationItem.tsx icon switch
- Mobile:
NotificationsScreen.tsx icon mapping
Add Push Dispatch (Optional)
If the new type should send push notifications or emails, add dispatch logic to the relevant server action or create a new Edge Function
RLS Policies Summary
| Table | SELECT | INSERT | UPDATE | DELETE |
|---|
notifications | Own rows | Service role | Own rows (mark read) | Service role (verified ownership) |
web_push_subscriptions | Own rows | Own rows | Own rows | Own rows |
reminder_schedules | Members see auction schedules; authenticated see global | Super admin (global) / manager+ (auction) | Same as insert | Same as insert |
reminder_schedule_day_overrides | Same as parent schedule | Same as parent schedule | Same as parent schedule | Same as parent schedule |
reminder_logs | Members see auction logs | Service role | None | None |
Testing Locally
Testing Notifications
- Insert a notification directly via SQL or the Supabase dashboard:
INSERT INTO notifications (user_id, type, title, body)
VALUES ('your-user-id', 'system', 'Test Notification', 'This is a test');
- Open the admin dashboard — the notification bell should show a badge
- Click the bell to see the notification in the panel
Testing Web Push
- Set
NEXT_PUBLIC_VAPID_PUBLIC_KEY in your admin .env.local
- Enable browser notifications in the notification panel
- Accept the browser permission prompt
- Send a test push via the Edge Function or directly via the
web-push library
Testing Reminders
- Create a reminder schedule via the admin UI or SQL
- Invoke the
dispatch-reminders Edge Function manually:
supabase functions invoke dispatch-reminders --no-verify-jwt
- Check the
notifications table and reminder_logs for results