The reference architecture I clone across every commerce app
Managerenta is a property/rental management app. It is also, quietly, the most reused thing I've built — because its layering became the template every other commerce app inherits. This is a tour of that template and, more usefully, of why each piece is shaped the way it is.
Why have a template at all
Every new product arrives with genuinely novel parts — a domain, a lifecycle, a pricing rule — and a large sameness underneath: how a request authenticates, becomes a validated call, hits the database, gets cached, and comes back as JSON. If that sameness is re-improvised each time, the novelty and the plumbing get debugged together, forever. Managerenta freezes the plumbing so a new app's budget goes to its actual problem. Chekka and Mogadget are literal descendants.
One boundary, three layers
All server code lives under src/server/ and imports "server-only", so the DB,
S3, and secrets can't leak into the client bundle. Above that boundary sit three
layers, each with one job:
| Layer | Job | Forbidden from |
|---|---|---|
| Model | Mongoose schema + flat xxxDB() fns; invariants in schema hooks | HTTP, caching |
| Service | business logic + Redis caching; one function per file | req / Response |
| Route | authorize → safeParse → call service → envelope | DB, cache keys, S3 |
The rule that makes the layers independently testable is that model reads never
throw. Each *DB function wraps a Prometheus timer and, on error, returns
null/empty — so the service layer decides what "nothing" means and the route
decides the HTTP, with no exceptions crossing a boundary by surprise:
const timer = databaseResponseTimeHistogram.startTimer();
try {
const result = await Property.findOneAndUpdate(
{ _id, userId, deleted: false }, { $set: payload }, { returnDocument: "after" });
if (!result) throw ErrPropertyNotFound;
timer({ operation, collection, method: "updatePropertyDB", success: "true" });
return { ...result.toObject(), id: result.id };
} catch { timer({ ..., success: "false" }); return null; }
Notice ownership is a query filter, not a later check: a handler that forgot to scope its read simply selects nothing, because there is no unfiltered read to call by mistake.
The wrapper's order is load-bearing
withApiHandler composes the cross-cutting concerns, and the sequence is a design
decision, not an accident. The CSRF Origin/Referer gate runs before the rate
limiter — it's the cheaper check (no Redis round-trip), and a rejected request
must not spend one of the caller's rate-limit tokens:
if (options.csrf !== false) {
const reason = csrfReject(req);
if (reason) { const res = fail(403, reason); observe(req, res.status, options.route, startNs); return res; }
}
if (rl) { rlResult = await enforceRateLimit(req, rl);
if (!rlResult.allowed) return applyRateLimitHeaders(fail(429, "Too many requests"), rlResult); }
await connectMongoDB();
Authorization that survives a bad policy
Authorization is a real AWS-IAM-style policy engine, not scattered role checks.
withAuth first resolves an effectiveOwnerId — the user's own id in personal
scope, or the org owner's id once they've switched into an organization — so org
members transparently act on the owner's resources through one evaluation path.
Personal-scope users even get an in-memory selfScopePolicy, so the engine has no
special case:
if (!auth.organizationId) {
const principalArn = principalArnForUser(auth.userId, auth.userId);
return { principalArn, policies: [selfScopePolicy(auth.userId)] };
}
The engine itself is pure — no I/O, no clock read (time is injected via the condition context) — which is exactly why it can be exhaustively unit-tested, and it follows AWS semantics: default-deny, explicit-Deny-wins. But the property that lets me trust multi-tenancy sits above the policy loop: a tenant-isolation floor denies any cross-scope org request before a single statement is read.
if (target.plane === "org" && target.orgId !== resourceScope(auth)) {
return { decision: "deny", reason: "implicit deny (cross-scope org resource)" };
}
Even a wildcard policy can't cross tenants, because the boundary is checked before the policy is.
Receipts, not vibes
The reason it's a reference is that it's proven. 93 Vitest files run against
throwaway scratch DBs that a globalSetup drops on teardown; 16 Playwright
specs drive auth, 2FA, passkeys, and a full-app path; and a three-pass
SECURITY_REVIEW.md catalogues roughly forty findings. The one I quote most is
S1: "2FA was never enforced on login; the toggle was decorative" — a feature that
looked done, wasn't, and was found by writing the audit down. It was fixed with a
two-step ticket flow: password success mints a 5-minute signed ticket and sets no
session cookie until a second call verifies the TOTP code.
Getting the skeleton right once, cloning it, and re-running that discipline on each descendant is the whole payoff: the next app argues about its domain, never about how a request becomes a database write.