Notifications and marketing endpoints
Operate the notification inbox, preferences, waitlist, contact form, and Slack alerts.
Notifications and marketing endpoints
The repository contains two distinct communication paths. Authenticated product notifications are stored per user and respect preferences. Public marketing submissions are rate-limited boundary endpoints intended to be called by a buyer-owned marketing site.
Product notifications
The supported notification types are:
| Type | Default email | Default in-app | Included producer behavior |
|---|---|---|---|
billing_update | enabled | enabled | billing owns its lifecycle email and emits in-app state |
security_alert | enabled | enabled | unrecognized-device sign-in and password reset |
system_announcement | enabled | enabled | no included producer; a product emits these deliberately |
Producers live with the domain that owns the event. Billing lifecycle notifications are emitted from
apps/server/src/domains/billing/service.ts. Security alerts are emitted from the Better Auth hooks
in apps/server/src/lib/auth.ts: a sign-in whose user agent has never been seen for that user, and
a completed password reset. The first session a user ever creates never alerts, so signup is silent.
Device recognition is user-agent based, not a device-identity system; treat it as a heuristic.
There is no email-change alert because the account contract has no email-change flow;
UpdateProfileRequestSchema covers name, profile image, and phone number only. Adding one means
adding the flow first.
There is no push-delivery implementation. Do not expose a push preference or claim push support without adding a provider, permission UX, subscription ownership, revocation, retry, privacy, and browser proof.
All notification routes require sessionAuth and scope reads and writes to the current users.id:
| Method | Route | Behavior |
|---|---|---|
GET | /notifications | paginated list with type/unread filters |
GET | /notifications/unread-count | current user's unread count |
PATCH | /notifications | mark up to 100 IDs or all rows as read |
GET | /notifications/preferences | effective email and in-app preferences |
PUT | /notifications/preferences | update deduplicated typed preferences |
GET | /notifications/:notification_id | owner-scoped detail |
DELETE | /notifications/:notification_id | owner-scoped deletion |
The browser polls unread count, invalidates only notification query keys after mutations, and sends the HttpOnly Better Auth cookie through the shared Hono client. The service must never accept a user ID from notification request input.
Notification dispatch is best effort after the owning transaction commits. It can be lost when the process exits and is not a durable queue or outbox. If delivery is required for a product invariant, follow Background work and design persisted delivery state.
Waitlist endpoint
POST /marketing/waitlist validates JoinWaitlistRequestSchema, stores one row per normalized
email, and returns the same non-disclosing success message for an existing address. The optional
fields are referral_source, source_page, and campaign.
The database row is durable. A newly inserted lead also triggers a best-effort Slack alert. Slack failure does not roll back the lead and duplicate submissions do not send another alert.
Contact endpoint
POST /marketing/contact validates ContactFormRequestSchema and sends a best-effort Slack alert.
The repository does not persist contact messages and there is no email, retry, inbox, or support
SLA. Do not connect a production form until Slack is configured and this loss policy matches the
product. Add durable storage or a provider-owned ticket ID when a submission must be recoverable.
Both public endpoints share an atomic Postgres limit of five requests per minute for the resolved client IP. The budget is shared across API replicas and depends on the selected trusted proxy profile producing the correct client IP.
Slack behavior
SLACK_BOT_TOKEN is optional. When absent, signup, waitlist, and contact alerts are disabled and
the server continues. When present, the bot posts sanitized text to #alerts-users. The channel is
a current application constant, not a configurable routing matrix.
Never treat Slack as the source of truth for a durable lead, a contact-support entitlement, or an incident system. Never include secrets, auth links, cookies, payment data, or unrestricted customer payloads in alert messages.
Verification
bun test --cwd apps/server src/domains/marketing
bun test --cwd apps/server src/domains/notifications
RUN_DB_INTEGRATION_TESTS=1 \
DATABASE_URL=postgresql://app:app@localhost:5432/app?sslmode=disable \
bun test --cwd apps/server
pnpm test:e2eBefore launch, prove duplicate waitlist behavior, invalid and oversized input, limit exhaustion, Slack disabled/failure behavior, notification preferences, owner isolation, mark-read limits, and the exact retention policy for leads, contact content, and notifications.