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.foundis triggered whenPATCH /api/persons/:idchanges an adult person frommissingtofound.person.child.foundis 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:
globalpersoncityregionsource
Examples:
global null
person <person_id>
city libertador
region distrito_capital
source hospital_list
Current person.found matching uses:
globalwithtarget = nullpersonwithtarget = <person.id>
Current person.child.found matching uses:
globalwithtarget = nullpersonwithtarget = <person.id>citywithtarget = <found_city>regionwithtarget = <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
- A person is marked found through
PATCH /api/persons/:id. - The route reads the previous person status.
- If the previous status was
missingand the updated status isfound, it callstriggerNotificationEventwithperson.foundfor adults orperson.child.foundfor minors. triggerNotificationEventfinds matching subscriptions.- It creates one
notificationsrow per matching destination. - The route schedules
sendEmailwithctx.waitUntil. sendEmailsends each pending email notification throughenv.EMAIL.send.- The notification row becomes:
sentwith the Cloudflare message id inmessage, orfailedwith 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:
createSubscriptiongetSubscriptiongetSubscriptionsupdateSubscriptiondeleteSubscriptiontriggerNotificationEventsendEmail
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.