API Conventions
The backend exposes a standardized set of endpoint categories. Every HTTP route falls into exactly one category, each with its own path prefix and API style.
Endpoint categories
| Category | Path prefix | API style | Purpose |
|---|---|---|---|
| Entity CRUD | /api/v1/entities/:entity/... | RESTful CRUD | Standard lifecycle operations on database-backed entities |
| Auth RPC | /api/v1/auth/... | RPC | Authentication, session, MFA, WebAuthn |
| System RPC | /api/v1/system/... | RPC | Infrastructure, config, runtime reads, health, OpenAPI |
| MCP | /mcp/... | Tool protocol | AI tool-calling protocol (OAuth + transport) |
| Webhooks | /webhook/... | RPC | External callbacks (US microservices only) |
| Well-known | /.well-known/... | RPC | Protocol-mandated discovery (OAuth) |
- SYSTEM is the default module for BE-internal RPC endpoints.
- AUTH is a separate module for authentication.
- ENTITIES is for RESTful CRUD only.
- MCP is a distinct tool-calling protocol for AIs — not AUTH, not RPC, not CRUD.
Entity CRUD
Database-backed entities with standard CRUD lifecycle use this pattern:
Code
:entity is always snake_case singular (e.g. customer, organization,
user_profile).
Bulk routes (bulk-delete, bulk-restore, bulk-update) and export are
per-entity opt-in: an entity exposes them only when its router registers
them, and a bulk op must be an atomic set operation — never a loop of single
writes. duplicate is the exception: it is ON by default for every
entity-factory router (<entity>.duplicate.bulk permission derived by
convention) and is switched OFF explicitly via duplicate: false — currently
disabled on organization, role_mapping, user_profile, ai_model, and
ai_cerebellum (IdP-synced or domain-lifecycle entities where cloning is
meaningless; the last two are served by the AI microservice via the
/ws/ai proxy, but duplicate stays off there too). organization has no bulk routes by architectural necessity
(per-record IdP sync) — see
Authentication → Organizations. Since
meta.actions is derived from the registered route table and always emits
the full standard op vocabulary, absent routes surface as explicit
enabled: false entries and the UI hides the corresponding actions.
Duplicate — clone semantics
POST /:entity/duplicate accepts { "uuids": [...] } and clones each source
row: a fresh uuid is generated, @Unique/@Key/audit/deletable columns are
reset, all other fields are copied verbatim, and cloned_from (when the
entity declares @CloneField) points at the source uuid. Every clone writes
an audit row with action CLONE (distinct from INSERT) so the version
history can tell "created via clone" from "created via form" — the delta is
the full new record. Per-item failures are collected in errors without
aborting the batch.
Export — meta-derived pipeline
GET /:entity/export?file_type=csv|xlsx|html streams the same row set the
list endpoint would return for identical filters/search/sort (one shared
filter builder — no duplicated query logic). The export layout is derived
from entity meta: columns = meta columns minus audit-actor (*_by),
version, deleted_at, and IANA-helper fields; header labels are resolved
from each column's label_key through the translations service for the
requested locale; datetime columns with datetime_iana_toggle are
rendered in the record's own timezone. Templates resolve by convention at
templates/<entity>_export_template.{xlsx,html}.
Delete semantics — soft vs hard capabilities
Deletion is capability-driven and declared per entity:
DELETE /:uuid— soft delete (deleted_at/deleted_bystamp). Registered only when the entity declares the delete capability.DELETE /:uuid/purge— hard delete (physicalDELETE FROM). Opt-in capability; entities that support ONLY hard delete register just this route — the softDELETE /:uuidroute does not exist and returns404. Entities supporting both expose both routes.POST /:uuid/restoreand thedelete.bulk/restore.bulkops exist only for soft-delete entities.
Which endpoint applies is never a client guess: meta.actions emits
delete.single (soft) and purge.single (hard) with enabled flags derived
from the registered route table. Enabled ops also carry
endpoint: { method, path } — the real registered route —
so the frontend dispatches write ops to their declared path (e.g.
user_profile writes under /api/v1/auth/users/*) instead of reconstructing
/entities/{entity}/:uuid by convention.
role_mapping is the current hard-delete-only entity: deleting a role
removes the local row AND the Casdoor role atomically (Casdoor first, local
second), so there is no deleted_at lifecycle and no restore.
Optimistic concurrency (?version=)
Every mutation that targets an existing row — PUT /:uuid,
DELETE /:uuid, DELETE /:uuid/purge, POST /:uuid/restore — requires the
caller-observed row version as a query parameter:
Code
- Missing or non-integer
version→400withinternal_code: VERSION_REQUIRED. - A stale version (row modified by someone else since you read it) → the
write's
WHERE ... AND version = $nmatches zero rows →409concurrency error. - The
versionfield is present on every entity row returned bylistandget— always send back the version you observed.
Write responses — the entity is the response
Every entity write (create, update, delete, purge, restore)
returns the authoritative entity row produced by PostgreSQL
RETURNING — never a wrapper like { "success": true } and never a
hand-built DTO. create uses 201, the other writes 200.
RPC endpoints under /api/v1/auth/* and /api/v1/system/* are exempt —
they orchestrate workflows (e.g. user creation = Casdoor + profile +
invitation) and return action-specific payloads.
List filters — canonical query-string format
GET /list accepts structured filters via QS bracket notation — the
only supported format (no JSON-stringified parameters):
Code
- Array values use repeated keys:
filters[0][value][]=A&filters[0][value][]=B. fieldandopare validated against the entity's allowed filter set.connectorapplies between consecutive conditions (AND/OR).
Aggregate columns in list rows
Some entities project server-computed aggregate columns into list rows —
e.g. organization rows carry user_count (a LEFT JOIN + GROUP BY
rollup computed in the same query as the page). These fields are read-only,
declared per entity, and behave like normal row fields in the response.
Write-payload standard ({entity} envelope)
Single-entity POST/PUT bodies MUST wrap the entity fields under entity
— flat entity fields at the body root are rejected with 400:
Code
translationsis optional:{key, language, value}rows persisted in the SAME transaction as the entity write. Requires thetranslation.*permission set (a missing/insufficient permission returns403)./api/v1/entities/translationitself takes{entity}only — atranslationssibling there is rejected.- Validation errors keep their shape; issue paths are prefixed with
entity.(e.g.entity.idp_name). - Exempt (keep their own bodies): bulk endpoints (
bulk-delete,bulk-restore,bulk-update,duplicate— already atomic via streams/temp tables), action endpoints,DELETE, and all RPC endpoints (/api/v1/auth/*,/api/v1/system/*). - Microservice entity endpoints (
/ws/:service/api/v1/entities/*) use the same{entity}envelope;translationsis rejected there. - Ext-JSON note:
extJsonBodyParserdecodes every JSON integer asbigint. Body schemas must acceptbigintfor integer fields — usezBoundedInt(min, max)fromsrc/http/validation.tsinstead ofz.number().int(). Query params stayz.coerce.number().
curl examples
Code
Auth RPC
Authentication endpoints under /api/v1/auth/... handle login, refresh,
WebAuthn, MFA, and user self-service. See Authentication
for the full endpoint list and flows.
System RPC
Infrastructure and runtime endpoints under /api/v1/system/...:
Code
POST /ws/ai/api/v1/system/docs/search is an authenticated RPC served by the
AI microservice (the docs knowledge base is AI-owned — the BE only proxies).
The caller supplies either raw query text or a precomputed embedding,
plus optional keywords, limit (1–20), and repo; the response contains
ranked documentation chunks from ai.docs_kb.
MCP
The MCP (Model Context Protocol) server exposes AI tool-calling endpoints
under /mcp/.... MCP is a distinct protocol with its own OAuth flow
(Authorization Code + PKCE + Dynamic Client Registration), its own token
verifier, and its own transport. It is NOT a variant of AUTH/CRUD/RPC.
Code
Naming conventions
| Layer | Name form | Example |
|---|---|---|
| PG table | snake_case plural | customers, user_profiles |
| TS entity class | PascalCase singular | CustomerEntity, UserProfileEntity |
| API URL entity segment | snake_case singular | /api/v1/entities/customer/... |
| Translation key entity segment | snake_case singular | entities.customer.title |
- Entity class = singular (one row schema)
- PG table = plural (collection of rows)
- URL entity segment = singular (resource type)
Next steps
- Authentication — Casdoor IDP integration, JWT, sessions, passkeys
- RBAC — the wildcard permission system and role mappings
- Architecture — request flow and HTTP layer internals