Skip to content

03 — someli-api architecture

A guided tour of the code so you can place a new file in the right folder and trace a request from URL to database without re-deriving it from scratch.


Folder map

someli-api/
├── server.js                    ← Express boot, MySQL connect, mount routes
├── conf.js                      ← env-var passthrough (46 vars)
├── ecosystem.config.js          ← PM2 manifest (production)
├── package.json
├── methods.js                   ← `ensureToken` Bearer-JWT middleware
├── routes/
│   ├── routes.js                ← THE big file: 394 endpoints, class-based pattern
│   ├── auth.js                  ← 291 endpoints, Express Router, auth-gated
│   ├── social.js                ← 15 endpoints, OAuth handshakes
│   ├── paddle.js                ← 2 endpoints, Paddle billing
│   └── partnerAuth.js           ← 4 endpoints, partner API (own JWT)
├── actions/
│   └── actions.js               ← generic CRUD layer
├── middlewares/
│   ├── auth.js                  ← token decrypt + JWT verify; sets res.userId etc.
│   ├── passport.js              ← Passport strategies (Google, FB, LI, TT, TW)
│   └── ...
├── helper/                      ← shared utilities (see code-overlap with admin-api!)
│   ├── aiLogics.js              ← AI provider routing (Bedrock / Vertex / OpenAI)
│   ├── helper.js                ← grab-bag utilities
│   ├── tokenGenerator.js        ← JWT sign + AES encrypt
│   ├── revokeToken.js           ← in-memory revocation map
│   ├── ragProcess.js            ← Google Cloud RAG corpus management
│   ├── webScraping.js           ← URL → content extraction
│   ├── stockImage.js            ← Pexels / Pixabay search
│   ├── constants.js
│   └── ...
├── agents/
│   ├── conversationAgent.js     ← multi-step LLM conversation
│   ├── researchAgent.js         ← research-flavoured agent
│   ├── profileAgent.js          ← profile-extraction agent
│   └── inputParserAgent.js      ← parse free-text input
├── modules/
│   └── dbDriver/lib/mysql.js    ← callback MySQL pool wrapper (App.db)
├── dashboard/                   ← in-process copy of someli-dashboard-be
│   ├── conf.js                  ← 5 sub-config vars
│   ├── routes/index.js          ← 22 analytics endpoints
│   └── ...
├── job_*.js                     ← ~108 standalone PM2 workers (publish, refresh, RAG-sync, …)
├── conf/
│   └── credentials.json         ← Google service-account JSON (verify .gitignore)
├── Dockerfile
├── nginx.conf                   ← reverse-proxy in front of node
├── Jenkinsfile                  ← legacy CI/deploy
└── .github/workflows/
    └── dev-api-deploy.yml       ← newer GHA workflow (only for `someli-api`)

Entry point — server.js

The boot sequence is in this exact order (read top to bottom):

  1. Load env: const conf = require('./conf');conf.js does require('dotenv').config() then re-exports process.env.*.
  2. Create Express app.
  3. Wire middleware (order matters):
  4. express-session (Passport-OAuth uses this)
  5. express-fileupload
  6. express.json({ limit: '150mb', parameterLimit: 50000 }) — applied to all routes except the four webhook paths
  7. Static: /favicon.ico, /uploads
  8. Healthchecks: /health, /db-health
  9. passport.initialize() + passport.session()
  10. cors()
  11. Connect to MySQL: modules/dbDriver/lib/mysql.js → db.connect(conf, callback).
  12. Initialise Socket.IO on the HTTP server.
  13. Build the shared App object: { db, server: app, socket: io }.
  14. Re-export appData = App as module.exports.appData — so other files (notably dashboard/) can re-enter the running app at runtime.
  15. Mount route modules in this order:
  16. /auth/*routes/auth.js
  17. /social/*routes/social.js
  18. /paddle/*routes/paddle.js
  19. /partnerAuth/*routes/partnerAuth.js
  20. /auth again → dashboard/routes/index.js (sharing the same prefix is intentional)
  21. / (root) → routes/routes.js (the apiRoutes.prototype.init() pattern)
  22. Start HTTP server on conf.port.

Consequence: changing the order of webhook bodyparser exemption breaks Paddle / Stripe signature verification silently. Don't reorder middleware unless you understand which routes need raw body.


Two route patterns

Pattern A — class-based (routes/routes.js, routes/auth.js's init)

routes/routes.js exports { api: apiRoutes, con } where: - apiRoutes is a constructor receiving the App object - apiRoutes.prototype.init() registers all 394 endpoints directly on App.server (the Express app) - con is a shared sync-mysql connection used throughout

You add a new endpoint by:

// inside apiRoutes.prototype.init()
App.server.post('/myNewEndpoint', methods.ensureToken, async (req, res) => {
  const { userId } = res;            // set by methods.ensureToken
  const result = con.query("SELECT ...");
  res.json({ status: 200, data: result });
});

Pattern B — Express Router (routes/auth.js, routes/social.js, routes/paddle.js, routes/partnerAuth.js)

Standard Express:

const router = express.Router();
router.use(auth);                   // applies the auth middleware globally
router.get('/myEndpoint', async (req, res) => {
  const { userId, accountId } = res; // set by middlewares/auth.js
  ...
});
module.exports = router;

Both patterns coexist. Which one you use depends on the file you're editing — match the surrounding style.


Request lifecycle (authenticated endpoint)

HTTP request
nginx (production only)
Express app (server.js)
[middleware: session → fileupload → JSON parser → CORS → passport]
Route dispatch
  ├─ if /auth/*    → routes/auth.js   (uses router.use(auth))
  ├─ if /social/*  → routes/social.js (Passport OAuth)
  ├─ if /paddle/*  → routes/paddle.js
  ├─ if /partnerAuth/* → routes/partnerAuth.js
  └─ else          → routes/routes.js (registered via apiRoutes.init)
middlewares/auth.js  (or methods.ensureToken for the GET / route in routes.js)
  ├─ read `Token` header (name from TOKEN_HEADER_KEY)
  ├─ AES-decrypt with secret
  ├─ JWT-verify with JWT_SECRET_KEY
  └─ set res.userId, res.accountId, res.role, res.isPersonnel
Handler function
  ├─ `con.query(...)` (sync-mysql, blocks event loop)
  ├─ or `App.db.query(...)` (callback-style)
  ├─ or `await pool.query(...)` (mysql2/promise)
  ├─ may call helper/aiLogics.js for AI
  ├─ may call helper/ragProcess.js for RAG
  ├─ may emit a socket event via App.socket
  └─ return JSON

Webhook routes — special

Four routes use express.raw() body parsing (registered inside routes/routes.js):

  • POST /stripe_webhooks
  • POST /paddle_sandbox_webhooks
  • POST /paddle_production_webhooks
  • POST /paddle_Admin_webhooks

These need the raw request body for signature verification. The server.js middleware ordering explicitly excludes these paths from express.json().


Background jobs

Each job_*.js is independent of the HTTP server. They run as their own PM2 processes.

Anatomy of a typical job:

// job_facebook_publish.js (illustrative)
require('dotenv').config();           // implicit via conf.js
const cron = require('node-cron');
const mysql = require('sync-mysql');
const con = new mysql({ host, user, password, database });

cron.schedule('*/5 * * * *', () => {
  const due = con.query("SELECT * FROM tPost WHERE status='scheduled' AND postTime <= NOW()");
  for (const post of due) {
    publishToFacebook(post);          // calls FB Graph API
    con.query("UPDATE tPost SET status='published' WHERE id=?", [post.id]);
  }
});

Categories (see ../../audit/someli-api/jobs-inventory.md for the full inventory):

  • Publishing: job_*_publish.js for each platform
  • Token refresh: refreshes OAuth tokens before they expire
  • RAG sync: syncs knowledge-base files into Google Cloud RAG corpus
  • Paddle webhooks reconciliation
  • AI content scheduling
  • Misc maintenance

The dashboard/ sub-app

dashboard/ is the in-process analytics service. It has its own conf.js, its own routes (dashboard/routes/index.js, mounted at /auth — yes, sharing the prefix with routes/auth.js), and ~22 endpoints for impressions, growth, leaderboards.

In production, the in-process version is what runs. A standalone copy lives in the separate someli-dashboard-be repo for local dev with mocks — see ../../audit/someli-dashboard-be/.

The two have drifted. The production in-process copy has ~1615 more lines (primarily a leaderboard endpoint).


Code-overlap caveats

someli-api/helper/ was forked into Someli-admin-api/helper/ and designer-api/helper/. State today:

  • Byte-identical between someli-api and Someli-admin-api: tokenGenerator.js, revokeToken.js, ragProcess.js, webScraping.js — if you fix a bug in one, fix it in the other in the same PR.
  • Drifted: aiLogics.js, helper.js, constants.js, basic.js, index.js, stockImage.js. Don't assume parity — read the actual code before applying a fix.
  • designer-api/helper/ has only index.js (88 lines) — much thinner.

Always consult ../../audit/CODE-OVERLAP-MATRIX.md before changing any helper/* file.


Where to put a new file

Adding Put it in
A new authenticated REST endpoint routes/auth.js (Express Router pattern)
A new unauthenticated REST endpoint routes/routes.js (class-based init pattern)
A new OAuth strategy / callback middlewares/passport.js + a new routes/social.js route
A new background job New file job_<name>.js at repo root + entry in ecosystem.config.js
A new helper function Existing file in helper/ if it fits topically; otherwise new file under helper/
A new AI agent agents/<name>Agent.js
A new dashboard analytics endpoint dashboard/routes/index.js (consider whether to also port to someli-dashboard-be)
A new Paddle/Stripe webhook handler routes/routes.js (with express.raw() middleware locally) and update the bodyparser-exempt list in server.js

Next

04-getting-started.md — pick your first task and ship it.