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 Go Module contract 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 synccatalog/apps.json is the single source of truth and the apps table 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 through observability.Guard (dan/eid/ebarimt/xyp/esign).
  • Adaptive load shedding (resilience/loadshedder.go) — returns 503 Service Unavailable once 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

  1. Shared middleware — logging, panic recovery, load shedding, Prometheus metrics, security headers, CORS.
  2. Authentication — the session token is read from the cookie or the Authorization: Bearer header and resolved against the sessions table. Only the SHA-256 digest of the token is stored.
  3. Tenant contexttenant_id is placed in the Go context and scopes every query.
  4. App gate — each module route checks app_installations; an uninstalled or disabled app returns 403 Forbidden.
  5. 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 billingcontacts.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