Impulse AI Docs
Intern dokumentasjon
Hopp til innhold

Varslingsplattform

Push-notifikasjoner i Impulse AI leveres gjennom en lagdelt plattform som skiller forretningslogikk (producers) fra transportlaget (delivery engine). Denne guiden dekker hele arkitekturen, fra AI-generering til APNS-levering.

Målgruppe: utviklere som skal legge til nye varslingstyper, feilsøke leveringsproblemer, eller forstå kategori-modellen.

Arkitekturoversikt

heading.anchorLabel
PRODUCERS PLATFORM LAYER
(eier HVA og NÅR) (eier HVORDAN)
┌──────────────┐
│ Reminder │ Cron hourly
│ (reminder) │ Template-basert
└──────┬───────┘
┌──────┴───────┐ ┌─────────────────────────────────┐
│ Notification │──►│ Delivery Engine │
│ Bus │ │ │
│ │ │ 1. Permission gate │
│ enqueue │ │ 2. Silence gate │
│ Notification│ │ 3. Category opt-in │
│ () │ │ 4. Rate-limit gate │
└──────┬───────┘ │ 5. Write notification_messages │
│ │ 6. Send APNS │
┌──────┴───────┐ │ 7. Record stats + events │
│ Lifecycle │ └─────────────────────────────────┘
│ (lifecycle) │
│ ASC webhook │
└──────────────┘

Nøkkelprinsipp: Producers kaller enqueueNotification() med ferdig body. De vet ingenting om APNS, rate-limits, eller DB-skriving. Delivery engine vet ingenting om AI, subscriptions, eller kampanjer.

Kategorier er utvidbarhetsaksen. Ny varslingstype = ny kategori i registeret + ny producer. Bus og engine er uberørt.

KategoriFormålBruker-toggleAIRate-limit
reminderStille nikk — template-basert re-engagement med 4-stegs eskaleringJaNei1/uke
lifecycleKontovarsler — abonnement, betalingNei (alltid på)Nei5/uke
campaignNyheter og tilbud — manuelt fra ConsoleJaNei1/uke

Global cap: Maks 4 varsler per uke per bruker, på tvers av alle kategorier (unntatt lifecycle som ikke teller mot cap).

Kategori-registeret

heading.anchorLabel
apps/api/src/notifications/categories.ts

Enkelt TypeScript-objekt med all konfigurasjon per kategori: navn (i18n), ikon, rate-limits, prioritet, AI-flagg. Brukes av bus, engine, rate-limiter, og iOS-innstillinger.

Notification Bus

heading.anchorLabel
apps/api/src/notifications/bus.ts

Eneste interface mellom producers og plattformen. Én funksjon:

enqueueNotification({
userId, category, body, title?,
scheduledFor?, deeplinkUrl?, metadata?,
priority?, sourceEntityId?, languageCode?
})

Bussen validerer kategori + body, beregner delay fra scheduledFor, og legger jobben på BullMQ-køen notification-deliver.

Delivery Engine

heading.anchorLabel
apps/api/src/notifications/delivery-engine.ts

BullMQ-worker med gate-kjede:

  1. Permissionios_permission_status = 'granted' + apns_token finnes
  2. Silencesilence_until ikke aktiv
  3. Category opt-in — kategori aktivert i brukerens innstillinger (hoppes over for lifecycle)
  4. Rate-limit — per-kategori + global ukentlig cap (Redis)
  5. Write — INSERT i notification_messages med kategori, tittel, deeplink, metadata
  6. Send — APNS push med categoryIdentifier (trigger iOS action buttons)
  7. Record — oppdater lastSentAt, logg event, inkrementer stats

APNS 410 (invalid token) → automatisk revokering av permission + token-nullstilling.

Reminder Producer

heading.anchorLabel
apps/api/src/notifications/producers/reminder-producer.ts

Template-basert “stille nikk” re-engagement. Kjører som hourly cron (24x/dag). Ingen AI-generering — rene maler.

4-stegs eskalering basert på dager siden siste aktivitet (impulse eller entry):

StegTerskelToneInnhold
13 dagerVarm3 roterende varianter (A/B/C) — anerkjenner aktivitet uten å avsløre innhold
27 dagerVarm”Impulse AI is here when you need it”
314 dagerRespektfull”Capture an impulse when you’re ready”
421 dagerSiste”We’re here. One tap away.”
28+ dagerStillhetIngen flere varsler

Uke 1-varianter (roterer basert på daysSinceActivity % 3):

  • A: “You captured something important. How did it go?”
  • B: “Have you checked in with yourself today?”
  • C: “Your impulse is waiting. Take a moment.”

For hver kjøring:

  1. Finn brukere der lokal tid er i leveringsvinduet (alle dager 08-21)
  2. Sjekk inaktivitet (ingen impulser/entries siste 3 dager)
  3. Bestem eskalerings-steg basert på dager siden siste aktivitet
  4. Velg template fra i18n-katalog via t() / tVar()
  5. enqueueNotification({ category: 'reminder', body, deeplinkUrl })

Dedup: Maks 1 reminder per ISO-uke per bruker.

Tidssone-støtte: Brukerens timezone (IANA, f.eks. Europe/Oslo) lagres i user_notification_preferences. iOS sender den ved hver app-foreground via APNS token sync.

Templates: reminder-templates.ts — engelsk (via i18n-katalog), varm kollegial tone. Ingen “we miss you”, ingen utropstegn, ingen em dash. Se Server-side i18n for språksystemet.

Lifecycle Producer

heading.anchorLabel
apps/api/src/notifications/producers/lifecycle-producer.ts

Event-drevet (ingen cron). Hooks inn i eksisterende App Store Server Notifications V2 handlers:

ASC EventPush-body
DID_FAIL_TO_RENEW”We couldn’t renew your subscription. Update your payment info.”
EXPIRED”Your subscription has expired. You can renew anytime.”
GRACE_PERIOD_EXPIRED”Your subscription is about to expire. Update payment to avoid interruption.”
Revalidation cron (3d før)“Your {tierName} expires {expiryDate}.”

Bruk: notifyLifecycleEvent(userId, 'subscription_expired') — én linje i eksisterende webhook-handler.

Rate Limiting

heading.anchorLabel
apps/api/src/notifications/category-rate-limiter.ts

To nivåer, begge Redis-baserte:

NivåRedis-keyCap
Per-kategorinotif:v2:cat:{userId}:{categoryId}:{weekKey}Fra CategoryConfig.maxPerWeek
Globalnotif:v2:global:{userId}:{weekKey}7/uke

lifecycle teller ikke mot global cap (countsTowardGlobalCap: false).

TTL: 8 dager. Graceful degradation: Redis utilgjengelig → fail open.

Action Buttons (iOS)

heading.anchorLabel
iosApp/Services/NotificationActionRegistry.swift

Statisk registry med action-definisjoner per kategori. Registreres ved app-launch, respons via generisk deeplink-handler.

KategoriActionDeeplink
reminder”Åpne impulsen”im://home?impulseId={id}
lifecycle”Forny abonnement”im://profile/subscription

Ny action = én entry i registry + én lokalisert streng. Null endring i delegate-kode.

Foreground-håndtering

heading.anchorLabel

AppDelegate implementerer UNUserNotificationCenterDelegate.willPresent:

  • lifecycle → vises som banner + lyd (viktig)
  • reminder → svelges (bruker er i appen)

Standard APNS alert — ingen Service Extension nødvendig:

{
"aps": {
"alert": {
"title": "Impulsspeil",
"body": "For 2 dager siden skrev du '...' — hva kjenner du nå?"
},
"category": "mirror"
},
"source": "notification",
"messageId": "uuid",
"deeplinkUrl": "im://home?source=notification"
}

category-feltet i APNS bestemmer hvilke action buttons som vises.

TabellFormål
user_notification_preferencesSamtykke, kategorier (JSONB), timezone, silence
notification_messagesMeldingsartifakt — body, tittel, deeplink, batch-ref
notification_eventsAppend-only observabilitetslog

Kategorier i DB

heading.anchorLabel

user_notification_preferences.categories er en JSONB-map:

{
"reminder": { "enabled": true, "lastSentAt": "2026-04-17T09:00:00Z" },
"lifecycle": { "enabled": true, "lastSentAt": null },
"campaign": { "enabled": false, "lastSentAt": null }
}

Dev-endpoint sikkerhet

heading.anchorLabel

Dev-endpoints (/dev/ping, /dev/test-consent, /dev/trigger-v2, /dev/reset) er beskyttet av én mekanisme:

DEV_API_TOKEN env var — når den ikke er satt, returnerer endpointene 404 Not Found. Ingen NODE_ENV-sjekk. Tokenet er sikkerhetsgrensen.

MiljøDEV_API_TOKENDev-endpoints
Lokal devSatt (Doppler dev)Tilgjengelig
StagingSatt (Doppler stg)Tilgjengelig
ProduksjonIkke satt404 — usynlige

Auth-bypass: X-Dev-Token header erstatter JWT-autentisering. Fungerer i alle miljøer der DEV_API_TOKEN er konfigurert. Implementert i apps/api/src/middleware/auth.ts.

Guard: apps/api/src/routes/notifications/dev-guard.tsrejectIfNotDev(), brukt av alle dev-endpoints.

Test og verifisering

heading.anchorLabel

Fire dev-endpoints (krever DEV_API_TOKEN i env):

POST /api/v1/notifications/dev/ping

heading.anchorLabel

Ren APNS transport-test. Sender test-push direkte til brukerens device, utenom bus/engine.

Verifiserer: APNS-credentials, token registrert, iOS permission, Apple aksepterer push.

POST /api/v1/notifications/dev/test-consent

heading.anchorLabel

Diagnostikk av brukerens samtykke-tilstand per kategori. Ingen sideeffekter.

Verifiserer: Permission-status, kategori-toggles, rate-limits, timezone, silence.

POST /api/v1/notifications/dev/trigger-v2

heading.anchorLabel

Trigger via bus → delivery engine (produksjonsvei).

{ "category": "mirror", "body": "Test-melding", "mode": "deliver" }

Moduser: deliver (send nå), generate (AI uten send), both.

Terminal window
# 1. Sjekk APNS-kobling
curl -X POST .../dev/ping -H "X-Dev-Token: $TOKEN"
# 2. Sjekk samtykke
curl -X POST .../dev/test-consent -H "X-Dev-Token: $TOKEN"
# 3. Test levering
curl -X POST .../dev/trigger-v2 -H "X-Dev-Token: $TOKEN" \
-d '{ "category": "lifecycle", "body": "Test kontovarsling" }'
# 4. Nullstill og gjenta
curl -X POST .../dev/reset -H "X-Dev-Token: $TOKEN"

iOS-innstillinger

heading.anchorLabel
Varslinger
├── [Stille uke-kort] (hvis aktiv)
├── Kategorier
│ ├── 🔔 Påminnelser [toggle]
│ ├── 📢 Nyheter [toggle]
│ └── 👤 Kontovarsler (alltid aktiv)
├── Siste varsler (meldingshistorikk)
apps/api/src/
├── i18n/ # Server-side i18n system
│ ├── index.ts # t(), tVar(), resolveLocale()
│ ├── locales.ts # 16 locale-koder (ServerLocale type)
│ └── messages/
│ ├── notifications.ts # Push-varsel strenger (EN-only katalog)
│ └── emails.ts # Transaksjonell e-post (fremtidig)
├── notifications/
│ ├── categories.ts # Kategori-register
│ ├── bus.ts # enqueueNotification()
│ ├── delivery-engine.ts # BullMQ worker (gates → APNS)
│ ├── category-rate-limiter.ts # Per-kategori + global cap
│ └── producers/
│ ├── reminder-producer.ts # Stille nikk (hourly, template-basert)
│ ├── reminder-templates.ts # 4-stegs eskalering via t() katalog
│ ├── lifecycle-producer.ts # ASC-events (deterministisk)
│ └── lifecycle-templates.ts # Lifecycle via tVar() katalog
apps/api/src/routes/notifications/
├── dev-ping.ts # APNS transport-test
├── dev-consent.ts # Samtykke-diagnostikk
└── dev-trigger-v2.ts # Bus → engine trigger
iosApp/Services/
├── NotificationService.swift # APNS-registrering + timezone
├── NotificationActionRegistry.swift # Action button-register
└── AppDelegate.swift # Foreground + action delegate

Notification Privacy Rule

heading.anchorLabel

Push-varsler er en offentlig overflate (låseskjerm, varslingssenteret, widgets). Aldri inkluder brukerinnhold i push-tekst:

  • Ingen impulse-titler eller beskrivelser
  • Ingen session-innhold eller refleksjoner
  • Ingen sjekk-inn-svar
  • Ingen AI-genererte analyse-sammendrag
  • Ingen personlige områdenavn

Varsler skal invitere uten å avsløre. Alle som ser brukerens låseskjerm skal ikke lære noe om deres indre prosess.

Legge til ny varslingstype

heading.anchorLabel
  1. Legg til strenger i i18n/messages/notifications.ts (EN-only, {placeholder} for variabler)
  2. Legg til kategori i categories.ts (id, navn, rate-limit, prioritet)
  3. Skriv producer — cron, event-driven, eller manuell trigger
  4. Kall enqueueNotification() med kategori + body via t() / tVar()
  5. Legg til action (valgfritt) i NotificationActionRegistry.swift

Delivery engine, bus, rate-limiter, og iOS settings UI tilpasser seg automatisk. Se Server-side i18n for fullstendig i18n-dokumentasjon.

Designdokument

heading.anchorLabel

Fullstendig arkitekturbeslutninger og v3-roadmap: docs/design/notification-platform-architecture.md