Skip to content

Implement Notification Service Using Scheduled Jobs #148

@theosiemensrhodes

Description

@theosiemensrhodes

Summary

Introduce a notificationService that serves as the public interface for all notification scheduling and delivery logic. It should abstract pgBoss job creation/execution and support email now, with a design that can extend to browser push / phone later.

Notifications should be scheduled primarily as event jobs (e.g., one job per class reminder) and resolve recipients + preferences at execution time in bulk, to avoid job fan-out and to ensure preference changes apply immediately.

Depends On

Goals

  • Provide a stable, general-purpose API for scheduling/triggering notifications without leaking pgBoss details.
  • Support scheduled and recurring notifications.
  • Implement a default-safe preferences system:
    • Global defaults per notification type (registry)
    • User-specific overrides stored in a table
    • No migrations/bulk updates required when adding new notification types

Public API (NotificationService)

Expose a small, stable interface:

  • notify({ type, audience, context, deliverAt, recurrence, idempotencyKey })

    • Used for immediate or scheduled notifications.
    • The service decides the job strategy (event job vs per-user) internally.
  • cancel({ idempotencyKey })

  • reschedule({ idempotencyKey, deliverAt })

  • Preferences (for UI + enforcement):

    • setPreference({ userId, type, channel, enabled })
    • clearPreferenceOverride({ userId, type, channel })
    • getEffectivePreferences({ userId }) (for ui)
    • getAllEffectivePreferences({ type, userIds })
    • getEffectivePreference({ type, userId })

Audience Shapes (initial)

  • { kind: "user", userId }
  • { kind: "users", userIds: string[] }
  • { kind: "class", classId }

Default strategy: prefer event jobs for shared domain objects (e.g., one job per class reminder), and resolve recipients + preferences at execution time.

Job Strategy & Execution Model

Event Jobs (recommended default)

Example: class created -> schedule check-in reminder 15 minutes before start.

  • On scheduling:

    • enqueue ONE job keyed by the domain object (e.g., classId + type + deliverAt)
    • persist/calculate an idempotencyKey for cancellation/rescheduling
  • On execution:

    • load the domain object (e.g., class) and validate it still applies (not canceled, still in time window, etc.)
    • resolve recipients (volunteers/instructors/etc.)
    • resolve preferences in bulk (single query per type/channel set)
    • send via enabled channels
    • write audit log / mark sent (idempotency)

Per-User Jobs (only when needed)

Reserved for truly individualized reminders where the audience is inherently per-user and job count is bounded.

Notification Type Registry (Defaults)

Define a central registry of notification types and their default channel behavior.

Example registry entry:

  • type: "available_shifts"
  • defaults:
    • email: true
    • push: true

This registry is the source of truth for:

  • which channels exist for each type
  • default enablement
  • template selection / rendering hooks

Notification Preferences System

Drizzle Table: notification_preferences

Store only overrides. If no row exists, fall back to the registry default.

Columns:

  • userId (FK)
  • type (notification type key; matches registry)
  • channel (e.g. email, push)
  • enabled (boolean)

Constraints & indexes:

  • Unique constraint / PK on (userId, type, channel)
  • Indexes for common lookup patterns:
    • (userId, type) or (userId, type, channel) (point lookup)
    • optionally (type, userId) for fan-out resolution by type

Preference Resolution Logic

When deciding whether to deliver via a channel:

  1. check for override row (userId, type, channel)
  2. if missing, use registry default (type, channel)
  3. effective result determines send/no-send

Implementation note:

  • For multi-recipient notifications, resolve overrides in bulk:
    • WHERE userId IN (...) AND type = ?
    • overlay onto defaults in memory

pgBoss Integration

  • Use pgBoss for scheduling + retries.
  • Jobs should include:
    • type
    • audience (or a domain reference like classId)
    • context
    • idempotencyKey
    • deliverAt / recurrence metadata

Error handling:

  • configure retry policies (attempts/backoff) suitable for email delivery

Sub-issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions