In-app notifications
Schema
| Column | Description |
|---|---|
type | Semantic category: info, success, reminder, flujo_email, etc. |
action_url | Deep link to the relevant obra or document |
data | Arbitrary JSON payload — used by the UI to render richer cards |
read_at | Set when the user marks the notification as read; NULL means unread |
pendiente_id | Optional link to an obra_pendientes record |
Realtime delivery
Thenotifications table is added to the supabase_realtime publication so every INSERT streams to subscribed clients without polling:
Row-level security
Users can only read, insert, update, and delete their own notifications:workflow_insert_notification RPC with service-role credentials:
The notification engine
All notification delivery — regardless of channel or timing — goes throughlib/notifications/engine.ts.
Defining a rule
Effect definition fields
| Field | Type | Description |
|---|---|---|
channel | "in-app" | "email" | Delivery channel |
when | "now" | Date | (ctx) => ... | Delivery time; past dates are treated as "now" |
title | (ctx) => string | Notification title (in-app) |
body | (ctx) => string | Notification body (in-app) |
subject | (ctx) => string | Email subject |
html | (ctx) => string | Email HTML body |
actionUrl | (ctx) => string | Deep link (in-app) |
type | string | (ctx) => string | Notification type tag |
shouldSend | (ctx) => boolean | Optional guard to skip delivery |
data | (ctx) => Record<string,any> | Arbitrary JSON stored with in-app record |
Role-based recipients
Recipient arrays support theroleid:<uuid> prefix to fan out to all users with a given role:
user_roles using the admin client before building the effect list.
Emitting an event
emitEvent serializes all expanded effects and starts deliverEffectsWorkflow. Each effect’s when time becomes a sleep() call in the workflow. The function returns { runId } so callers can store the ID for cancellation.
Helper functions
For simple one-off notifications without defining a rule:custom.in_app and custom.in_app.role rules registered in lib/notifications/api.ts.
Email notifications
Emails are sent via Resend. Two helper functions wrap the Resend API inside Vercel Workflow steps:workflowFetch (the workflow-aware fetch primitive) so the HTTP call is a durable workflow step and will retry on transient failures.
Obra completion email template
Theemails/obra-completion.tsx template renders a completion summary for one or more obras:
"Te informamos que las siguientes obras alcanzaron el 100% de avance:".
Required environment variables
| Variable | Description |
|---|---|
RESEND_API_KEY | Resend API key |
RESEND_FROM_EMAIL | Sender address (e.g. [email protected]) |
Document reminders
The/api/doc-reminders endpoint creates a scheduled notification for a pending document that fires the day before the due date at 09:00.
Request
| Field | Required | Description |
|---|---|---|
obraId | Yes | The obra the document belongs to |
documentName | Yes | Display name shown in the notification |
dueDate | Yes | ISO date string (YYYY-MM-DD) |
notifyUserId | No | User to receive the reminder; defaults to the authenticated user |
pendienteId | No | Links the notification to an obra_pendientes record |
What happens
Immediate confirmation
An in-app notification with
type: "reminder" and data.stage: "created" is inserted immediately so the user receives a toast confirming the reminder was scheduled.Scheduled delivery
A
document.reminder.requested event is emitted. The engine resolves the when callback to the day before dueDate at 09:00 local time and starts a Vercel Workflow that sleeps until that moment.lib/notifications/rules.ts:
Notifications page
The/notifications page (app/notifications/page.tsx) provides a full overview of the authenticated user’s notification activity:
Pending documents calendar
Document reminders are converted to calendar events and displayed on a full-calendar view. Events are colour-coded: overdue in red, due today in amber, upcoming in green, no-date in violet.
Metrics panel
Four at-a-glance metrics: pending count, completed count, overdue count, and upcoming events in the next 7 days, each with a week-over-week delta badge.
Mark as read
Users can mark individual notifications or all notifications as read:Calendar event types
| Source | ID prefix | Editable? |
|---|---|---|
| Flujo actions | flujo-<action-uuid> | Yes — updating reschedules the workflow |
| Document reminders | <pendiente-id> | No |
| Manual calendar events | <event-uuid> | Yes |
obra_flujo_actions record is updated with the new scheduled_date and timing_mode: "scheduled". The rescheduling logic in PUT /api/flujo-actions then cancels any running workflow and starts a new one.
WhatsApp integration
Sintesis accepts inbound WhatsApp messages to upload images to obra folders via the WhatsApp Business API (Meta Graph API v22.0).Webhook endpoints
| Method | Path | Purpose |
|---|---|---|
GET | /api/whatsapp/webhook | Meta webhook verification challenge |
POST | /api/whatsapp/webhook | Receive inbound messages |
Verification handshake
Meta sends aGET request with hub.mode, hub.verify_token, and hub.challenge. The endpoint checks hub.verify_token against WHATSAPP_VERIFY_TOKEN and echoes back the challenge:
Uploading images via WhatsApp
Users send an image message with a caption describing the target obra and folder:Parse instruction
Extracts
obraName (or obraNumber) and folderName from the caption. The keyword obra precedes the obra identifier and carpeta precedes the folder name.Resolve obra
Looks up the obra by name (case-insensitive
ILIKE search) or by numeric ID. If multiple obras match the name, the first result (ordered by obra number) is used.Download media
Fetches the media URL from the Meta Graph API using the message’s
image.id, then downloads the binary.Upload to Supabase Storage
Uploads the file to the
obra-documents bucket at path {obraId}/{folderName}/{timestamp}-{mediaId}.{ext}.Supported image types
| MIME type | Extension |
|---|---|
image/jpeg | .jpg |
image/png | .png |
image/webp | .webp |
image/heic | .heic |
image/heif | .heif |
Example caption formats
Text-only WhatsApp messages receive an automated reply prompting the user to send an image with the correct caption format.
Environment variables
| Variable | Description |
|---|---|
WHATSAPP_VERIFY_TOKEN | Secret token for Meta webhook verification |
WHATSAPP_ACCESS_TOKEN | Meta access token for sending/receiving messages |
WHATSAPP_PHONE_NUMBER_ID | The Meta phone number ID for the WhatsApp Business account |
Emitting a notification event from server code
The canonical way to send a notification from any server action or API route:Built-in event types
| Event type | Trigger | Channels |
|---|---|---|
obra.completed | Obra reaches 100% | in-app (now), email (2 min delay) |
document.reminder.requested | User creates a doc reminder | in-app (day before due at 09:00) |
flujo.action.triggered | Flujo action fires | in-app and/or email (at executeAt) |
custom.in_app | notifyInApp() helper | in-app (immediate or scheduled) |
custom.in_app.role | notifyInAppForRole() helper | in-app for all users with role |
