Skip to content

H. Backend Architecture Assessment

← Back to README


reverb-backend Architecture

Strengths

Strength Evidence
Comprehensive service coverage 80+ services covering all business domains
Consistent service pattern AbstractService base class, AbstractPromiseService for async
Knex migrations for schema management Versioned migration files from 2018
Multi-process separation Web, meta, API, email-queue as separate processes
Working CI Travis CI runs tests against real PostgreSQL + Redis + RabbitMQ
Auth depth 8+ passport strategies including OAuth, social login, passwordless

Weaknesses

Weakness Impact
No TypeScript Type errors are runtime-only — bugs reach production
80+ flat service files No domain grouping — discoverability is poor
No OpenAPI specification API shape is undiscoverable without reading source code
No API versioning Breaking changes to existing routes affect all consumers simultaneously
seneca.js unmaintained Core messaging path has no security patch support
Default secrets in config Silent fallback to hardcoded values if env vars are absent

The Seneca / RabbitMQ Architecture

The Procfile runs four processes that communicate via RabbitMQ:

web:               HTTP API (Express)
service_meta:      Background metadata tasks
service_api:       Internal API service
service_email_queue: Email queue consumer

The senecaClient.js shows the messaging pattern:

module.exports = seneca()
  .use('seneca-amqp-transport')
  .client({
    type: 'amqp',
    pin: 'service:*,controller:*,action:*',
    url: config.MQ.URL
  });

Messages are dispatched by service pattern matching. This is a service bus architecture — conceptually sound, but the implementation (seneca) is a liability.

Seneca.js Risk Assessment

seneca@3.23.3 is effectively unmaintained. The npm page shows no updates in 3+ years. The plugin ecosystem is fragmented.

Options: 1. Keep and monitor: If seneca works on current Node and tests pass, defer replacement. Low short-term risk. 2. Replace with direct AMQP (amqplib): Removes the seneca layer, retains RabbitMQ. Medium effort. Clean solution. 3. Replace with BullMQ (Redis): Eliminates RabbitMQ dependency. Aligns with soreto-zoe's architecture. Most modern, but requires evaluating whether async messaging is actually needed between the 4 processes. 4. Collapse internal processes: If service_meta and service_api can be folded back into the main app.js process (by removing async dispatch), this eliminates the messaging layer entirely. Requires understanding what these processes actually do.

Discovery question: Is RabbitMQ actually in use in production? What would happen if service_meta and service_email_queue went down? (See O — Discovery Questions)


Service Layer Architecture

The services/ directory is a flat structure of 80+ JavaScript files. There are no subdirectories, no barrel files, and no internal domain grouping.

Current structure:

services/
├── AbstractService.js
├── AbstractPromiseService.js
├── auth.js
├── campaign.js
├── campaignVersion.js
├── reward.js
├── order.js
├── ... (75+ more)

Problem: Every route file requires specific service files by name. Adding a new domain concept requires knowing the naming convention. There is no module index or barrel export.

Recommended target structure (for reference — do not refactor now):

services/
├── auth/
│   ├── authService.js
│   ├── authTokenService.js
│   └── passwordlessService.js
├── campaign/
│   ├── campaignService.js
│   └── campaignVersionService.js
├── reward/
│   ├── rewardService.js
│   └── rewardPoolService.js
└── ...

This restructure should happen only during the TypeScript migration, not as a standalone refactor, to avoid churn.


soreto-zoe Architecture

Domain Purpose

soreto-zoe is the integration and job orchestration engine. Its domain responsibilities are:

  1. Job scheduling — Bull queues backed by Redis, cron-triggered
  2. Affiliate network integrations — 12+ networks (Awin, Rakuten, CJ, Partnerize, etc.)
  3. Post-reward processing — Harvest orders, validate, send reward emails
  4. Notification sending — Shared URL notifications
  5. Database maintenance — Partition management, password table cleanup
  6. Email delivery — Email queue processing via Mandrill

Processor Plugin Architecture

The processors/ directory uses a discoverable plugin pattern:

processors/
├── {affiliate-network}/
│   ├── mapper/         ← Transform affiliate data to soreto format
│   └── *.js            ← Main processor entry point
└── soreto/
    ├── postReward/     ← Core reward processing
    ├── email/          ← Email delivery
    └── ...

Each processor is loaded dynamically by Bull:

q.process("job_worker", 0, `${__dirname}/processors/${worker.jobWorkerProcessorPath}`);

This is a well-designed extensible pattern. Adding a new affiliate network means adding a new directory with a mapper and processor file. The architecture is correct; the implementation quality (untyped JS, no tests) needs improvement.

The Reverb API Client Problem

Processors import require('reverb') — the local module that calls reverb-backend's HTTP API. This creates a runtime dependency:

// processors/soreto/postReward/harvest.js
const _reverb = require('reverb');
let campaignVersions = await _reverb.postReward.getCampaignVersionsEnabledV2(190);

Problems: - No types — getCampaignVersionsEnabledV2() return type is any - No contract — if reverb-backend changes the response shape, soreto-zoe breaks silently at runtime - No versioning — there is no way to pin soreto-zoe to a specific version of the reverb API

Solution: Replace local_modules/reverb with @soreto/api-client — a typed, versioned package. See I — Shared Platform.


API Consistency

reverb-backend exposes four route families:

Route Family Purpose Authentication
/api/* REST API (consumed by melissa, zoe, reverb-react) Passport (bearer/cookie)
/app/* App-level routes Unknown
/partner/* Partner/merchant routes Basic auth
/web_site/* Public website routes None

Missing: No versioning (/api/v1/, /api/v2/). When reverb-backend makes a breaking change to an existing endpoint, all consumers break simultaneously. The NEXT_PUBLIC_APP_API_URL in melissa already appends /api/v1 — meaning the backend should adopt this prefix officially.


Data Access Patterns

Both reverb-backend and soreto-zoe use Knex as the query builder, both connecting to the same PostgreSQL instance with different schemas:

reverb-backend soreto-zoe
Schema reverb zoe
Knex version 0.95.7 0.16.3
Connection pool Default Configured via appSettings.ts
SSL Conditional on NODE_ENV Conditional on NODE_ENV

Question: Does the zoe schema reference tables in the reverb schema directly via PostgreSQL cross-schema queries? Or does all cross-schema communication happen via the HTTP API? This affects the upgrade sequencing for the database layer. (See O — Discovery Questions)


Scalability Assessment

Concern Current State Risk
reverb-backend multi-process 4 Heroku dynos Can scale horizontally but Heroku dynos are expensive
Redis sessions connect-redis — single Redis Session affinity risk if multiple web dynos
Elasticsearch Used for analytics queries Single cluster — no HA visible
Bull queues (zoe) Single Redis instance Bull 3 has known memory leaks under high load
RabbitMQ Single AMQP connection No confirmation of HA/cluster setup