Skip to content
befday
Esc
navigateopen⌘Jpreview
On this page

Birthday Engine

The flagship native retention surface — merchants offer real birthday perks, the app shows a birthday calendar and delivers via push, redeeming through existing voucher/points rails. The one feature that owns befday's name.

Status: Accepted (direction); implementation deferred
Date: June 2026
Decision: Build a birthday engine as the flagship retention surface in apps/native. On a user’s birthday (and a lead-up window), merchants the user has a relationship with surface real birthday perks. The app shows a birthday calendar of “what’s waiting for me,” delivers via push, and redeems through existing voucher/points rails. It’s the highest-emotion, most on-brand, guaranteed once-a-year return, and uniquely defensible.

Relationship to the existing Twin Birthday feature (already built)

apps/native already ships a Twin Birthday surface (docs): a social birthday calendar that surfaces people who share your birthday day, backed by the twinBirthdays router + user.me. It is consumer-only and owns the “who shares my day” social moment — it has no merchant perks. This Birthday Engine is complementary, not a replacement: Twin Birthday owns the social layer; the Birthday Engine adds the merchant-perk / value layer on the same dateOfBirth. See Reconciliation with Twin Birthday below before implementing — the two should share one birthday surface, not compete.


TL;DR

The birthday is befday’s identity hook. Merchants define birthday perks (modeled as voucher campaigns); a daily job mints perk instances for eligible users in their birthday window; the app shows a “what’s waiting for you” calendar and delivers via push; perks redeem through existing voucher/points rails. Anti-abuse rests on a locked birth date + idempotency per (user, perk, year). Builds on the shipped Twin Birthday social surface — adds the perk layer, doesn’t fork it.


Context

befday Points Currency and Consumer Receipt Management give users money and memory reasons to return. The missing pillar is identity / emotion — personal and uniquely befday’s. The brand is literally befday, so the birthday should be the centerpiece. A birthday is:

  • High-emotion — a perk feels like a gift, not a discount.
  • Guaranteed cadence — one per year; a predictable, calendar-driven return.
  • Defensible — “the app that gets me free stuff on my birthday” is a one-line identity no competitor owns.
  • Two-sided value — merchants get a high-intent, emotionally primed visit (birthdays bring groups and spend); befday gets a retention spike + a reason for push permission.

The pieces already exist: verified-phone identity (Stamp Identification Flow), voucher campaigns/codes/redemptions, and the points wallet (befday Points Currency). The engine composes these rather than inventing primitives.


Decision

A birthday engine that (1) captures and verifies birth date, (2) lets merchants define birthday perks, (3) matches eligible users to perks during a birthday window, (4) surfaces a birthday calendar in native, (5) delivers via push, and (6) redeems through existing voucher/points rails.

1. Birth date capture

  • Collect birth_date (month + day required; year optional, for age-gating only) during onboarding or profile.
  • Reuse the existing field: the app already collects dateOfBirth at onboarding (it’s the anchor for the shipped Twin Birthday feature). The engine should derive month/day from that same source of truth rather than introducing a parallel birthday field — see Reconciliation.
  • Lock editing — once set, changing it requires support or is capped (e.g. once per 12 months). This is the single most important anti-abuse rule: an editable birthday = unlimited birthday perks. (Note: Twin Birthday today treats dateOfBirth as freely editable profile data — adding perks requires introducing this lock.)
  • Birth year is optional and never required to earn a perk (lowers friction, respects privacy); store separately if used for age-gated offers (e.g. alcohol).

2. Merchant birthday perks

A merchant opts in and defines a birthday perk — modeled as a specialized voucher campaign so it reuses all existing redemption + settlement plumbing:

Field Example
Perk type free item / % off / fixed off / bonus points
Eligibility any user / only past customers of this shop
Window on-the-day / birthday week / birthday month
Cap once per user per year; max redemptions
Funding merchant-funded (default), like any voucher

Eligibility default: offer to past customers of the shop (users with a store_customer link or prior order). This keeps perks high-intent and prevents the app becoming a freebie-farm. A merchant may opt into “any befday user” as an acquisition play.

3. Matching engine (the core)

A scheduled job (daily) finds users entering their birthday window and mints birthday perk instances (voucher codes) for eligible merchant campaigns:

Daily birthday job:
1. Find users whose birthday falls within any active perk's window
   (on-day / week / month), respecting age-gates.
2. For each (user, eligible merchant perk):
   - Idempotent per (user_id, perk_campaign_id, birthday_year)
   - Mint a personal voucher_code (or points grant) for that user
   - Create a birthday_perk_grant row
3. Enqueue notifications (see §5).

Idempotency is keyed on birthday_year — one grant per user per perk per year. This, combined with locked birth dates, is the anti-abuse backbone.

4. Native birthday calendar

The headline surface in apps/native:

  • “Your birthday is in N days” countdown when the window approaches.
  • “What’s waiting for you” — the list/grid of birthday perks the user has been granted this year, each with merchant, perk, and expiry.
  • On the day — a celebratory state (“Happy birthday! Here’s your gifts”) — high emotion, screenshot-worthy, shareable (feeds referrals from Platform Stamps & Referral).
  • Redeemed perks move to a “used” state; unused ones show expiry.

5. Delivery — push notifications

Push is what turns a granted perk into a visit. The birthday sequence:

Timing Message
~7 days before “Your birthday’s coming up 🎂 — N perks are waiting”
On the day (AM) “Happy birthday! Your gifts from [merchants] are ready”
Mid-window reminder “Don’t miss it — your birthday perk at [merchant] expires [date]”

The birthday is also the single best justification for requesting push permission at onboarding (“get notified when your birthday gifts are ready”) — far more compelling than generic “enable notifications.”

6. Redemption

  • Voucher-style perks redeem through the existing POS voucher flow (the data model’s voucher_redemptions) — no new POS work beyond surfacing “birthday” provenance.
  • Bonus-points perks grant into the befday Points Currency wallet via the existing idempotent earn path (event_type extended with birthday).

Why this over the other retention ideas

From the retention shortlist (nearby/map, spend insights, tiers, streaks), the birthday engine wins on defensibility × emotion × brand fit:

Idea Emotion Frequency Defensible (ownable) Build cost
Birthday engine Highest Once/yr (but guaranteed + push) Uniquely befday Medium (reuses vouchers)
Nearby + perks Low High No (everyone has maps) Medium
Spend insights Low Medium No Low
Tiers/status Medium Passive No Low

The others are good supporting features; the birthday engine defines the app. Build it as the spine, layer the rest around it.


Data Model Impact (sketch)

Reuses voucher + points infra. New: birth date on the user, perk campaign flag, and a grant ledger.

user / profile extension

Column Type Notes
birth_month integer 1–12, required for the engine
birth_day integer 1–31
birth_year integer nullable — age-gating only, never required for perks
birth_date_locked_at timestamp set on first save; gates future edits (anti-abuse)

Store month/day separately from year so matching is a cheap indexed comparison and year stays optional/private.

voucher_campaigns extension (birthday flag)

Rather than a new table, mark a campaign as a birthday perk:

Column Type Notes
is_birthday boolean default false — flags this campaign as a birthday perk
birthday_window enum on_day | week | month
birthday_eligibility enum past_customers (default) | any_user

birthday_perk_grants (grant ledger)

One row per (user, perk, year) — the idempotency + audit record.

Column Type Notes
id uuid PK
user_id text FK → user.id
campaign_id uuid FK → voucher_campaigns.id (the birthday perk)
birthday_year integer the year this grant is for
voucher_code_id uuid nullable FK → voucher_codes.id (for voucher-style perks)
points_granted integer nullable — for bonus-points perks
status enum granted | notified | redeemed | expired
granted_at timestamp

Idempotency: unique (user_id, campaign_id, birthday_year) — the core anti-abuse constraint, backed by locked birth dates.


API Impact (sketch)

Procedure Status Notes
consumer.profile.setBirthDate New Sets month/day (+optional year); enforces lock
consumer.birthday.upcoming New Countdown + granted perks for the birthday calendar
merchant.birthdayPerk.upsert New Create/edit a birthday perk (a flagged voucher_campaign)
(job) runBirthdayGrantJob New Daily matcher — mints grants, idempotent per (user, perk, year)
(job) sendBirthdayNotifications New Enqueues the push sequence
consumer.birthday.redeem Reuse Routes to existing voucher redeem or points grant

All grants are server-side, idempotent, scheduled — mirroring stamp/voucher issuance patterns already in the stack.


Anti-abuse

  • Locked birth date — the #1 control. Editable birthdays = infinite perks. Lock on first save; changes are rare/supported-only.
  • Idempotent per year(user_id, campaign_id, birthday_year) unique; reinstall/re-trigger grants nothing extra.
  • Verified identity — perks require the existing verified-phone account (Stamp Identification Flow); ties a birthday to a real person.
  • Eligibility default = past customers — keeps perks high-intent and avoids freebie-farming across the whole network.
  • Age-gating — perks needing age (alcohol) require birth_year and a verified-adult check.

Consequences

Type Consequence
Pro The most on-brand, defensible retention hook befday can build — owns “the birthday app” identity.
Pro Reuses voucher + points rails — medium build, not a new subsystem.
Pro Strongest possible justification for push-notification opt-in.
Pro Drives merchant value (high-intent, group, emotionally-primed visits) and feeds referrals (shareable birthday moment).
Con Once-a-year per user — must be paired with the everyday hooks (points, receipts, nearby) so the app isn’t dormant 364 days.
Con Requires reliable birth-date capture + a hard lock; mishandling enables abuse.
Con Needs a dependable scheduled job + notification infrastructure (timezones, delivery windows).
Con Merchant adoption needed — empty birthday calendars feel worse than no feature; seed with platform-funded perks early.

Resolved Decisions

Question Decision
Flagship hook? Yes — birthday engine is the identity/emotion pillar
Perk model Specialized voucher_campaign (is_birthday) — reuse redemption rails
Default eligibility Past customers of the shop; merchant may opt into any-user
Birth date Month/day required, year optional; locked after first save
Idempotency Per (user, perk, birthday_year)
Delivery Push sequence (pre-window, on-day, reminder); drives opt-in
Redemption Existing voucher flow or points grant

Reconciliation with Twin Birthday

The app already ships Twin Birthday — a social birthday calendar. The Birthday Engine must build on it, not duplicate or fork it.

What exists today (Twin Birthday)

Aspect Twin Birthday (built)
Job Social — “who shares my birthday day,” current user merged in
Data twinBirthdays.getByMonth + user.me; anchored on dateOfBirth
Scope Consumer-only — explicitly no merchant counterpart, no perks
Surface Month calendar grid, per-day people sheet, reach-out via threads
Birth date dateOfBirth, collected at onboarding, freely editable profile data
Calendar math lib/twin-birthday-calendar.ts (unit-tested — treat as contract)

What the Birthday Engine adds

Aspect Birthday Engine (this decision)
Job Value — merchant birthday perks matched to the user
Data voucher_campaigns (is_birthday), birthday_perk_grants, points
Scope Two-sided — merchants define perks; users receive/redeem them
Surface “What’s waiting for you” perks list + push delivery
Birth date Same dateOfBirth, but locked after first save (anti-abuse)

The reconciliation rules

  1. One birthday surface, two layers. Don’t ship a second birthday screen. The native birthday surface keeps Twin Birthday’s social layer (who shares my day) and adds a perks layer (“what’s waiting for me”) — likely as a section or tab on the same screen. Twin Birthday owns social; the engine owns value.
  2. Single source of truth for the date. Both read the same dateOfBirth. The engine derives birth_month/birth_day from it; do not introduce a parallel birthday field. The data-model “extension” in this doc should be re-framed as deriving from / locking the existing dateOfBirth, not adding a new one.
  3. Locking is a real change to existing behaviour. Twin Birthday treats dateOfBirth as editable. Adding perks requires introducing birth_date_locked_at and gating edits — this is a modification to the existing profile/onboarding flow, not greenfield. Flag it as a migration, and confirm it doesn’t break Twin Birthday’s expectations.
  4. Don’t break the tested contract. lib/twin-birthday-calendar.ts is unit-tested. Perk annotations should be layered on top of the existing calendar derivations without rewriting that pure logic — keep its tests green.

Net effect on this decision

  • The “birth date capture” and user/profile data-model sections are partially already built (dateOfBirth exists) — the genuinely new work is the lock, the merchant perk campaign flag, the grant ledger, the matching job, and push.
  • Build cost is lower than greenfield for capture, but adds the integration constraint of not regressing Twin Birthday.
  • Open question added: should the perks layer live as a tab on the Twin Birthday screen, or a sibling surface that links to it?

Open Questions

  • Window default: is the standard perk on-the-day, birthday week, or birthday month? (Longer = more redemption, more merchant cost.)
  • Empty-calendar cold start: how many platform-funded/seed perks does befday guarantee so early users always have something?
  • Year-over-year: do perks reset cleanly each year; any “birthday streak” mechanic for returning users?
  • Group/birthday-party angle: can a birthday user bring friends (party perk) — higher merchant value, more complex?
  • Timezone & delivery time: what local time do on-the-day notifications fire?
  • Birthday + points multiplier: does the birthday also boost points earning that day (ties the points currency to the brand moment)?
  • Verification of birth date: do we ever need to verify it (e.g. for age-gated perks), and how without adding friction?

Was this page helpful?