Skip to main content

Overview

OpenCouncil’s notification system automatically sends targeted updates to citizens about council meeting subjects relevant to their location and topic preferences. Notifications are delivered via multiple channels—email, WhatsApp, and SMS—at two key moments: before meetings (agenda preview) and after meetings (summary).

System architecture

The notification system consists of several interconnected data models:
1

Notification

Core record linking a user to a specific meeting and type (before/after)Relations: user, city, meeting, deliveries, subjects
2

NotificationSubject

Links notifications to relevant meeting subjects with a reason (proximity, topic, or general interest)Unique constraint: One entry per (notification, subject) pair
3

NotificationDelivery

Represents an upcoming or completed delivery via email or messageFields: title, body, email, phone, status (pending/sent/failed), messageSentVia (whatsapp/sms)
4

AdministrativeBody

Controls notification behavior with notificationBehavior field:
  • NOTIFICATIONS_DISABLED: No notifications created
  • NOTIFICATIONS_AUTO: Notifications auto-sent immediately
  • NOTIFICATIONS_APPROVAL: Notifications require admin approval

Notification creation triggers

Notifications are created automatically when specific tasks complete, or manually by administrators.
Created when processAgenda task completes successfullyContent: Preview of meeting subjects based on processed agendaBehavior: Controlled by administrative body’s notificationBehavior setting

Subject-to-user matching

The notification system uses sophisticated matching logic to determine which users should be notified about which subjects.

Subject importance levels

Each subject has two importance dimensions:

Topic importance

  • doNotNotify: No topic-based notifications
  • normal: Notify users interested in the topic
  • high: Notify all users (general interest)

Proximity importance

  • none: No location-based notifications
  • near: Notify users within 250m
  • wide: Notify users within 1000m

Matching rules

A user receives a notification for subject S if any of these conditions are met:
1

General interest

S.topicImportance is high → Notify everyone
2

Topic match

S.topicImportance is normal AND user has the subject’s topic in their interests
3

Near proximity

S.proximityImportance is near AND user has a location within 250m of subject location
4

Wide proximity

S.proximityImportance is wide AND user has a location within 1000m of subject location
If no subjects match any user through these rules, no notifications are created at all.

Statistics

The creation process produces two key metrics:
  • Notifications created: Number of users notified (one notification per user)
  • NotificationSubjects created: Total subjects across all users

Notification deliveries

NotificationDeliveries represent the actual sending of notifications through specific channels.

Delivery lifecycle

1

Creation

All deliveries are initially created with pending status:
  • Email delivery: Always created for every notification
  • Message delivery: Created additionally if user has a phone number
2

Sending

Deliveries are sent either through:
  • Admin approval via the notification approval UI, OR
  • Immediate sending (for “send immediately” or NOTIFICATIONS_AUTO bodies)

Email delivery

Title format: OpenCouncil {municipalityName}: {adminBody} - {date}Body content: Formatted HTML email containing:
  • Titles, topics, and descriptions of all notification subjects
  • Navigation buttons (top and bottom) linking to the notification view page
Future enhancement: LLM-generated titles incorporating keywords from important subjects
  • Sent using Resend service
  • Uses title and body created during delivery creation
  • Status updated to sent on success

Message delivery (WhatsApp/SMS)

Message delivery flow

Service: All message delivery handled through Bird APIWhatsApp-first approach:
  1. Attempts WhatsApp delivery using pre-approved templates
  2. Falls back to SMS via Bird if WhatsApp fails
Status tracking: messageSentVia field tracks actual delivery method (whatsapp/sms)

WhatsApp template system

WhatsApp requires pre-approved templates for users who haven’t messaged in 24 hours (always assumed).
Used for beforeMeeting notificationsParameters:
  • date: Meeting date
  • cityName: City name (e.g., “Athens”)
  • subjectsSummary: Comma-separated subject titles
  • adminBody: Administrative body name
  • notificationId: For redirect button to /notifications/{id}
Both templates take exactly the same parameters for consistency.

SMS fallback

If WhatsApp delivery fails, the system uses SMS with the body content created during delivery creation.

Administrative body notification flow

Each administrative body has a notificationBehavior setting that controls the notification lifecycle:
Behavior: Skip notification creation entirelyUse case: Bodies that don’t want any automated notifications

Core implementation

The notification system is implemented across several key modules:

Matching engine

src/lib/notifications/matching.ts
export async function matchUsersToSubjects(
  subjects: Subject[],
  usersWithPreferences: UserPreference[],
  subjectImportanceOverrides?: Record<string, SubjectImportance>
): Promise<Map<string, Set<SubjectMatch>>>
Matches users to subjects based on importance levels and preferences, using PostGIS for proximity calculations.

Notification creation

src/lib/db/notifications.ts
export async function createNotificationsForMeeting(
  cityId: string,
  meetingId: string,
  type: NotificationType,
  subjectImportanceOverrides?: Record<string, ImportanceSettings>
): Promise<{
  notificationsCreated: number;
  subjectsTotal: number;
  notificationIds: string[];
}>
Core function that:
  1. Checks administrative body notification behavior
  2. Gets all users with preferences for the city
  3. Applies importance rules and matches subjects to users
  4. Creates Notification records with NotificationSubjects
  5. Creates NotificationDeliveries (email + message if phone exists)
  6. Optionally sends immediately if NOTIFICATIONS_AUTO

Proximity calculations

src/lib/db/notifications.ts
export async function calculateProximityMatches(
  userLocationIds: string[],
  subjectLocationId: string,
  distanceMeters: number
): Promise<boolean>
Uses PostGIS ST_DWithin to calculate geographic distances, with special handling for inverted coordinates outside Greece bounds.

Delivery system

src/lib/notifications/deliver.ts
export async function releaseNotifications(
  notificationIds: string[]
): Promise<{
  success: boolean;
  emailsSent: number;
  messagesSent: number;
  failed: number;
}>
Processes pending deliveries:
  • Sends emails via Resend
  • Sends messages via Bird (WhatsApp → SMS fallback)
  • Updates delivery status and messageSentVia
  • Implements 500ms delays to avoid rate limiting

Content generation

Notification content is generated at creation time:
export async function generateEmailContent(
  notification: NotificationData
): Promise<{ title: string; body: string }>
Email content includes formatted HTML with subject cards, topics, and navigation buttons. SMS content is plain text for fallback use.

Automatic triggers

Notifications are automatically triggered from task handlers:
src/lib/tasks/processAgenda.ts
// After successful subject creation
const stats = await createNotificationsForMeeting(
  task.councilMeeting.cityId,
  task.councilMeeting.id,
  'beforeMeeting'
);

Admin notification management

Administrators manage notifications through /admin/notifications with:

Administrative body settings

Configure notificationBehavior per body with impact summaries

Notification filters

Filter by status, city, administrative body, date range

Grouped notification list

Hierarchical view: City → Admin Body → Meeting → Type (before/after)

Bulk actions

Release all, release selected, view failed deliveries, convert to auto

User preference management

Users manage their notification preferences through a simple profile integration:
1

Profile section

NotificationPreferencesSection component shows active cities with preferences
2

Per-city actions

  • Edit button: Redirects to existing /{cityId}/notifications UI
  • Unsubscribe button: Deletes NotificationPreference record
3

Existing infrastructure

Reuses existing onboarding flow for preference editing:
  • Location map interface
  • Topic selection
  • saveNotificationPreferences() function

Security and authorization

  • Only super admins can access /admin/notifications
  • City admins can create notifications for their city’s meetings
  • Delivery content is sanitized before sending
  • Unsubscribe links include secure tokens

Performance considerations

500ms delays between delivery attempts prevent rate limit issues with email and messaging services.
Bulk operations process notifications in chunks to prevent timeouts on large datasets.
Strategic indexes on status, createdAt, and composite keys optimize common query patterns.
Proximity calculations use PostGIS geography type for accurate distance measurements on Earth’s surface.

File reference

Key implementation files

  • src/lib/db/notifications.ts - Core notification creation and management
  • src/lib/notifications/matching.ts - Subject-to-user matching logic
  • src/lib/notifications/deliver.ts - Multi-channel delivery system
  • src/lib/notifications/content.ts - Email and SMS content generation
  • src/lib/notifications/bird.ts - Bird API integration (WhatsApp/SMS)
  • prisma/schema.prisma - Notification data models (lines 920-1013)

Build docs developers (and LLMs) love