GitHub Dashboard →

Architecture

System Overview

architecture
┌──────────────────┐     ┌────────────────────────────────────────┐     ┌──────────────────────┐
│   Admin Panel    │────▶│           visionpush-api               │────▶│  visionpush-storage  │
│  (Angular 18)    │     │  Axum 0.8 · OAuth 2.1 · OpenAPI 3.1   │     │  Filesystem  │  S3   │
└──────────────────┘     └──────────────┬─────────────────────────┘     └──────────────────────┘
                                        │ NATS JetStream publish
                              ┌─────────▼──────────┐
                              │    NATS JetStream   │
                              │   PUSH_CAMPAIGNS    │
                              │   PUSH_BATCHES.*    │
                              │   PUSH_FEEDBACK     │
                              └─────────┬──────────┘
                                        │ consume
                              ┌─────────▼──────────────────────────────┐
                              │          visionpush-worker              │
                              │  campaign_dispatcher · batch_ios        │
                              │  batch_android · batch_web             │
                              │  scheduled_send · feedback · webhook   │
                              │  health :9090/healthz                  │
                              └─────────┬──────────────────────────────┘
                                        │
                  ┌─────────────────────┼──────────────────────┐
                  ▼                     ▼                      ▼
          Apple APNS             Google FCM v1          Web Push (VAPID)
          (HTTP/2)               (HTTP/1.1)             (RFC 8030 + ECE)

Crates

visionpush-api

The public-facing REST API server. Built on Axum 0.8 with Tokio. Responsibilities:

  • REST API (/v1/*) — device registration, campaign creation, push dispatch, analytics queries, media upload, webhook management, in-app messages, notification channels
  • OAuth 2.1 / OIDC 1.0 — embedded auth server (PKCE, refresh tokens, password reset, team invites) via visionpush-auth
  • Campaign dispatch — validates and publishes campaign batches to NATS PUSH_CAMPAIGNS stream
  • Analytics queries — queries visionpush-analytics for funnel and timeline data
  • Prometheus metrics — exposes /metrics endpoint
  • Media storage — delegates to visionpush-storage

Key files:

directory tree
src/
  main.rs          — startup: DB, NATS, storage, auth, routes, graceful shutdown
  config.rs        — Config struct built from environment variables
  state.rs         — AppState shared across all handlers
  routes/          — one file per feature domain
  middleware/      — CORS, auth, rate limiting, request ID
  db/nats.rs       — JetStream stream bootstrap

visionpush-worker

A background process that consumes NATS streams and delivers push notifications. Runs entirely headless — no HTTP server except a /healthz health probe on :9090.

Processors (each runs as an independent Tokio task via tokio::join!):

Processor Stream Consumer
campaign_dispatcher PUSH_CAMPAIGNS campaign-dispatcher-consumer
batch_ios PUSH_BATCHES ios-batch-consumer
batch_android PUSH_BATCHES android-batch-consumer
batch_web PUSH_BATCHES web-batch-consumer
scheduled_send 30-second poll loop
feedback PUSH_FEEDBACK
webhook PUSH_FEEDBACK

Resilience: per-request timeouts (10 s APNs/FCM, 15 s Web Push), AIMD exponential backoff with full jitter on 429/throttle responses, semaphore-based backpressure, permit release before sleep so the rest of the batch keeps moving.

Analytics: writes PushEventRecord to visionpush-analytics writer; periodic flush every 5 seconds; nightly compaction at 03:00 UTC.

visionpush-core

Platform push clients with no external state. Each client is created once at startup and pooled.

Module Protocol Notes
apns HTTP/2 (TLS) p8 key auth, supports all interruption levels
fcm HTTP/1.1 FCM v1 API, service account JWT
vapid HTTPS RFC 8030 + ECE (AES-128-GCM content encryption)

Key types:

  • PushMessage — complete campaign message with localized content, options, targets
  • PushTarget — device token + platform + language + environment
  • PushResult / DeliveryStatusSent | Delivered | Failed | Bounced | Throttled | Expired
  • PlatformCredentials — APNs p8 key, FCM service account JSON, VAPID keypair

visionpush-analytics

Zero-dependency analytics engine using Apache Arrow, Parquet, and DataFusion. No external OLAP database required.

Storage layout:

directory tree
{data_dir}/
  events/
    date=2026-05-19/
      events-{timestamp}.parquet    ← per-flush file (ZSTD compressed)
  agg/
    daily/
      app=<id>/date=2026-05-19/stats.parquet   ← pre-aggregated daily stats

Write path: PushEventWriter buffers events in memory, flushes to Parquet when the threshold is reached (or on timer). Thread-safe via Arc<Mutex<Vec<PushEventRecord>>>.

Read path: PushEventReader uses a DataFusion SessionContext that scans both events/ (live) and agg/ (pre-aggregated) directories. Context is rebuilt when the generation counter advances (write-read coordination without locks).

Nightly compaction (03:00 UTC): merges per-flush Parquet files into daily aggregated files and updates pre-aggregations. Reduces query scan time as data ages.

visionpush-auth

A standalone OAuth 2.1 / OIDC 1.0 server embedded inside the API process.

Endpoints:

http
GET  /.well-known/openid-configuration   — OIDC discovery
GET  /oauth/authorize                    — authorization endpoint (PKCE)
POST /oauth/token                        — token exchange
GET  /oauth/userinfo                     — user info
POST /auth/password/change               — password change
POST /auth/reset/request                 — password reset request (sends email)
POST /auth/reset/confirm                 — password reset confirmation
POST /auth/invite/accept                 — team invite acceptance

Tokens: signed HS256 JWTs (JWT_SECRET). Passwords: Argon2id. Sessions: refresh token rotation with family tracking (detect theft). PKCE: S256 only.

visionpush-storage

Content-addressed blob storage with two backends behind a single BlobStorage trait.

Content addressing: every blob is identified by its BLAKE3 hash. Identical files are stored once (deduplication). Reference counting enables safe garbage collection.

Storage layout (both backends):

path pattern
{prefix}/ab/cd/{blake3-hash}.{ext}          ← content blob
{prefix}/ab/cd/{blake3-hash}.meta.json      ← metadata sidecar
Backend Use case Feature flag
FileSystemBlobStorage Local dev, single-server (always available)
S3BlobStorage Production (AWS S3, Cloudflare R2, MinIO, Backblaze B2) features = ["s3"]

S3 backend uses manual AWS SigV4 signing (sigv4.rs::Signer) built on sha2 + hmac — no AWS SDK, zero extra dependencies.

Accepted content types: image/jpeg, image/png, image/gif, image/webp. Max size: 10 MiB.

Data Flow

Campaign Dispatch

  1. POST /v1/push/send
    API validates the request, resolves audience (all / segment / external IDs), and publishes a CampaignMessage to NATS PUSH_CAMPAIGNS.
  2. campaign_dispatcher (worker)
    Reads PUSH_CAMPAIGNS, splits the audience into batches of BATCH_SIZE devices, and publishes PushBatch to PUSH_BATCHES.{ios|android|web}.
  3. batch_{ios|android|web} (worker)
    Reads PUSH_BATCHES.{platform}, deserializes the batch, calls the visionpush-core platform client per device. On success: publishes PushEvent to PUSH_FEEDBACK. On throttle: AIMD backoff, NAK message for redelivery.
  4. feedback processor (worker)
    Reads PUSH_FEEDBACK, writes PushEventRecord to visionpush-analytics, updates the device table (bounced tokens, last_seen).
  5. webhook processor (worker)
    Reads PUSH_FEEDBACK, calls registered webhook URLs for subscribed events.

Analytics Query

flow
GET /v1/analytics/campaigns/{id}/funnel

API → PushEventReader.campaign_funnel(org_id, campaign_id)
    → DataFusion SQL scan over events/ + agg/ directories
    → returns Vec<FunnelRow> {event_type, platform, count}

NATS Streams

Stream Subjects Purpose
PUSH_CAMPAIGNS push.campaign Campaign dispatch jobs
PUSH_BATCHES push.batch.ios, push.batch.android, push.batch.web Device batches per platform
PUSH_FEEDBACK push.feedback Delivery results

All streams use file storage with limits retention. JetStream guarantees at-least-once delivery; idempotency is ensured by the push_events table unique constraint on (campaign_id, device_id, event_type).

Database Tables

Table Description
organizationsTop-level tenants
team_membersUsers with RBAC roles (owner, admin, developer, viewer)
appsPush apps (one per mobile/web app)
platform_configsAPNs p8 keys, FCM service accounts, VAPID keypairs (AES-GCM encrypted)
api_keysScoped API keys (vp_live_…)
devicesPush tokens, platform, last_seen, frequency cap
segmentsAudience segments with filter rules
campaignsCampaigns with A/B variants, scheduling, status
push_eventsPer-event delivery funnel, mirrored to Parquet
notification_channelsAndroid notification channel config per app
in_app_messagesIn-app message templates
webhooksWebhook endpoints per app
webhook_deliveriesWebhook delivery log with retry state
usage_countersMonthly push volume per org
password_reset_tokensTime-limited tokens for password reset

Migrations are managed by sqlx-migrate, run automatically on API startup and via the pre-deploy Helm hook Job.

Multi-tenancy

Every database row carries org_id. The API middleware extracts the JWT subject or API key, resolves the org_id, and injects it into AppState for every request. Cross-tenant access is structurally impossible — all queries include WHERE org_id = $1.

RBAC roles in ascending privilege order:

Role Can do
viewer Read campaigns, analytics
developer + Manage apps, devices, API keys
admin + Manage team members, webhooks
owner Full access including billing and org deletion