Skip to main content

Notifications

Notifications are internal only. There are no public or partner API endpoints for creating subscriptions or sending notifications.

The domain lives in app/lib/notifications:

  • schema.ts: supported events, channels, statuses, topics, and payload types.
  • actions.ts: subscription CRUD, event triggering, notification row creation, and email sending.

Tables

subscriptions defines who should receive a notification.

id
event
channel
destination
topic
target
created_at
updated_at

notifications is the delivery audit log.

id
subscription_id
event
channel
destination
person_id
payload
status
message
created_at
updated_at

Subscription rows are hard-deleted on unsubscribe. Notification rows keep a copied destination and payload so delivery history remains readable if a subscription is later removed.

Events

Supported events:

  • person.found: a missing person was marked as found.
  • person.child.found: a missing person under 18 was marked as found.
  • person.tip.created: new information was added for a person. This event is typed but is not wired to delivery yet.

Current runtime trigger:

  • person.found is triggered when PATCH /api/persons/:id changes an adult person from missing to found.
  • person.child.found is triggered when the same transition happens for a person under 18.

The old action route /api/persons/:id/found was removed. Found updates use the normal person member route with a flat body:

{
"status": "found",
"found_notes": "Localizada con su familia.",
"found_state": "distrito_capital",
"found_city": "libertador",
"finder_name": "Nombre",
"finder_phone": "+584121234567"
}

Channels

Supported channels:

  • email: active in V1 through Cloudflare Email Service.
  • telegram: reserved for the Telegram bot flow.

Email sender config is in wrangler.jsonc:

EMAIL
NOTIFICATION_EMAIL_FROM
NOTIFICATION_EMAIL_FROM_NAME

The sender is restricted to:

notificaciones@mail.venezuelatebusca.com

Topics

Subscriptions route by topic and target.

Supported topics:

  • global
  • person
  • city
  • region
  • source

Examples:

global null
person <person_id>
city libertador
region distrito_capital
source hospital_list

Current person.found matching uses:

  • global with target = null
  • person with target = <person.id>

Current person.child.found matching uses:

  • global with target = null
  • person with target = <person.id>
  • city with target = <found_city>
  • region with target = <found_state>

source is a supported value but is not matched yet.

Use snake_case handles for topic targets, matching the rest of the app:

libertador
la_guaira
distrito_capital
hospital_list

Supported found-region handles:

distrito_capital Distrito Capital
la_guaira La Guaira

Location Routing

Do not route critical notifications from free-text location fields. The existing last_seen_location value is free text and means “where this person was last reported,” not necessarily “where this person was found.”

Child-found routing uses normalized found-location facts:

found_city
found_state

triggerNotificationEvent adds location topics only for person.child.found and only when those fields are present:

global/null
person/<person_id>
city/<found_city>
region/<found_state>

For the current Defensoría recipients:

event channel destination topic target
person.child.found email dmetropolitana@defensoria.gob.ve region distrito_capital
person.child.found email ddpmorelos@gmail.com region distrito_capital
person.child.found email d-laguaira@defensoria.gob.ve region la_guaira
person.child.found email ddplaguaira@gmail.com.ve region la_guaira

For a Caracas-only subscription, use the normalized city handle from the found form:

event channel destination topic target
person.child.found email dmetropolitana@defensoria.gob.ve city libertador

Flow

  1. A person is marked found through PATCH /api/persons/:id.
  2. The route reads the previous person status.
  3. If the previous status was missing and the updated status is found, it calls triggerNotificationEvent with person.found for adults or person.child.found for minors.
  4. triggerNotificationEvent finds matching subscriptions.
  5. It creates one notifications row per matching destination.
  6. The route schedules sendEmail with ctx.waitUntil.
  7. sendEmail sends each pending email notification through env.EMAIL.send.
  8. The notification row becomes:
    • sent with the Cloudflare message id in message, or
    • failed with a short error message.

No queue, no workflow, and no retry loop are used in V1. Failed rows remain in D1 for manual inspection or a later retry script.

Email Copy

Emails are plain text only.

Every email includes:

  • event-specific subject and body.
  • no-reply instruction.
  • support address: soporte@venezuelatebusca.com.
  • concise citizen/voluntary/non-profit disclaimer.
  • concise privacy and verification disclaimer.

Example subject:

Persona localizada: Ana Merida

Example body:

Ana Mérida fue marcada como localizada en Venezuela te busca.

Datos del registro: cédula V-12345678, 12 años, reportada en Caracas.

Información recibida: Localizada con su familia.

Contacto: María Pérez, teléfono +584121234567.

Puedes revisar el registro aquí: https://venezuelatebusca.com/?person=<id>

No respondas a este correo. Para ayuda, escribe a soporte@venezuelatebusca.com.

Venezuela te busca es una iniciativa ciudadana, voluntaria y sin fines de lucro. No vendemos ni compartimos tu información con terceros; solo la usamos para ayudar a localizar personas. Los datos publicados son responsabilidad de quien los envía; verifícalos antes de difundirlos.

Managing Subscriptions

Use the internal actions when writing application code:

  • createSubscription
  • getSubscription
  • getSubscriptions
  • updateSubscription
  • deleteSubscription
  • triggerNotificationEvent
  • sendEmail

For one-off operational changes, use D1 directly.

Add a global found email subscription locally:

wrangler d1 execute venezuela-te-busca-database --local --command "
INSERT INTO subscriptions (id, event, channel, destination, topic, target, created_at, updated_at)
VALUES ('sub-example-found', 'person.found', 'email', 'person@example.com', 'global', NULL, datetime('now'), datetime('now'));
"

Add the same subscription in production:

wrangler d1 execute venezuela-te-busca-database --remote --command "
INSERT INTO subscriptions (id, event, channel, destination, topic, target, created_at, updated_at)
VALUES ('sub-example-found', 'person.found', 'email', 'person@example.com', 'global', NULL, datetime('now'), datetime('now'));
"

Remove a subscription:

wrangler d1 execute venezuela-te-busca-database --remote --command "
DELETE FROM subscriptions WHERE id = 'sub-example-found';
"

Testing

Run the focused test suite:

bun test tests/lib/notifications.test.ts

Run typecheck:

bun run typecheck

Local email does not deliver to a real inbox. Miniflare writes the generated email body to a local temp file and returns a message id. Real delivery requires the deployed Worker, Cloudflare Email Service, and a remote subscription row.