H. Backend Architecture Assessment
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:
- Job scheduling — Bull queues backed by Redis, cron-triggered
- Affiliate network integrations — 12+ networks (Awin, Rakuten, CJ, Partnerize, etc.)
- Post-reward processing — Harvest orders, validate, send reward emails
- Notification sending — Shared URL notifications
- Database maintenance — Partition management, password table cleanup
- 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 |