Architecture Specification
System architecture, layering and technical decisions behind
Gerege Nexus. The project is built on the
Gerege Template Platform
(open-gerege-mn-erp) template.
Монгол
·
English
1. System overview
Gerege Nexus is a high-performance modular monolith ERP and business application platform wired directly into Mongolia's national digital infrastructure.
1.1 High-performance modular monolith
- Zero-latency execution — business modules
(
contacts,products,inventory,billing,documents,developer_portal,gov_services,esign,ledger,office) implement the GoModulecontract and compile into a single binary. - Tenant app store — whether a module is active for a
tenant is decided dynamically from PostgreSQL
(
app_installations). - DAG dependency resolution — a directed acyclic graph plus semver constraints resolve module dependencies without cycles.
- Catalog sync —
catalog/apps.jsonis the single source of truth and theappstable is reconciled from it on every boot.
1.2 Cloud-native resilience (inspired by go-zero)
- Adaptive circuit breaker
(
resilience/breaker.go) — Google SRE sliding window error-rate rejection. Every live national-service call is guarded by a per-service breaker throughobservability.Guard(dan/eid/ebarimt/xyp/esign). - Adaptive load shedding
(
resilience/loadshedder.go) — returns503 Service Unavailableonce in-flight concurrency is exceeded. - Exponential backoff retry
(
resilience/retry.go) — retries transient failures.
1.3 State data exchange and identity
- XYP state exchange — citizen civil registration
(
WS100101) and legal entity data (WS100201). - DAN and E-ID (
eidmongolia.mn,developer.gerege.mn) — PKI digital signature, mobile OTP, bank SSO and biometric face verification. - OAuth2 / OIDC provider
(
/.well-known/openid-configuration) — the platform's own authorisation server: authorization code with PKCE (S256 only), RS256 id_tokens published through JWKS, rotating refresh tokens and a consent screen. It is the only way an external app learns who somebody is — the platform never runs anybody else's code.
Mock mode is a development convenience only; it is disabled automatically when
ENVIRONMENT=production.
2. Architecture diagram
+-----------------------------------------------------------------------------------+
| Gerege Nexus |
+-----------------------------------------------------------------------------------+
|
+-------------------------+-------------------------+
| |
+-------------------+ +-------------------+
| Next.js 16 Client | | Go 1.26 Backend |
| (App Router) | | (Chi Router) |
+-------------------+ +-------------------+
| |
+-------+-------+ +-------+-------+
| | | |
+---------------+ +---------------+ +---------------+ +---------------+
| AI Copilot UI | | E-ID / DAN | | Cloud-Native | | State Exchange|
| Drawer Panel | | SSO Provider | | Resilience | | (xyp.gerege) |
+---------------+ +---------------+ +---------------+ +---------------+
|
+---------------+
| Schema-per- |
| tenant Postgres|
+---------------+
3. Request pipeline
- Shared middleware — logging, panic recovery, load shedding, Prometheus metrics, security headers, CORS.
- Authentication — the session token is read from the
cookie or the
Authorization: Bearerheader and resolved against thesessionstable. Only the SHA-256 digest of the token is stored. - Tenant context —
tenant_idis placed in the Go context and scopes every query. - App gate — each module route checks
app_installations; an uninstalled or disabled app returns403 Forbidden. - Module handler — business logic and database transactions.
4. Core data model
| Table | Purpose |
|---|---|
tenants, users,
memberships |
Multi-tenancy and user membership |
roles, permissions,
role_permissions, membership_roles |
RBAC model |
sessions |
Server-side session tokens (SHA-256 digests) |
apps, app_installations,
installation_events |
App store and installation history |
contacts, products,
warehouses, stock_levels,
stock_movements |
Core business data |
billing_invoices, billing_payments,
billing_allocations, document_records |
Invoices, payments, allocations and digital documents |
oauth2_clients |
OAuth2 client applications |
ai_prompts, ai_knowledge |
Tenant AI configuration and knowledge base (migration
00005) |
gov_services, gov_applications,
gov_application_events, gov_appointments |
Government service registry, applications, appointments (migration
00006) |
gov_org_units, gov_unit_members,
gov_workflows, gov_workflow_versions,
gov_workflow_steps, gov_workflow_transitions,
gov_routing_rules, gov_tasks,
gov_upstream_connectors,
gov_delivery_outbox |
Configurable workflow engine — composite FKs including
tenant_id make cross-tenant links unstorable (migration
00007) |
access_change_events |
Access-control change audit trail (migration
00008) |
esign_documents, esign_signature_logs,
esign_cert_checks |
PDF e-sign documents, signature log, certificate checks (migrations
00009, 00011) |
audit_events, national_identity_links |
Audit trail and national-identity links (migration
00010) |
oauth2_tokens |
OAuth2 access tokens, digests only (migration
00015) |
oauth2_authorization_codes,
oauth2_refresh_tokens, oauth2_consents,
oauth2_signing_keys |
The authorization code flow: single-use codes, rotating refresh
chains, per-person consent, and the encrypted signing key (migration
00047) |
platform_events |
Cross-module event outbox — written inside the producer's
transaction and dispatched at-least-once by an in-process poller
(migration 00035) |
ledger_accounts, journal_entries,
journal_lines |
Double-entry, append-only general ledger; a cancellation is a
reversing entry, never a delete (migration 00039) |
webhook_endpoints, webhook_deliveries |
Tenant webhook endpoints and a durable delivery log with retry
(migration 00040) |
platform_admins |
Platform operators — authority above any tenant; each grant is an
auditable row (migration 00041) |
tenants.status |
active / suspended — platform-level
suspension (migration 00041) |
sessions.impersonated_tenant_id,
sessions.impersonation_reason |
A platform operator's 30-minute stay inside a workspace;
tenant_id stays NULL so the session↔︎membership constraint
does not apply (migration 00042) |
office_documents |
Office module docx/xlsx files; version advances on
every save (migration 00044, tenant plane
0003) |
Where they live: platform_events and
webhook_endpoints/deliveries stay in public as
queue infrastructure (two-second pollers must discover every tenant's
work in one query). Other business tables live in each tenant's
tenant_<uuid> schema; identity, RBAC, the app store
and the audit trail stay in public.
See DATABASE_SCHEMA.md
for the full schema reference.
All schema changes go through goose migrations in
backend/db/migrations/. Runtime DDL is not allowed.
4.1 External service boundary
The following systems are external service workers we do not own — the platform only consumes them through clients and never re-implements their internals:
| Service | Client | Role |
|---|---|---|
| XYP (ХУР) | platform/gerege/xyp.go |
State data exchange |
| E-ID / DAN | platform/eid, platform/dan |
National identity |
| Gerege eSign HSM | platform/gerege/esign.go |
Digital signatures (PKCS#7) |
| e-Barimt | platform/ebarimt (PosAPI 3.0) plus billing's async
submitter |
Tax receipts (ДДТД, lottery) |
Every client follows the same contract: (1) mock mode via
config.MockEnabled — off by default in production; (2)
error-classified retry (transport/5xx only — a 4xx or a business
rejection is never repeated); (3) context cancellation honored; (4)
national_service_calls_total and
national_service_mock_mode metrics. Everything else
we build ourselves — the platform's purpose is to make creating
new business apps fast and free (make new-app scaffolding,
see MODULE_AUTHORING_GUIDE.md).
4.2 How modules talk to each other
Apps share a binary but never reach into each other's tables. Three sanctioned channels:
| Channel | When | Example |
|---|---|---|
| Master-data service call (direct, synchronous) | A lookup another module owns | billing → contacts.Service.Get |
| Snapshot | A legally significant value must freeze on the document | The contact name on an invoice — a later rename never rewrites it |
Event bus (platform/events) |
A module announces what it did and others react | Publish (writes the outbox row INSIDE the producer's
transaction) and PublishOutside (outside it), both through
the platform_events outbox and a poller, at-least-once.
Producers: billing (invoice.created,
invoice.cancelled, credit_note.created,
payment.posted, payment.cancelled, each in the
same transaction as the thing it describes, so there is no window in
which one exists without the other), esign
(document.signed), documents
(signed/approved). Consumers:
ledger subscribes to all five and posts a balanced journal
entry (an invoice debits receivables; a payment debits cash or bank and
credits receivables, dated on the day the money moved;
a cancellation reverses that entry; a credit note posts an entry of
its own rather than a reversal — debit sales and VAT,
credit receivables); SubscribeAll fans every event out to
the tenant's webhooks. PublishSync was
removed (2026-08-21): it dispatched only type-keyed handlers
and therefore skipped SubscribeAll, silently dropping the
webhook fan-out for exactly the events somebody had chosen to deliver
reliably |
Tenant webhooks
(platform/webhook) |
Notifying an external tenant system | SubscribeAll fans every event out to tenant-registered
endpoints with durable, retried delivery (this replaced the in-memory
integration.Manager) |
Capability discovery
(appregistry) |
Finding what another module provides, by interface, without importing it | FindCapability[T]() /
FindCapabilities[T]() — inventory implements
internal.StockAvailability; when no app provides it,
discovery returns "none" |
| Transactional outbox (asynchronous) | Handing a specific delivery to an external system | gov_delivery_outbox → upstream agencies; invoices →
e-Barimt |
Dependencies run one way: master-data modules (contacts,
products) sit at the bottom of the graph and depend on
nothing above them, and each consumer declares the narrow interface it
needs at its own call site.
Full rationale: RESEARCH_ERP_MODULARITY.md.
5. Architectural decisions
| Decision | Rationale |
|---|---|
| Modular monolith over microservices | In-process calls avoid network latency; module boundaries are enforced by Go interfaces |
No ORM (pgx plus hand-written SQL) |
Keeps queries explicit and tunable, avoids hidden N+1 |
| Schema-per-tenant multi-tenancy | Business tables live in each tenant's
tenant_<uuid> schema, control tables in
public. dbguard binds every acquired
connection to the request's tenant through search_path, so
isolation is physical rather than a filter. The application's
WHERE tenant_id remains the first layer. The
00037 RLS policies stay in the database as the rollback
path, but the binary no longer depends on them |
| Catalog file as source of truth | Adding an app needs no manual SQL; the apps table syncs
automatically |
| Opaque session tokens | Avoids the revocation problem of stateless JWTs |
6. Maintainers
- Gerege Systems Development Team (@gerege-systems)
- Gemini AI, Claude AI