Impulse AI Docs
Intern dokumentasjon
Hopp til innhold

HTTP/Worker Service Split

API-et kjorer som to separate prosesser pa Railway. Begge bruker samme kodebase, men SERVICE_MODE-variabelen bestemmer hva hver prosess gjor.

Klienter (iOS, Android, Web)
┌─────────────────────┐ ┌─────────────────────────┐
│ api │ │ api-worker │
│ SERVICE_MODE=web │ │ SERVICE_MODE=worker │
│ │ │ │
│ HTTP server │ │ BullMQ workers │
│ Fastify routes │ Redis │ 6 cron jobs │
│ SSE connections │◄──────►│ AI-kall (Anthropic) │
│ Queue producers │ │ Push-notifikasjoner │
│ QueueEvents │ │ Minimal health endpoint │
│ Admin dashboard │ │ │
└─────────────────────┘ └─────────────────────────┘
│ │
└──────────┬───────────────────┘
┌──────┴──────┐
│ Redis │
│ Supabase │
└─────────────┘

Hvorfor splitte

heading.anchorLabel
Problem med single-processLosning med split
AI-kall (10-30s) blokkerer event loopWorker prosesserer AI separat, HTTP forblir responsiv
Kan ikke skalere HTTP uten a duplisere workersSkaler web-replicas uavhengig av worker
Crons kjorer pa alle replicasCrons kjorer kun pa worker
En treg AI-jobb pavirker SSE-heartbeatsIsolerte prosesser, isolerte problemer

Deklarativ service-konfigurasjon

heading.anchorLabel

Konfigurasjon av hva hver mode kjorer styres av en enkelt fil: apps/api/src/lib/service-config.ts.

const SERVICE_CONFIGS = {
all: { http: true, workers: true, crons: true, adminRoutes: true },
web: { http: true, workers: false, crons: false, adminRoutes: true },
worker: { http: false, workers: true, crons: true, adminRoutes: false },
}

Forbrukere sjekker serviceConfig.workers, serviceConfig.crons osv. — aldri SERVICE_MODE-strengen direkte. Dette gir:

  • Ett sted a endre — ny mode = ny linje i konfig-objektet
  • Positiv logikkif (serviceConfig.crons) istedenfor if (SERVICE_MODE !== 'web')
  • Kompilator-sikkerhet — TypeScript validerer at alle flagg finnes

SSE cross-process bridge

heading.anchorLabel

SSE-endepunkter (enrichment-status, guidance-status, summary-status) kjorer pa web-prosessen, mens BullMQ workers prosesserer jobber pa worker-prosessen. For at SSE skal motta oppdateringer pa tvers brukes BullMQ sin innebygde mekanisme:

1. Klient → POST /impulses (web) → push() → Redis Queue
2. Klient → GET /impulses/:id/enrichment/status (web) → subscribe() → EventEmitter
3. Worker plukker opp jobb → processJob()
a. updateJob('processing') → lokal EventEmitter (kun worker)
b. job.updateProgress({status:'processing'}) → Redis Stream
4. Web: QueueEvents lytter → 'progress' event → updateJob() → EventEmitter → SSE
5. Worker fulllforer → BullMQ markerer completed i Redis
6. Web: QueueEvents → 'completed' event → updateJob() → EventEmitter → SSE

Event-typer og leveringsmekanisme

heading.anchorLabel
EventMekanismeKilde
queuedIn-process (push() pa web)Web
processingjob.updateProgress() → QueueEvents progressWorker → Redis → Web
retryingjob.updateProgress() → QueueEvents progressWorker → Redis → Web
completedQueueEvents completed (eksisterte allerede)Worker → Redis → Web
failedQueueEvents failed (eksisterte allerede)Worker → Redis → Web

SSE-endepunktene er uendret — de bruker subscribe() som for. Bridgen er transparent.

Alle 6 cron-jobber kjorer kun nar serviceConfig.crons === true (modes: all, worker).

CronIntervallHva
Gift expiry6 timerUtloper uinnloste gavekoder
Subscription revalidation6 timerRe-syncer stale subscriptions mot Apple
Stale impulse reaper5 minRydder stuck impulses
Account deletion reaper30 minHard-delete av slettede kontoer
Analytics cache reaper6 timerFjerner utlopte dashboard-cacher
Reminder producer1 timeSender re-engagement push-notifikasjoner

Railway-oppsett

heading.anchorLabel

Begge services bruker Railway sin native environment-modell med branch-mapping:

ServiceStaging (branch: main)Production (branch: production)
apiSERVICE_MODE=webSERVICE_MODE=web
api-workerSERVICE_MODE=workerSERVICE_MODE=worker

Begge services i begge environments: healthcheck pa /health/8bde..., Railway-tildelt port.

Env vars og Doppler

heading.anchorLabel

Doppler styrer alle app-secrets med en config per environment (stg, prd). Shared variables synces automatisk til begge services.

Infrastruktur-vars settes direkte i Railway per service (ikke i Doppler):

Varapiapi-worker
SERVICE_MODEwebworker
QUEUE_CONCURRENCY_MULTIPLIER2 eller 3
RAILPACK_START_CMDvia railway.tomlnode apps/api/dist/server.js
RAILPACK_BUILD_CMDvia railway.tomlpnpm --filter @im/shared build && pnpm --filter api build

Env vars per service-mode

heading.anchorLabel

Ikke alle Doppler-vars er relevante for begge services. Overflodige vars ignoreres, men her er oversikten:

KategoriVarsBrukes av
InfrastrukturSUPABASE_URL/KEY, REDIS_URL, SENTRY_DSNBegge
AI-workloadsANTHROPIC_API_KEY, VOYAGE_API_KEY, AI_MODEL_*Kun worker
Push-notifikasjonerAPNS_* (5 vars)Kun worker
HTTP-sikkerhetAPI_CLIENT_KEY, DEV_API_TOKEN, HEALTH_SECRETKun api
CORSWEB_URL, LANDING_URL, CONSOLE_URLKun api
Admin dashboardCONSOLE_ADMIN_TOKEN, CONSOLE_SENTRY_*Kun api
SubscriptionsASC_*, APPLE_APP_SHARED_SECRETBegge
Push til main → api (staging) + api-worker (staging) deployer automatisk
Push til production → api (prod) + api-worker (prod) deployer automatisk

Ingen manuell deploy nodvendig. CI-workflowen (deploy-production.yml) kjorer DB-migrasjoner; Railway deployer services basert pa branch.

Lokal utvikling

heading.anchorLabel

Lokalt brukes SERVICE_MODE=all (default) — alt kjorer i en prosess som for. Ingen endring i utvikleropplevelsen.

For a teste splitten lokalt:

Terminal window
# Terminal 1 — HTTP
SERVICE_MODE=web pnpm --filter api dev
# Terminal 2 — Worker
SERVICE_MODE=worker pnpm --filter api dev

SSE-events mangler

heading.anchorLabel

Sjekk at begge services er koblet til samme Redis-instans. QueueEvents bruker Redis Streams for cross-process kommunikasjon.

Terminal window
# Sjekk Redis-tilkobling pa worker
railway service link api-worker && railway logs | grep "Redis"

Crons kjorer ikke

heading.anchorLabel

Verifiser at worker er oppe og SERVICE_MODE=worker:

Terminal window
railway service link api-worker && railway logs | grep "cron\|reaper\|producer"

Duplikate crons

heading.anchorLabel

Forhindres av distributed cron locks (se nedenfor). Hvis en cron likevel kjorer dobbelt, sjekk Redis-tilkobling — uten Redis faller lock-mekanismen tilbake til unconditional execution.

Distributed Cron Locks

heading.anchorLabel

Alle 6 cron-jobber er wrappet med withCronLock() (apps/api/src/lib/cron-lock.ts). Dette sikrer at kun en worker-replica kjorer en gitt cron om gangen.

Hvordan det fungerer

heading.anchorLabel
Worker A: withCronLock('gift-expiry', 300, fn)
→ SET cron:lock:gift-expiry <hostname> NX EX 300
→ OK (acquired) → kjor fn → DEL lock
Worker B: withCronLock('gift-expiry', 300, fn) (samtidig)
→ SET cron:lock:gift-expiry <hostname> NX EX 300
→ null (lock exists) → skip
  • NX — sett kun hvis nokkel ikke eksisterer (atomisk)
  • EX — auto-expire etter TTL sekunder (crash safety)
  • DEL etter execution — frigjor locken umiddelbart sa neste intervall kan kjore

TTL er satt lengre enn forventet kjoretid, men kortere enn intervallet:

CronIntervallLock TTLBegrunnelse
Gift expiry6 timer300s (5 min)Enkle DB-updates
Subscription revalidation6 timer600s (10 min)Apple API-kall per subscription
Stale impulse reaper5 min240s (4 min)DB-scan + updates
Account deletion reaper30 min1500s (25 min)Hard-deletes, kan ta tid
Analytics cache reaper6 timer300s (5 min)Enkel DELETE query
Reminder producer1 time3000s (50 min)Scanner alle brukere + APNS

Feilhaandtering

heading.anchorLabel
  • Redis utilgjengelig: Kjorer cron uten lock (bedre duplikat enn ingen kjoring)
  • Worker crash: Lock auto-expires etter TTL
  • Normal completion: Lock frigis umiddelbart med DEL

Konvensjon for nye crons

heading.anchorLabel

Alle nye cron-jobber SKAL bruke withCronLock:

import { withCronLock } from '../../lib/cron-lock.js'
const run = () => withCronLock('my-new-cron', ttlSeconds, async () => {
await myFunction()
}).catch(err => logger.error({ err }, 'Cron failed'))

Arkitekturen stotter:

  • Flere web-replicas — Railway scale, alle stateless, deler Redis
  • Flere worker-replicas — BullMQ distribuerer jobber automatisk, cron locks forhindrer duplikater
  • Nye modes — legg til en linje i SERVICE_CONFIGS (f.eks. cron-only for dedikert cron-service)
  • Concurrency-justeringQUEUE_CONCURRENCY_MULTIPLIER env var, ingen deploy