2. Core Concepts & Architecture
2.1 The six building blocks
Everything MangoX exposes is described by JSON documents of six types. Each type lives in its own folder:
| Type | Folder | Answers the question | Example |
|---|---|---|---|
| Entity | entity/ | What does the data look like and where is it stored? | tag: fields title, slug, tag_group; stored in collection tag |
| Resource | resource/ | Which URL segment exposes it? | /api/v1/tag |
| Action | action/ | Which HTTP method + sub-path is an operation? | list = GET /, read = GET /:id, approve = PATCH /:id/approve |
| Role | role/ | Who is the caller? | admin, guest, editor, … |
| Policy | policy/ | May this role run this action on this resource, and with which filters/fields? | admin may list/read/create/update/delete tag, sees created_by(username,full_name) |
| Setting | setting/ | Which policy should apply, based on data? | "use policy-team-read-full if my membership role is admin, otherwise policy-team-read-basic" |
Their relationships:
┌──────────┐ 1 n ┌──────────┐
URL ───────►│ resource │──────►│ entity │◄──── json_schema, relations, plugins
└────┬─────┘ └──────────┘
│ matched with
┌────▼─────┐
method+path ──►│ action │
└────┬─────┘
│ (resource, action, role)
┌────▼─────┐ optional ┌──────────┐
JWT role ───►│ policy │◄─────────────│ setting │
└────┬─────┘ picks one └──────────┘
│ root_entity, condition, data, …
▼
MongoDB query
2.2 JSON layout and scopes
json/
├── system/ ← scope "system": shared defaults shipped with the code
│ ├── action/<slug>.json
│ ├── entity/<collection_name>.json
│ ├── policy/<slug>.json
│ ├── resource/<slug>.json
│ ├── role/<slug>.json
│ └── setting/<slug>.json
└── <TEAM_ID>/<TENANT>/ ← scope "<TEAM_ID>/<TENANT>": per-tenant overrides
├── entity/…
├── policy/…
└── …
- The file name is the item's key:
collection_namefor entities,slugfor everything else. - A tenant item replaces the system item with the same key. Everything else is inherited from
system. - Items created or edited through the admin API are written copy-on-write into the tenant folder. Files under
json/system/are never modified by the API, and a tenant cannot delete a system item (403). - Other recognised types:
collection,code,form,rule,form-setting,api-config.
From files to Redis
- At startup
schemaSync.bootstrap()flushes theschema:<APP_NAME>:*keys and loads every JSON file into Redis:schema:<APP_NAME>:global:<type>forsystemschema:<APP_NAME>:tenant:<TEAM_ID>/<TENANT>:<type>for tenant scopes
- Requests read the configuration from Redis (with an in-process cache in front).
- Changes made through the API (
POST/PUT/DELETE /api/v1/<type>) are written to the JSON file and pushed to Redis immediately. No restart is needed. - Changes made by editing files by hand are only picked up after a restart: the file watcher is intentionally disabled.
POST /api/v1/admin/reloadreloads the in-memory entity cache from Redis. Use it when another process has rewritten the Redis keys. See Deployment.
2.3 Identities and roles
A request is executed with a list of roles:
| Caller | Roles used for policy matching |
|---|---|
Anonymous call to /api/v1/front/* | ["guest"]. A token is ignored on these routes |
| Authenticated user | [user.role_name] taken from the JWT (for example "admin", "editor", "user") |
Super administrator (is_super_admin: true or role_system: "admin") | ["super_admin", "admin"], and the super-admin bypass applies (see below) |
| Authenticated, no role at all | ["default"] |
Tokens are verified by src/module/_auth/guards/jwt.guard.ts:
- HS256 tokens signed with
JWT_SECRET. These are issued byPOST /auth/login. - RS256 tokens signed by an external identity provider (SSO), verified with
SSO_PUBLIC_KEY/SSO_PUBLIC_KEY_PATH. If such a token carries atenant_idthat differs fromTENANT, it is rejected unless the user is a system admin. - Revoked tokens (logout, "revoke all sessions") are rejected through a Redis blacklist.
Roles are free-form strings. json/system/role/*.json only lists them for admin UIs. What a role can do is defined entirely by the policies that mention it.
Super-admin bypass. For a super administrator no policy has to match. The engine still borrows the highest-priority policy written for role admin on that resource, to reuse its select (joins). Its row filters are applied as well, which matters for policies that use @context filters (see Known issues).
2.4 Request lifecycle
Take GET /api/v1/tag?title=ilike.news as an example:
- Route. The wildcard controller (
src/module/common_v2/common.ts) receives the request. Public calls under/api/v1/front/*go throughsrc/module/_front/front.controller.tsinstead. - Authenticate. The JWT guard verifies the token and builds the role list.
- Resolve resource + action (
loadAction,src/module/_setting/setting-mode.ts):- The first path segment (
tag) is looked up among resources (tenant first, then system). - The method and the rest of the path are matched against every action's
method+path:GET+/giveslist. - Authenticated routes only match actions with
auth: true;/front/*only matches actions withauth: false.
- The first path segment (
- Tenant check. If the resource has
is_tenant: true, thex-tenant-idheader is required. Its value is injected into the body on writes and added as atenant_idfilter on every read, update, delete and join. - Defaults. For list calls
limitdefaults to10,pageis converted toskip, and the default order is-created_at,-updated_at,-timestamp,-_id. - Policy (
builderQuery,src/module/common_v2/helper/builder.ts):- If a setting exists for (resource, action, first role), its dispatcher may pin a specific policy.
- Candidate policies = those whose
resource,actionandroleall match, sorted bypiority(highest first). - For each candidate: run its
datacontext queries and itscode_context, then evaluatecondition_context. The first policy that passes wins. - If none passes: 403 Forbidden.
- Writes: the body is checked against
condtion_body(and an optionalform). - Reads, updates and deletes: the policy's
conditionis merged into the client query. The policy'sselectreplaces the client's, and its filters are ANDed.
- Execute. The query runs against the policy's
root_entity. The core service:- applies the entity's
json_schema(validation and field filtering on writes, projection on reads); - builds the aggregation pipeline (filters,
$lookupjoins, sort, paging); - runs the entity's plugins.
- applies the entity's
- Post-process. The optional policy
codehook may transform the result, and an optionaltriggermay fire. - Respond with the standard envelope.
2.5 What runs where
| Concern | Code |
|---|---|
| Boot, MongoDB/Redis connection | src/index.ts, src/configs/core/unified.ts, src/configs/core/bootstrap.ts |
| JSON files ↔ Redis | src/core_v2/store/json-store.ts, src/core_v2/store/schema-sync.ts, src/core_v2/schema/manager.ts |
| Admin CRUD of JSON configuration | src/module/_setting/setting.controller.ts, setting.adapter.ts |
| Resource/action/policy/setting lookup | src/module/_setting/setting-mode.ts |
| Policy engine | src/module/common_v2/helper/builder.ts, excute.ts, helper.ts, condition.ts |
| Query language → intermediate query | src/core_v2/query/converter.ts |
| Intermediate query → MongoDB pipeline | src/core_v2/adapters/mongodb/ |
| Validation (AJV), field filtering | src/core_v2/schema/validator.ts, src/core_v2/authorization/authorization.ts |
| Relations | src/core_v2/schema/loader.ts, src/core_v2/adapters/base/relationship-registry.ts |
| Plugins | src/core_v2/plugins/definitions/*.plugin.ts |
2.6 Database adapters
Each entity chooses its adapter with databaseType:
| Value | Status | Notes |
|---|---|---|
mongodb | ✅ supported | The default. Every shipped entity uses it |
rest | ✅ supported | Proxies the entity to an external REST API described by an api-config record (api_config on the entity). Filtering, select, sort and paging are passed through according to that config's capabilities |
SQLite or SQL backends are not available in this version. Some comments still mention SQLite, but no SQL adapter is registered.