Overview
Payghaam splits into an HTTP API, background workers, PostgreSQL (source of truth), and Redis (queues + cache). The dashboard is a Next.js app for operators; the Flutter SDK and REST API are integration surfaces for your product.
SDK / server / webhook ─▶ API edge (auth + identity verification)
│ buffered — request returns immediately ("accepted")
▼
Redis / BullMQ ─┬─▶ event-ingestion-{0..N} (FIFO per user)
│ └─ Ingestion worker: upsert subscriber, persist event
│ └─▶ journey-user-{0..N} (FIFO per user)
│ └─ JourneyEngine: conditions, actions, timers
└─▶ delivery (high concurrency)
└─ ChannelStrategyFactory → APNs / FCM / SMS / email
└─ classify failures → ProjectIssue (notifications)Ingestion buffer
High-frequency writes from the SDK — identify, subscription registration, tag updates, events, and delivery receipts — never touch Postgres on the request. The edge validates the call (including identity verification), pushes it onto a buffer, and returns accepted. A worker then resolves the subscriber, persists the row, and routes journey events downstream. A Postgres hiccup degrades to queued, not a 5xx.
The buffer is partitioned by external_id (FIFO, one consumer per partition), so a single user's mutations are applied in order, and every job carries an idempotency key with a durable completion marker — so a client retry is exactly-once. Low-frequency calls that need a definitive answer (consent opt-out, API journey triggers) stay synchronous.
Per-user ordering
Both the ingestion buffer and the journey engine hash a user into one of N FIFO partitions, so step order is preserved end to end even under load. Delivery workers run at higher concurrency because network I/O is the bottleneck.
Journey graph
- Nodes are actions (send push, wait, branch).
- Edges carry conditions (tag filters, event properties) — conditions live on edges, not nodes.
- Wait steps schedule delayed jobs; stale timers are ignored if the user already moved on.
- Idempotency keys prevent duplicate sends on worker retries.
Delivery gateway
Each send creates a MessageDelivery row. The worker picks a channel strategy (FCM, APNs, Twilio, SMTP, webhook), applies consent and frequency caps, and sends through a per-channel circuit breaker. Failures are normalized across providers into a category (config / token / rate / provider-down / payload) and stored with the row.
Channel-wide problems and provider outages roll up into deduplicated issues that surface in the dashboard notification manager, and auto-resolve when sends recover. See Delivery health.
Secrets at rest
FCM service accounts, APNs .p8 keys, and SMTP/Twilio credentials are encrypted with AES-256-GCM before storage. The dashboard never returns raw secrets — only a configured indicator.
