8.9 KiB
Plan: Attendance Shift Reminders (pg_cron / Supabase-scheduled)
TL;DR — Use pg_cron in your Supabase Postgres to schedule a SQL job every minute that enqueues due notifications into a Postgres table (scheduled_notifications). A lightweight processor (preferred: Cloud Run worker) will pick up queued notifications, deduplicate via notification_pushes/try_mark_notification_pushed, and call the existing send_fcm function to deliver pushes. This makes scheduling fully server-side and reliable even when user devices are offline.
High-level approach
- Use Postgres-side scheduling (
pg_cron) to run a stored procedure every minute. The stored procedure finds:- shifts starting in ~15 minutes (15-minute reminder), and
- shifts ending now (exact end reminder).
- The procedure enqueues one row per user per reminder into
scheduled_notifications(idempotent insert). - A processor service (Cloud Run worker or Edge Function) polls or streams
scheduled_notificationsrows, marks them usingtry_mark_notification_pushed(notification_id)and callssend_fcmfor delivery.
Why this design?
pg_cronkeeps scheduling close to data and avoids external cron reliance.- Enqueue + processor splits concerns: SQL is simple and reliable; sending FCM is a network task better handled in application code.
- Deduplication and skip-if-checked logic are enforced before enqueue and again at send-time for safety.
Detailed Steps
- Add DB schema and helper functions
-
notification_pushes(dedupe table) — if not present, create: CREATE TABLE IF NOT EXISTS notification_pushes ( notification_id text PRIMARY KEY, created_at timestamptz DEFAULT now(), payload jsonb ); -
scheduled_notifications(queue table): CREATE TABLE IF NOT EXISTS scheduled_notifications ( id bigserial PRIMARY KEY, schedule_id bigint NOT NULL, user_id uuid NOT NULL, notify_type text NOT NULL, -- 'start_15' | 'end' scheduled_for timestamptz NOT NULL, created_at timestamptz DEFAULT now(), processed boolean DEFAULT false, processed_at timestamptz, retry_count int DEFAULT 0, last_error text ); CREATE INDEX ON scheduled_notifications (processed, scheduled_for); ALTER TABLE scheduled_notifications ADD CONSTRAINT IF NOT EXISTS uniq_sched_user_type UNIQUE (schedule_id, user_id, notify_type); -
try_mark_notification_pushedSQL helper (if not present): CREATE OR REPLACE FUNCTION try_mark_notification_pushed(nid text, payload jsonb DEFAULT '{}'::jsonb) RETURNS boolean LANGUAGE plpgsql AS $$ BEGIN INSERT INTO notification_pushes(notification_id, payload) VALUES (nid, payload); RETURN TRUE; EXCEPTION WHEN unique_violation THEN RETURN FALSE; END;;
- Stored procedure to enqueue due reminders
-
Function:
enqueue_due_shift_notifications() -
Behavior:
- Compute current UTC
now(). - For 15-minute reminders: find
duty_scheduleswherestart_atBETWEEN now() + interval '15 minutes' - interval '30 seconds' AND now() + interval '15 minutes' + interval '30 seconds' (1-minute window). Adjust tolerance as desired. - For end reminders: find
duty_scheduleswhereend_atBETWEEN now() - interval '30 seconds' AND now() + interval '30 seconds'. - For each schedule, resolve the assigned
user_id(or multiple participants). - Skip enqueue if
attendance_logsalready shows a check-in for that schedule and remind type isstart_15(you selected this behavior). - Insert into
scheduled_notificationswithON CONFLICT DO NOTHINGto avoid duplicates.
- Compute current UTC
-
Example function outline (PL/pgSQL): CREATE OR REPLACE FUNCTION enqueue_due_shift_notifications() RETURNS void LANGUAGE plpgsql AS $$ DECLARE rec RECORD; BEGIN FOR rec IN SELECT id AS schedule_id, assigned_user_id AS user_id, start_at FROM duty_schedules WHERE start_at BETWEEN now() + interval '15 minutes' - interval '30 seconds' AND now() + interval '15 minutes' + interval '30 seconds' AND status = 'active' LOOP IF EXISTS (SELECT 1 FROM attendance_logs al WHERE al.schedule_id = rec.schedule_id AND al.type = 'check_in') THEN CONTINUE; END IF; INSERT INTO scheduled_notifications (schedule_id, user_id, notify_type, scheduled_for) VALUES (rec.schedule_id, rec.user_id, 'start_15', rec.start_at) ON CONFLICT (schedule_id, user_id, notify_type) DO NOTHING; END LOOP;
FOR rec IN SELECT id AS schedule_id, assigned_user_id AS user_id, end_at FROM duty_schedules WHERE end_at BETWEEN now() - interval '30 seconds' AND now() + interval '30 seconds' AND status = 'active' LOOP INSERT INTO scheduled_notifications (schedule_id, user_id, notify_type, scheduled_for) VALUES (rec.schedule_id, rec.user_id, 'end', rec.end_at) ON CONFLICT (schedule_id, user_id, notify_type) DO NOTHING; END LOOP; END;
;
- Schedule
pg_cronjob
- Enable
pg_cron(Supabase project admin). Schedule: SELECT cron.schedule('shift_reminders_every_min', '*/1 * * * *', $$SELECT enqueue_due_shift_notifications();$$); - Monitor
cron.jobandcron.job_run_detailstables for health.
- Processor options (choose one)
-
Option A — Cloud Run / long-running worker (recommended):
- Worker responsibilities:
- Poll
scheduled_notifications WHERE processed = false AND scheduled_for <= now()in batches, or LISTEN for notifications. - For each row: build
notification_id = 'shift-' || schedule_id || '-' || user_id || '-' || notify_type. - Call
SELECT try_mark_notification_pushed(notification_id, to_jsonb(row)); if it returns false, skip (already sent). - Otherwise, fetch FCM tokens for
user_idfromfcm_tokenstable, and call existingsend_fcmedge function (HTTP) with payload {user_id, title, body, data, notification_id} batching tokens if needed. - Mark scheduled row processed (processed = true, processed_at = now()). On transient failure increment
retry_countand updatelast_error.
- Poll
- Deploy worker to Cloud Run with
SUPABASE_URL,SUPABASE_SERVICE_ROLE_KEY,SEND_FCM_URLas secrets.
- Worker responsibilities:
-
Option B — Supabase Edge Function polling (if you prefer serverless):
- Deploy an Edge Function
process_scheduled_notificationsthat runs quickly and processes a batch then exits. Usepg_cronto call it via HTTP ifpg_httpis enabled, or use GitHub Actions scheduled trigger to call it frequently. Less real-time but serverless-friendly.
- Deploy an Edge Function
-
Option C — Postgres direct HTTP (only if
pg_http/pg_netavailable):- Extend enqueue function to directly POST to
send_fcmendpoint. Not recommended unless your Supabase instance supports and you accept putting external calls in DB.
- Extend enqueue function to directly POST to
- Security & roles
pg_cronruns as the role that created the cron job; ensure it has SELECT onduty_schedulesand INSERT onscheduled_notifications.- Processor uses
SUPABASE_SERVICE_ROLE_KEYfor API calls and to readfcm_tokens.
- Observability, retries, and failure handling
- Add a
retry_countandlast_errortoscheduled_notifications(already included) for DB-visible retries. - Structured logs in the processor:
notification_id,schedule_id,user_id, token_count, status. - Monitor
scheduled_notificationsrows withretry_count> threshold.
- Tests & verification
- Unit tests for
enqueue_due_shift_notifications(). - Manual test: insert a test
duty_schedulesrecord 15 minutes ahead; run the function manually; assert a row inscheduled_notifications. - Run processor in staging and confirm
notification_pusheshas thenotification_idand device receives the push.
Files / artifacts to add
supabase/migrations/xxxx_add_scheduled_notifications.sql— create tables and helper functions above.supabase/migrations/xxxx_add_notification_pushes.sql— if missing (or reuse existing migration).tools/notification_processor/— worker service (Node or Dart) with Dockerfile for Cloud Run.DEPLOYMENT.md— enablepg_cron, run migrations, deploy processor, and set secrets.
Operational notes
- Ensure
start_at/end_atcolumns aretimestamptzand queries are UTC-aware. - Skip-if-checked enforced at enqueue; processor double-checks dedupe before sending.
- Tune time window and polling batch sizes for expected scale.
Verification checklist
- Enable
pg_cronand run migrations to createscheduled_notificationsandnotification_pushes. - Manually call
enqueue_due_shift_notifications()and confirm expected rows are created. - Start processor locally against staging DB; verify processing updates
notification_pushesand callssend_fcm(mock/staging endpoint). - Validate on-device push delivery and skip-if-checked behavior.
Decision / assumptions
- Preferred flow:
pg_cronenqueue + Cloud Run processor. - If DB
httpextensions are available and approved, a simpler single-step approach is possible but less portable. - You asked to skip reminders if user already checked in — implemented at enqueue time.