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):
- Load env:
const conf = require('./conf');—conf.jsdoesrequire('dotenv').config()then re-exportsprocess.env.*. - Create Express app.
- Wire middleware (order matters):
express-session(Passport-OAuth uses this)express-fileuploadexpress.json({ limit: '150mb', parameterLimit: 50000 })— applied to all routes except the four webhook paths- Static:
/favicon.ico,/uploads - Healthchecks:
/health,/db-health passport.initialize()+passport.session()cors()- Connect to MySQL:
modules/dbDriver/lib/mysql.js → db.connect(conf, callback). - Initialise Socket.IO on the HTTP server.
- Build the shared
Appobject:{ db, server: app, socket: io }. - Re-export
appData = Appasmodule.exports.appData— so other files (notablydashboard/) can re-enter the running app at runtime. - Mount route modules in this order:
/auth/*→routes/auth.js/social/*→routes/social.js/paddle/*→routes/paddle.js/partnerAuth/*→routes/partnerAuth.js/authagain →dashboard/routes/index.js(sharing the same prefix is intentional)/(root) →routes/routes.js(theapiRoutes.prototype.init()pattern)- 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_webhooksPOST /paddle_sandbox_webhooksPOST /paddle_production_webhooksPOST /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.jsfor 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-apiandSomeli-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 onlyindex.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.