Skip to main content

Notifications & Digests

Fyndow keeps people informed two ways. Notifications are immediate, per-event messages — "your booking was confirmed", "an invoice was paid" — delivered across in-app, push, email, and messaging channels according to the event's importance and the user's preferences. Digests are the opposite: a periodic, personalized roll-up of everything that happened, so low-urgency activity (new forum posts, upcoming bookings) reaches the user without filling their inbox one message at a time.

This chapter documents both. For where these messages fire within the larger journeys, see End-to-End Journeys; for the status changes that emit them, see State Machines.


How a notification is sent

There is a single send entry point. Given a target user, an event type, a title, an optional body, and an optional deeplink, it does the following:

  1. Always creates an in-app notification and publishes it in real time to the user's personal real-time channel. In-app delivery is unconditional — it never depends on preferences.
  2. Resolves the channel policy for this user and event type (see Event channel policy).
  3. Looks up the user's contact handles — email, phone, WhatsApp number, Telegram chat id.
  4. Dispatches to each enabled external channel concurrently and best-effort, logging every attempt to a delivery log.

Channels

ChannelTransportDefault behaviour
in_appStored record + real-time pushAlways on
pushRegistered device tokens (APNs/FCM)On for most events
emailEmail dispatcherOn for money & commitment events
smsSMS dispatcherOpt-in only (off by default)
whatsappWhatsApp dispatcherOpt-in only (off by default)
telegramTelegram dispatcherOpt-in only (off by default)

Each external channel is wired through a common dispatcher contract, and the service selects the right dispatcher by channel name. The push channel is handled separately because it fans out to a user's registered device tokens rather than a single recipient address; a user with no registered tokens simply receives nothing on push.

Message content

Channel message bodies are produced from the event type plus optional template parameters, which together produce the per-channel text. Deeplinks are absolute URLs built from a configurable application base address (defaulting to https://fyndow.com) joined with the notification's link path, so a push or email lands the user on the right booking, invoice, or order.

Deduplication and resilience

  • Dedup window. Before sending on any external channel, the service checks the delivery log for a successful send of the same event type to the same user on the same channel within the last 60 seconds, and skips if found. This prevents duplicate emails/pushes when an action fires the same event twice in quick succession.
  • Best-effort. All external dispatches run independently and are settled individually — a failing channel is logged but never throws, so notification failure can never break the action that triggered it (booking confirmation, checkout, etc.). Callers guard the send the same way for the same reason.
  • Delivery logging. Every external attempt writes a row recording the channel, event type, recipient, success flag, any external id, and any error — the source of truth for both dedup and diagnostics.

Event channel policy

Whether an event reaches a user by email or push (in-app is always on) is decided per event type by a default policy, which the user can override. The policy sorts events into two tiers.

Money & commitments — email + push + in-app. High-stakes events the user should not miss: booking confirmation, new booking request, booking cancelled, order received, order paid, invoice sent, invoice paid, payment received, quote received, quote accepted, dispute opened, dispute response, dispute resolved, and the three verification outcomes (approved, rejected, and changes requested), and the outcomes that follow paying to put a Happening in front of the wider public — the payment landing, the post being approved, and the post being declined.

Everything else — push + in-app only (no email). Lower-urgency activity that should not crowd the inbox: new message, order shipped, order delivered, review received, new post, post reply, mention, the community join request/approved/ denied set, booking reminder, booking started, booking completed, re-engagement, payment reminder, review request, credential expiring, a new Happening from a business you follow, a Happening starting soon, and a comment on one of your Happenings.

Any event type not explicitly listed defaults to push + in-app only. SMS, WhatsApp, and Telegram are never on by default — they are strictly opt-in, regardless of tier.

User overrides

A user can override the policy per event type. The preferences service stores explicit channel choices keyed by user and event type. When resolving channels for a send, a stored preference for that exact user-and-event-type pair wins; otherwise the default tier applies. The "view preferences" endpoint returns the full set of event types — there are 41, from booking confirmation through a catch-all general type — each annotated with its currently effective channels (stored value, or default), so the UI can render a complete, accurate toggle grid even before the user has saved anything.

In-app is conceptually always-on; sms/whatsapp/telegram are off unless the user opts in.


Happenings notifications

Happenings — Fyndow's time-boxed events, promotions, and ads — generate their own set of notifications. They ride the same send pipeline and channel policy described above; this section spells out when each one fires and who receives it. As everywhere else, these sends are best-effort: a failed push can never block a post from going live, getting reviewed, or expiring.

There are two audiences. The author of a Happening hears about what happens to their own post — its review outcome and its conversation. The followers of a posting business hear about new and imminent Happenings from businesses they have chosen to follow.

Messages to the author

  • Your post was approved. When a public Happening clears review and goes live, its author is told it is now live. This is a commitment-tier event, so it reaches email as well as push and in-app.
  • Your post was declined. If a public Happening is not approved, its author is told, with the reviewer's reason, and reminded that they can edit and resubmit it for another look without paying again. Also a commitment-tier event.
  • Your payment landed. When the payment for a public Happening succeeds and the post moves into the review queue, its author gets confirmation. Commitment-tier.
  • Someone commented. When a viewer comments on a Happening, its author is notified (the author is never pinged for their own comments). This is ambient activity, so it arrives by push and in-app, not email.

Emoji reactions are deliberately quiet: they update the running count on the card but do not generate a notification, so a popular post never floods its author's inbox.

Messages to followers

Only Happenings attached to a business fan out to followers — an event posted by a person as themselves has no business followers to reach, so it notifies no one on publish.

  • A new Happening went live. The moment a business's public Happening is approved and goes live, everyone following that business is notified that there's something new from a business they follow.
  • It's starting soon. Shortly before a live public event begins — within the hour — followers of the posting business get a reminder so they don't miss it.

Both follower messages are discovery nudges, so they arrive by push and in-app and stay out of email. And because they obey the same per-event-type preferences as everything else, a follower who would rather not hear about Happenings can switch them off without touching the rest of their notifications.

Community Happenings — the free, members-only kind — go live immediately with no review step, so they skip the approval and payment messages entirely; their conversation still notifies the author the same way.


Personalized digests

Digests answer a different need: "what should I know about, without being pinged for each item?" A digest is a generated snapshot, stored per user, that the user can read in-app and mark as read. Generation is run by an HTTP-triggered job (the platform uses external cron to hit job endpoints), batched across users.

What a digest contains

Generating a digest assembles a content object from several sources, scoped to that user and their business (if they own one):

SectionSource
Upcoming bookingsBookings where the user is provider or customer, pending or confirmed, dated within the next 7 days
Overdue invoicesInvoices on the user's business that are overdue
New forum postsPosts in the user's subscribed forums created in the last 7 days (excluding removed posts)
Business statsJobs completed and revenue (successful payments) in the last 7 days
Expiring credentialsVerified credentials on the business expiring within the next 30 days

The resulting record is written for a weekly period with a generation timestamp. Sections that don't apply — a customer with no business has no overdue invoices, business stats, or expiring credentials — simply come back empty.

Frequency and batching

  • Per-user frequency. Each user has a digest-frequency preference — daily, weekly, or on-demand (default daily). It is read and updated through the digests preferences endpoints.
  • Batch generation. The job iterates users (up to a 1,000-user batch) and generates each one independently; a failure for one user is swallowed so the batch continues. Generated content is currently compiled over a weekly window regardless of the stored frequency label.

:::note TODO The digest content window is fixed at one week (and the stored period is weekly), while the user-facing digest-frequency preference offers daily / weekly / on-demand. How the scheduling job maps each user's chosen frequency (and timezone-aware delivery time) onto generation runs is not part of the digest generation itself — it lives in the job/scheduling layer. Document the exact mapping there. :::

Reading a digest

Digests are retrieved through the digests API: list (paginated, newest first), fetch the latest, fetch one by id, and mark one read (which records the time it was read). Each digest belongs to exactly one user and is always scoped to that user on read, so one user can never fetch another's digest.


Notifications vs. digests at a glance

AspectNotificationsDigests
TimingImmediate, per eventPeriodic roll-up
GranularityOne message per eventMany items summarised together
ChannelsIn-app, push, email, SMS/WA/TelegramIn-app (read in the app)
Best forMoney & commitments, direct messagesAwareness: forum activity, upcoming work
ControlPer-event-type channel preferencesdaily / weekly / on-demand

Together they let urgent, money-and-commitment events reach a person reliably across channels, while ambient community and schedule activity arrives as a calm, batched summary — see how both surface across the end-to-end journeys.