Skip to content

Architecture

Bounded contexts and the gateway

3 min readUpdated Verified against a661527 on 2026-09-26

Page actions

ChatGPT and Claude are third-party services. The link gives them this page's public address and a short instruction. This site records nothing.

How one codebase runs as five contexts, how a process is sliced with SGGS_MODULES, and how the /api routing is generated from the route table and proven on every staging deploy.

The API is one stdlib module tree, webapp/sggs/, factored into five bounded contexts. Any of them can run alone as a service from the same image, and the routing that sends each /api prefix to the right place is generated from the code, never written by hand (ADR-0010, ADR-0011).

Bounded contexts and the gateway The 29 routes are tagged with five bounded contexts (reader, search, verify, insights, knowledge), each declaring the tables it reads. SGGS_MODULES selects the contexts a process serves and readiness refuses a database missing any declared table. gateway/routes.json says per environment where /api goes; tools/gen_gateway.py generates the Vercel rewrites from the route table, the API functions are built at deploy time, and CI proves the routing with X-Service probes and the 311-record golden contract. 09 · Bounded contexts and the gateway One codebase, five contexts: how a function serves a slice of it, and howrouting is generated so it cannot drift Five contexts, one codebaseRouting, generated from the codeProven on every staging deploy reader — pages, banis, healthreaderpages, banis, healthsearch — search, wordsearchsearch, wordverify — quotation verdictsverifyquotation verdictsinsights — themes, analyticsinsightsthemes, analyticsknowledge — raag timing, formsknowledgeraag timing, formsCONTEXT_TABLES — each its own tablesCONTEXT_TABLESeach its own tablesSGGS_MODULES=all | reader,search,… — a process serves only the contexts it names; readiness needs their tablesSGGS_MODULES=all | reader,search,…a process serves only the contexts it names; readiness needs their tablesserve.ROUTES — path → handler, contextserve.ROUTESpath → handler, contextgateway/routes.json — where each /api goesgateway/routes.jsonwhere each /api goestools/gen_gateway.py — routes → Vercel rewritestools/gen_gateway.pyroutes → Vercel rewriteswritesbuildsfrontend/vercel.json — the rewrites, committedfrontend/vercel.jsonthe rewrites, committedAPI functions — one per context, plus all — built at deploy, not keptAPI functionsone per context, plus allbuilt at deploy, not keptgen_gateway.py --check — vercel.json = generatedgen_gateway.py --checkvercel.json = generated--verify-output — each function: its slice only--verify-outputeach function: its slice onlyX-Service probes — each context answers itselfX-Service probeseach context answers itselfThe golden contract — 311 records via the gatewayThe golden contract311 records via the gatewayReversible in one line — remove a context from routes.json and the all function answers it againReversible in one lineremove a context from routes.json and the all function answers it again component in this projectdata at restexplanation or settingpin: a reviewed lock filegate: fails the build or the deployproven on every staging deployflow: data or control moves this wayreference: pins, reads or derives from Source of truth: webapp/serve.py · gateway/routes.json ·tools/gen_gateway.py · tools/build_api_functions.py v1.3.10 · verified 2026-09-27 · d2f2e41Sri Guru Granth Sahib Ji — Knowledge Base · engineering wiki
Poster 09 — five bounded contexts, module slicing, and the gateway generated from the code · open full size

The contexts and their routes

ContextRoutes (/api/…)Module
readermeta, ang, shabad, health, random, lines, banis, banisggs/reader.py
searchsearch, wordsggs/search.py
verifyverifysggs/verification.py
insightsthemes/network, analytics/*, related, line_concepts, neighborssggs/insights.py
knowledgetiming/clock, timing/raag, timing/divergence, formssggs/knowledge.py

No two contexts share a path prefix, so any route can be sent to exactly one service. Each module declares the tables it reads (TABLES); CONTEXT_TABLES in serve.py collects them and is the source for the per-service database slices (make slices).

Running one context

Terminal window
SGGS_MODULES=search SGGS_DB=build/slices/search.sqlite python3 webapp/serve.py

SGGS_MODULES is all by default (the monolith) or a comma list; an unknown name is an error, a route outside the enabled set is a 404, and startup refuses a database missing any declared table (/readyz reports ready, contexts, missing_tables, version, commit).

Routing, generated

gateway/routes.json says per environment where /api goes. Staging and — on integration — production both list all five contexts, so each has its own Vercel function (api/svc/<context>) and all catches the rest. Production today (v1.3.10, released before the split) still answers every path with all; the five contexts go live there together in a later release, after the app is on the App Store (owner decision 2026-09-26, services-production step 3). Setting api_platform: render proxies /api back to the Render single API — the rollback. tools/gen_gateway.py combines it with serve.ROUTES and writes the rewrites in frontend/vercel.json: every prefix of a routed context and its /api/v1 twin to its function, everything else to all — staging’s rules first, host-conditioned, then production’s, host-less. At deploy time tools/build_api_functions.py generates the functions themselves; nothing generated is committed.

What CI proves

  • gen_gateway.py --check: the committed rewrites equal what the code generates.
  • build_api_functions.py --verify-output: each function bundles only its entry, the API code and its own database slice.
  • X-Service probes: each routed context answers its own routes; /api/nope falls through to all.
  • The whole golden contract (311 records: the seven suites tools/contract_http.py replays) replays through the gateway on the staging site.

Reversal is one line: remove a context from gateway/routes.json and all answers it again on the next deploy. Production changes only when the move is released through the normal pipeline, and that release owes the performance gate — tools/perf_baseline.py --compare docs/perf/baseline-2026-09.json from the same vantage, p95 within +10 % per context — besides the data canary (runbook).