Roles, Permissions, and Routes
This documentation serves as a technical reference (including for AI) regarding the access levels in the Flowi Agentic platform.
The platform has a Multi-tenant SaaS architecture. Access control rests primarily on the user's global_role column — which is either SUPER_ADMIN or nothing — and on the user's role within the context of a Tenant, stored in user_tenant_memberships.platform_role.
No role comes from an external provider
The session is always the platform's own JWT, issued and validated by the application itself (security/JwtTokenProvider.java, service/AuthService.java). OIDC login does exist — the SSO connections under /admin/sso, handled by SsoConnectionService and SsoCallbackService — but it only authenticates: JIT provisioning writes globalRole = null (SsoCallbackService.provision), no directory group becomes a role, and no tenant membership is created. Anyone looking for an identity console to grant roles will not find one: roles are granted in the platform's own database, through /admin/users and /admin/tenants/{id}/members.
1. Profiles and Access Levels
A. Super Admin (Global Admin)
- Frontend Identification:
roles.includes('SUPER_ADMIN') - Backend Identification:
hasRole('SUPER_ADMIN'), resolved from the global role claim in the JWT the platform issues itself. - Scope: The entire system. Not tied to a specific tenant in the global administration interface.
- Responsibilities: Register tenants, manage global process definitions (BPMN/CMMN), DMN templates, configure AI, and manage all users.
B. Tenant Admin (Tenant Administrator)
- Frontend Identification:
!isSuperAdmin&¤tTenant?.role === 'ADMIN' - Backend Identification:
@tenantSec.isCurrentTenantAdmin(authentication)on/a/*resources. Reaches nothing under/api/admin/**—SecurityConfigrequiresSUPER_ADMINon that matcher, and a Tenant Admin's authority isROLE_ADMIN. - Scope: Restricted to their own Tenant's data.
- Responsibilities: Configure the default Business Key for the tenant, enable/disable published processes in their catalog, manage robots (AI Agents), API Keys, and Webhooks for the tenant. Tenant members are not on that list:
/api/admin/tenants/{id}/members*isSUPER_ADMINonly.
B.1. Tenant Modeler
- Frontend Identification:
currentTenant?.role === 'MODELER', resolved byreaches()insrc/lib/access.ts— the single place in the frontend where a role becomes reach. - Backend Identification:
@tenantSec.isCurrentTenantModeler(authentication). - Scope: Restricted to their own Tenant's data.
- The rule, in one line:
MODELERisADMINminus the tenant's keys, its robots, its webhooks and its people. They design, configure and publish processes, and use the modeller copilot; they do not administer the organisation. - Reaches: the process catalogue and the modeller (including publishing a version), forms, decision tables, process roles, process variables, document types, knowledge bases, AI agents, the CMS, the e-mail trigger rule (under the definition's Configuration tab) and the e-mail deliveries.
- Does not reach: tenant members, robots and runners, API Keys, webhooks and mail inboxes — an inbox holds server credentials, the same nature as an API Key.
- It is not a rank. They do not inherit the tenant-wide view a Manager has: at runtime a modeller sees what a
USERsees./monitorstill requires Manager or Admin.
The copilot spends the tenant's AI quota
Granting the copilot to the modeller means someone who is not an administrator spends the tenant's money. That is a deliberate trade — see specs/core/modeler-role.md. It is why the usage row carries the user id, not only the tenant's.
B.2. Tenant Manager
Frontend Identification:
currentTenant?.role === 'MANAGER'Backend Identification:
@tenantSec.isCurrentTenantManager(authentication)— acceptsMANAGERandADMINwith a membership (UserTenantMembership) in the current tenant;SUPER_ADMINalways passes.Scope: Restricted to their own Tenant's data.
Responsibilities: Two modules recognise the manager today.
- CMS — creates, edits and deletes collections and records, bulk-loads reference tables and clears collections.
- Process definitions — per-step monitoring (
/a/definitions/{key}/monitorand/monitor/{activityId}) and writing the process documentation (PUTandDELETEon/a/definitions/{key}/documentation) accept a manager. Reading the documentation only requires being authenticated.
In every other module a manager still behaves as a Tenant User. When adding a new module, decide explicitly whether it should recognise the manager.
B.3. Tenant Modeler
- Frontend Identification:
currentTenant?.role === 'MODELER' - Backend Identification:
@tenantSec.isCurrentTenantModeler(authentication)— acceptsMODELERandADMINwith a membership (UserTenantMembership) in the current tenant;SUPER_ADMINalways passes. An API key has no membership and therefore never passes. - Scope: Restricted to their own Tenant's data.
- The rule, stated once:
MODELERisADMINminus the tenant's keys, its robots, its webhooks and its people. They design, configure and publish processes; they do not administer the organisation. - Responsibilities: process catalogue, modeler and versions — including publishing a version; forms; decision tables (DMN); process roles, process variables and document types; knowledge bases and the process's AI agents; the Java delegate selector; the process documentation; the mail trigger rule (
/a/mail-rules), which now lives on the definition's Configurações tab, and the delivery log (/a/mail-deliveries) — a message that started no process is a diagnosis about the design. They also drive the modeler copilot and AI validation. - What they cannot reach: tenant members (
/a/members), robots and runners (/a/robots,/a/robot-runners), API keys (/a/api-keys), webhooks (/a/webhooks) and mail inboxes (/a/mail-inboxes, which hold server credentials — the same nature as an API key). To pick the inbox a rule uses they read/a/mail-rules/inboxes, a projection returning only id, name and status — never host, username or password reference. - They are not a rank above the Manager.
MODELERis not inPlatformRole.seesEverythingInTenant(): at runtime they see what aUSERsees. Cancelling, suspending or reactivating an instance, reading the failed-job queue and following/monitorremain the Manager's.
A non-administrator now spends the tenant's money
The copilot and AI validation consume the tenant's token quota. Granting the modeler those screens is a deliberate trade — the alternative is a role that cannot use the tool the product is built around. That is why the ai_usage_logs row for a copilot turn carries the user_id on top of the tenant, not only the tenant: the governance screen has to answer who spent it. A per-person ceiling exists in user_ai_quotas and is honoured both on the preflight check and on the debit (TenantAiQuotaService); today it is only written by PUT /api/admin/users/{id}/ai-quota, which is Super Admin.
C. Tenant User (Standard User)
- Frontend Identification:
currentTenant?.role === 'USER' - Backend Identification: Restricted access via operational endpoints (
/a/*), with the tenant coming from the session token. - Scope: Restricted to the operation of their own Tenant.
- Responsibilities: Start process instances, execute tasks (
/tasks), fill out forms, and view operational dashboards.
2. Frontend Routes (React / TanStack Router)
The table below crosses frontend routes with each profile's visibility — what the sidebar (components/Sidebar.tsx) shows and what each route's beforeLoad refuses. It is not the authorization matrix — section 3 is what governs — but since the modeller role it is complete: every restricted route goes through requireArea(), and the route-to-area map lives in src/lib/access.ts. There are four areas: design (Admin and Modeller), content (Admin, Modeller and Manager), organisation (Admin only) and tenantWideMonitoring (Admin and Manager).
The refusal is no longer silent: requireArea() sends the person to /dashboard with ?denied=<area>, and the Dashboard explains in translated text why that screen is not theirs.
A ❌ in the Super Admin column means "not their route", not "blocked": no beforeLoad refuses a Super Admin, and a Super Admin who selects a tenant then sees the same tenant entries in the sidebar.
| Route / Path | Module / Function | Super Admin | Tenant Admin | Modeller | Tenant User |
|---|---|---|---|---|---|
/dashboard | Overview, charts, and counters. | ❌ (Uses admin panel) | ✅ | ✅ | ✅ |
/admin/tenants | Global listing and creation of tenants. | ✅ | ❌ | ❌ | ❌ |
/admin/users | Global listing of users. | ✅ | ❌ | ❌ | ❌ |
/admin/ai-config | AI configuration, in two tabs: LLM Model Catalog and Vector Database (RAG). | ✅ | ❌ | ❌ | ❌ |
/definitions | Local Definitions (Process Catalog) - For the Admin and the Modeller: views published processes, enables/disables, publishes a version, and configures local instances, document types and the e-mail trigger. | ❌ | ✅ | ✅ | ❌ |
/admin/global-templates | Global Templates - For SA: Lists all, allows creating BPMN/CMMN/DMN and configuring global Document Types, Variables, and Attachments. | ✅ | ❌ | ❌ | ❌ |
/admin/process-roles | Centralized catalog for Process Roles. | ✅ | ❌ | ✅ | ❌ |
/admin/global-templates/$key | Template details and advanced configuration tabs. | ✅ | ❌ | ❌ | ❌ |
/instances | Listing of active instances, history, and errors. | ❌ (Global management via API only) | ✅ | ✅ | ✅ |
/instances/start/* | Form to start instances. | ❌ (Restricted to prevent tenant-less instances) | ✅ | ✅ | ✅ |
/tasks | Inbox and human task execution. | ❌ | ✅ | ✅ | ✅ |
/cms | Dynamic content management for the tenant. - The screen requires a Manager, a Modeller or an Admin; a standard user is refused, and told why on screen. - Writing (create/edit/delete/bulk load): Manager or above. - ⚠️ The read API ( GET /a/cms/**) is still open to any authenticated member, bounded by the collection's confidentiality level. Hiding the route in the frontend does not close the endpoint. | ❌ | ✅ | ✅ | ❌ |
/admin/knowledge-bases | The tenant's knowledge bases, grouped with the CMS under Content. - The screen requires a Manager, a Modeller or an Admin, and the API requires the same role — reading included. Hiding the route on the front end is not what closes the endpoint. | ❌ | ✅ | ✅ | ❌ |
/monitor | Per-step monitoring: process diagram with the work parked at each activity. Manager or above. | ❌ | ✅ | ❌ | ❌ |
/admin/document-types | The tenant's document type catalogue, under Modeling. - The screen requires an Admin or a Modeller, and the API requires the same to create, edit and delete. - Reading ( GET /a/document-types) goes down to Manager, because it is what fills the type selector in the form builder; an ordinary user gets 403. | ❌ | ✅ | ✅ | ❌ |
/decisions | Decision Tables (DMN), under Modeling → Decision Tables. - beforeLoad (requireArea('design')) refuses anyone who does not design processes — Admin, Modeller and Super Admin pass — because GET /a/decisions already required that level: a plain user only saw an empty list and buttons that answered 403.- ⚠️ Evaluation ( POST /a/decisions/{key}/evaluate) remains open to any authenticated member — that is how processes consult the table. | ✅ | ✅ | ✅ | ❌ |
/robots | Automation and bots linked to the tenant. - The screen requires a tenant Admin, and the API requires the same on every /a/robots/** route — including reading and writing the parameter configuration, which is where a secret reference comes in. | ❌ | ✅ | ❌ | ❌ |
/admin/api-keys | Programmatic API keys per tenant. | ❌ | ✅ | ❌ | ❌ |
/admin/webhooks | Configured callbacks per tenant. | ❌ | ✅ | ❌ | ❌ |
/admin/tenants/$id | Tenant member management. Super Admin only: beforeLoad redirects a Tenant Admin to the Dashboard, and every /api/admin/tenants/** endpoint requires SUPER_ADMIN. | ✅ | ❌ | ❌ | ❌ |
/mail-inboxes | The tenant's mail inboxes, under Integration, next to API Keys and Webhooks. - It holds the IMAP connection and the password reference: it is a credential, which is why it stays with the tenant Admin. - The rule list is still there, read-only, so an administrator can see what depends on that account before changing or deleting it. | ❌ | ✅ | ❌ | ❌ |
/mail-deliveries | Every message the platform read, and what it did with it. - A refused message is a diagnosis about the process that should have started, so the modeller sees it too. | ❌ | ✅ | ✅ | ❌ |
/admin/rag-config is not a frontend route
rag-config exists only as a backend endpoint (/api/admin/rag-config). Navigating to /admin/rag-config in the SPA opens nothing — RAG configuration is the second tab of /admin/ai-config. That applies to any link or document pointing there.
3. Backend Endpoints and Permissions (REST API)
The APIs in Spring Boot are divided by URL convention and security filters.
The catalogue of paths, methods and parameters is REST API; what this page adds is who may call each one and what the backend does when the caller may not.
Main Prefixes
/admin/**: Global management endpoints.- The security filter requires
SUPER_ADMIN.ADMINis a role held inside a tenant and opens nothing under/api/admin/**; each controller narrows from there, and most operations areSUPER_ADMINonly (see the administration table in REST API). - Operate with no selected tenant, cross-tenant where applicable.
- The security filter requires
/a/**: Multi-tenant operational endpoints.- Require a tenant selected in the token (
POST /api/auth/select-tenant). - The backend validates if the authenticated user has the listed tenant in their permissions.
- The prefix only guarantees an authenticated user in the tenant. The role is decided by each controller's
@PreAuthorize, and there are four tenant predicates:isCurrentTenantAdmin(ADMINonly),isCurrentTenantManager(MANAGERorADMIN),isCurrentTenantModeler(MODELERorADMIN), and the deliberate absence of an annotation, listed under Routes left open on purpose.SUPER_ADMINpasses on all of them. A new route with no role decision failsTenantEndpointDeclaresItsRoleTest.
- Require a tenant selected in the token (
Key Process Control Endpoints
There is no /admin/definitions
Deploying, versioning and publishing BPMN/CMMN live at /a/definitions/..., tenant-scoped through the token. An integration written against /admin/definitions gets a 404. The fact that definitions are global does not change the path: what is global is the published artefact, not the route.
| Endpoint | Method | Context | Access | Description |
|---|---|---|---|---|
/a/definitions | GET | Tenant | TA, Manager, User | Returns the active process catalog for the tenant. Authentication is enough. |
/a/definitions/deploy | POST | Tenant | TA, Modeler | Deploys the BPMN/CMMN file (multipart). Does not publish the version. |
/a/definitions/{key}/versions | GET | Tenant | TA, Modeler | Version history; the published one carries published: true. |
/a/definitions/{key}/versions/{definitionId}/publish | POST | Tenant | TA, Modeler | Moves the publication to the given version. |
/a/definitions/{key}/config | PUT | Tenant | TA, Modeler | Process-specific configuration in the tenant (Business Key Template and attachment policy). |
/a/definitions/{key}/attachment-policy | GET | Tenant | TA, Modeler | The process's effective attachment policy. A read made by the catalogue screen, which already requires the tenant Admin. What enforces the policy at runtime is the server (AttachmentAccessService), not the form: no runtime screen calls this route, which is why it is closed. |
/a/definitions/{key}/mcp-policy | GET | Tenant | TA, Modeler | What an assistant connected over MCP may do in the process. Same design as the attachment policy: what enforces it is ExecutionMcpTools, inside the server. |
/a/delegates | GET | Tenant | TA, Modeler | Loaded delegate beans, carrying each one's Java class name and declared parameters — an information surface about the installation. Only the modeler's selector consumes it, and the modeler already requires the tenant Admin. |
/a/delegates/active | GET | Tenant | TA | Active global delegate JARs. Already required a role before #115. |
/a/definitions/{key}/monitor | GET | Tenant | TA, Manager | Work parked per step of the process, broken down by reason. Accepts ?version=. |
/a/definitions/{key}/monitor/{activityId} | GET | Tenant | TA, Manager | What is parked at that step: instance, reason, assignee or error. Accepts ?version=. |
/a/definitions/{key}/toggle | PATCH | Tenant | TA, Modeler | Enables/Disables the process in the tenant's catalogue. |
/a/definitions/{key}/suspend | PUT | Tenant | TA, Modeler | Suspends the definition: no new instance starts. Distinct from disabling it in the catalogue. |
/a/definitions/{key}/activate | PUT | Tenant | TA, Modeler | Reactivates the suspended definition. |
/a/instances | POST | Tenant | TA, User | Starts a workflow instance on behalf of the logged-in tenant. Generates the Business Key according to fallback hierarchy. |
/a/instances/search | GET | Tenant | TA, User | Searches for instances based on variables tied to the logged-in tenant. |
/a/instances/{id} | DELETE | Tenant | TA, Manager | Cancels an instance. Destructive — not offered to an ordinary user, and refused by the backend. |
/a/instances/{id}/suspend | PATCH | Tenant | TA, Manager | Suspends the instance. |
/a/instances/{id}/activate | PATCH | Tenant | TA, Manager | Reactivates a suspended instance. |
/a/instances/failed-jobs | GET | Tenant | TA, Manager | The tenant's failed job queue. |
/a/instances/failed-jobs/{jobId}/retry | POST | Tenant | TA, Manager | Re-runs the job. |
/a/instances/failed-jobs/{jobId}/deadletter | POST | Tenant | TA, Manager | Moves the job to the deadletter. |
/a/instances/{id} | GET | Tenant | TA, User | Instance detail. An ordinary user only reaches an instance they take part in; another's answers 404, not 403 — a 403 would confirm it exists. |
/a/instances | GET | Tenant | TA, User | Paginated list of active instances. Accepts page, size (default 20, capped at 100) and type. Returns the {content, number, size, totalElements, totalPages} envelope — not an array. The page index is called number in the response and page in the request, following the Spring Data shape used by the other paginated routes. |
Who can see an instance
A tenant manager or administrator sees every instance. An ordinary user sees the instances they take part in, from three sources combined:
- the ones they started;
- the ones they take part in — a task assigned to them, or one they are a candidate for, directly or through one of their process roles;
- the ones they took part in — a task they already completed.
Deliberately the same rule the Tasks screen applies: two screens disagreeing about who is involved is worse than either rule alone.
"Sees everything in the tenant" has one definition, in PlatformRole.seesEverythingInTenant(), and it covers SUPER_ADMIN, ADMIN and MANAGER. MODELER and USER are left out — and the modeler was left out by decision, not by the ordering of the enum: they design the process, but at runtime they see what an ordinary user sees. Both screens read it through the same entry point (UnifiedTaskService.visibilityOf); neither carries a role list of its own. The caller's role is read first from the JWT authorities — the only source that carries the platform-wide SUPER_ADMIN, who has no membership in the tenant — and then from the membership's platformRole.
The filter covers the list, the detail, the diagram and the variable search — a search that saw past the list would make the filter a formality.
Attachment Endpoints
Process instance attachments (entityType = PROCESS_INSTANCE) are not governed by the platform role but by the user's role in that definition, following the policy configured at /a/definitions/{key}/config. Administrators pass every check. Other attachment types — knowledge base, process knowledge, global attachments — follow their own modules' rules and are unaffected.
| Endpoint | Method | Access | Description |
|---|---|---|---|
/a/attachments | POST | Per uploadMode + uploadRoles | Upload. Refused in FORM_ONLY for PROCESS_INSTANCE. |
/a/attachments | GET | Tenant member | Lists the entity's current attachments (superseded ones are left out). |
/a/attachments/capabilities | GET | Tenant member | What the current user can do: canUpload, canPreview, canReplace. This is what the UI asks instead of duplicating the rule. |
/a/attachments/{id} | GET | previewRoles | Attachment metadata. |
/a/attachments/{id}/download | GET | previewRoles | File contents. |
/a/attachments/{id}/replace | POST | replaceRoles | Uploads the new version and marks the previous one superseded. |
/a/attachments/{id}/history | GET | previewRoles | The entity's superseded versions. |
/a/attachments/{id} | DELETE | Nobody, for PROCESS_INSTANCE | Refused even for administrators. Still valid for the other types. |
An empty role list does not mean "open"
An empty list (previewRoles, replaceRoles, uploadRoles) means any user holding some role in that definition. Someone with no role in the process does not reach its attachments. A process with no configured policy behaves as AD_HOC under that same default rule.
A task inside the process acts as the system
The policy above answers "which person may open this file". A task running inside the process has no person: in an asynchronous service task the job executor thread carries no SecurityContext at all. That is why a process task has system access to everything inside its own tenant — the person who designs the process is the administrator, and each step inherits that authority instead of borrowing the identity of whoever started the instance.
System access requires both conditions at once: being inside an agent tool call and having a routed tenant. If the agent declares a different tenant from the one routed on the connection, access is refused and the mismatch is logged at error level.
The boundary between tenants does not depend on that check. The attachments table lives in the tenant_<slug> schema and the connection is routed by TenantAwareDataSource — an agent cannot reach another tenant's attachment even if every permission check were removed, because the connection cannot see the other schema.
Deletion stays refused for everyone, the system included: on PROCESS_INSTANCE the only write path is replacement.
CMS Endpoints
Reading is open to any tenant member; writing requires Manager or above. In both cases access still depends on the collection's confidentiality level (see section 5).
| Endpoint | Method | Access | Description |
|---|---|---|---|
/a/cms/collections | GET | SA, TA, Manager, User | Lists only the collections the caller has clearance to read. |
/a/cms/collections | POST | SA, TA, Manager | Creates a collection with its fields and confidentiality level. |
/a/cms/collections/{id} | GET | SA, TA, Manager, User | Collection detail. |
/a/cms/collections/{id} | PUT, DELETE | SA, TA, Manager | Changes the schema/confidentiality or deletes the collection. |
/a/cms/collections/{id}/records | GET | SA, TA, Manager, User | Lists records, paginated. |
/a/cms/collections/{id}/records | POST | SA, TA, Manager | Creates a record. |
/a/cms/collections/{id}/records | DELETE | SA, TA, Manager | Deletes all records of the collection (clean reload). |
/a/cms/collections/{id}/records/bulk | POST | SA, TA, Manager | Idempotent bulk load of reference tables. |
/a/cms/collections/{id}/records/{rid} | PUT, DELETE | SA, TA, Manager | Updates or deletes a record. |
Calls authenticated by API Key (X-API-Key) write to the CMS — that is how a reference table gets loaded by a script — but with clearance fixed at Public: a leaked key does not reach classified data.
The key still needs the scope for the method: read for the GETs in the table above, write for the POST, PUT and DELETE. Without it the request stops in the filter with a 403, before any role is evaluated. Detail in REST API → Scopes.
Knowledge Base Endpoints
Unlike the CMS, reading also requires Manager, Modeler or above: the listing of bases and documents shows what the AI consults, and that is curation, not content for consumption. An ordinary user gets a 403 on every line below.
| Endpoint | Method | Access | Description |
|---|---|---|---|
/a/knowledge-bases | GET | SA, TA, Manager, Modeler | Lists the tenant's bases and the global ones visible to it. |
/a/knowledge-bases | POST | SA, TA, Manager, Modeler | Creates a base. Flagging isGlobal requires Super Admin — the service refuses anyone else. |
/a/knowledge-bases/{id} | GET | SA, TA, Manager, Modeler | Base details with the document count. |
/a/knowledge-bases/{id} | DELETE | SA, TA, Manager, Modeler | Deletes the base. Refused while a process is still linked. A global base only by the Super Admin. |
/a/knowledge-bases/{id}/documents | GET | SA, TA, Manager, Modeler | Lists the documents, paginated. |
/a/knowledge-bases/{id}/documents | POST | SA, TA, Manager, Modeler | Uploads PDF, TXT or MD; indexing runs in the background. |
/a/knowledge-bases/{id}/documents/{docId} | DELETE | SA, TA, Manager, Modeler | Removes the document and its vectors. |
/a/knowledge-bases/process/{key} | GET | SA, TA, Manager, Modeler | Bases linked to that process. |
/a/knowledge-bases/process/{key}/link/{id} | POST | SA, TA, Manager, Modeler | Links the base to the process. |
/a/knowledge-bases/process/{key}/unlink/{id} | DELETE | SA, TA, Manager, Modeler | Undoes the link. |
The per-process knowledge base — the one the catalogue's RAG tab feeds — is the same capability reached by another route, so it requires the same role. Closing /a/knowledge-bases without closing these would leave an ordinary user wiping here the collection they cannot touch there.
| Endpoint | Method | Access | Description |
|---|---|---|---|
/a/definitions/{key}/knowledge/documents | GET | SA, TA, Manager, Modeler | Lists the documents indexed for that process. |
/a/definitions/{key}/knowledge/documents | POST | SA, TA, Manager, Modeler | Uploads a document and triggers indexing. |
/a/definitions/{key}/knowledge/documents/{docId} | DELETE | SA, TA, Manager, Modeler | Removes the document and its vectors. |
/a/definitions/{key}/knowledge/count | GET | SA, TA, Manager, Modeler | Number of chunks indexed for the process. |
/a/definitions/{key}/knowledge | DELETE | SA, TA, Manager, Modeler | Wipes all vectors of the process. Destructive. |
/a/definitions/{key}/knowledge/reindex | POST | SA, TA, Manager, Modeler | Clears and reindexes every attached file. |
An API key does not reach these routes: it carries no tenant membership, and the role check requires one.
Document Type Endpoints
A document type is referenced by the form's File field, but whoever fills the form never calls these routes: the documentTypeId is already stored in the published schema and goes straight to /a/attachments. That is why reading stays at Manager or Modeler — it is the form builder's selector — while maintaining the catalogue is the Modeler's, who is the one designing the form that points at the type.
| Endpoint | Method | Access | Description |
|---|---|---|---|
/a/document-types | GET | SA, TA, Manager, Modeler | Lists the tenant's types, or a process's global ones with ?isGlobal=true&processKey=. Reading the global ones stays open to the tenant: they are the types of the process it subscribes to. |
/a/document-types | POST | SA, TA, Modeler | Creates a tenant type. With ?isGlobal=true, SA only. |
/a/document-types/{id} | PUT | SA, TA, Modeler | Changes name, description or status. If the type is global, SA only. |
/a/document-types/{id} | DELETE | SA, TA, Modeler | Deletes the type. A published form pointing at it loses the reference. If the type is global, SA only. |
The global layer is not written by a tenant Admin
isGlobal=true writes into the shared layer: the type comes to hold for every tenant that subscribes to the process. The tab that authors those types lives under Global Templates, which in the UI is Super Admin, yet the API accepted the flag from any tenant Admin — and a tenant Admin only answers for its own tenant. POST ?isGlobal=true, and the PUT/DELETE of a type that is already global, now answer 403 to anyone who is not a Super Admin. This is the same design that already protected the global knowledge base (KnowledgeBaseService.createKnowledgeBase) and the global agent (AiAgentService.resolveTenant), and all three cases live in the same test, GlobalLayerIsNotOpenTest.
Robot Endpoints
A robot is code the tenant publishes and the platform runs with the tenant's credentials. That is why every /a/robots/** route requires a tenant Admin — registration, JAR upload, execution, state, logs, and also the parameter configuration, which is where the secret comes in: a parameter marked secret holds a reference (${var.erpToken}) to the tenant variable that carries the token. Reading the configuration already names the declared parameters and shows where each reference points, so read and write sit at the same role — Manager and ordinary user get 403 on both. The /robots/$key screen, the only consumer of these routes, already required a tenant Admin in its beforeLoad; it was the route that stood open.
| Endpoint | Method | Context | Access | Description |
|---|---|---|---|---|
/a/robots/{key}/config | GET | Tenant | SA, TA | The robot's parameter configuration in that tenant, together with the parameters declared by the JAR. |
/a/robots/{key}/config | PUT | Tenant | SA, TA | Writes the parameter configuration. This is where a secret parameter's reference comes in. |
Pinned by RobotConfigAuthorizationTest.
Conducting a Case (CMMN)
The case plan is conducted by whoever works on the case, and closing the stage is the only management action.
| Endpoint | Method | Context | Access | Description |
|---|---|---|---|---|
/a/instances/{id}/plan-items | GET | Tenant | TA, Manager, User | The case's live steps, each with the actions it accepts. COMPLETE_STAGE is offered only to whoever manages the tenant. |
/a/instances/{id}/milestones | GET | Tenant | TA, Manager, User | Milestones the case has already reached. |
/a/instances/{id}/plan-items/{planItemId}/start | POST | Tenant | TA, Manager, User | Starts the offered step. |
/a/instances/{id}/plan-items/{planItemId}/disable | POST | Tenant | TA, Manager, User | Discards the optional step. |
/a/instances/{id}/plan-items/{planItemId}/enable | POST | Tenant | TA, Manager, User | Offers a discarded step again. |
/a/instances/{id}/plan-items/{planItemId}/trigger | POST | Tenant | TA, Manager, User | Fires a waiting user event. |
/a/instances/{id}/plan-items/{planItemId}/complete-stage | POST | Tenant | SA, TA, Manager | Closes the stage, ending the optional steps still offered inside it. An ordinary user gets 403. |
The first six rows are not "open to any authenticated user": all of them go through WorkflowInstanceService.requireVisible, the same gate as the instance detail, and a case the person does not take part in answers 404. Pinned by CasePlanItemControllerAuthorizationTest and CasePlanItemServiceTest.
Modeler AI Endpoints
| Endpoint | Method | Context | Access | Description |
|---|---|---|---|---|
/a/ai-validation/bpmn | POST | Tenant | SA, TA, Modeler | Semantic BPMN validation by LLM. Manager and ordinary user get 403: the screens that trigger the call (/definitions, /definitions/{key}, /admin/global-templates/{key}) belong to designing the process, and every call spends the tenant's token quota. An exhausted or never-configured quota answers 402, not a validation opinion. |
/a/modeler/copilot/stream | POST | Tenant | SA, TA, Modeler | Modeler copilot over SSE. The row written to ai_usage_logs carries the user_id of whoever drove the turn, on top of the tenant and the turn's executionCorrelationId — without it governance would know the tenant spent, but not who. |
Routes Left Open on Purpose
Under /a/**, a missing @PreAuthorize means "any authenticated user of the tenant". On the four rows below that is a decision, not an oversight — closing them would break a screen an ordinary user needs. Each one has its limit next to its promise, and each limit has a test that pins it down.
| Endpoint | Method | Why it stays open | Where the limit is |
|---|---|---|---|
/a/attachments, /a/attachments/{id}/replace, /a/attachments/{id} | POST, DELETE | An ordinary user attaches a file through their own task form — the entityType=process path, which is ungoverned. Requiring Admin on the route would take attachments out of the process. | Inside the service, by process role: AttachmentAccessService.requireUpload/requireReplace/requireDeletable, following the process policy's uploadRoles/replaceRoles (see Attachment Endpoints above). Pinned by AttachmentControllerAuthorizationTest and AttachmentAccessServiceTest. |
/a/notifications, /a/notifications/{id}/read, /a/notifications/read-all | GET, PUT | Every user has an inbox; no role applies. | The userId comes from the token, never from the body or the URL. NotificationService.markRead refuses when the notification belongs to someone else, and markAllRead is an UPDATE ... WHERE user_id = :userId. Passing someone else's notification id answers 403 and writes nothing — an access refusal, not a malformed request, so the caller can tell "no such id" (404) from "not yours" (403). Pinned by NotificationControllerAuthorizationTest and NotificationServiceTest. |
/a/dashboard | GET | It is the landing screen of any tenant member. | The tenant and the viewer's visibility. Active tasks and finished processes go through UnifiedTaskService.visibilityOf — the same entry point Tasks and Instances read: whoever sees the whole tenant reads the total, everyone else reads only what they take part in. The definition counter stays tenant-wide, because starting a process remains open to an ordinary user. Pinned by DashboardFollowsInstanceVisibilityTest and DashboardCountsOnPostgresTest. |
/a/instances/{id}/plan-items/{planItemId}/start, /disable, /enable, /trigger | POST | Conducting your own step is the work of whoever works on the case, and the case screen carries no role beforeLoad. Requiring Manager would take away from an ordinary user the case they started themselves. | CasePlanItemService calls WorkflowInstanceService.requireVisible before any transition: a case the person does not take part in answers 404. The engine refuses a transition the state does not allow, and availableActions hides only COMPLETE_STAGE. Pinned by CasePlanItemControllerAuthorizationTest and CasePlanItemServiceTest. |
The dashboard card counts what the viewer can see
GET /a/dashboard no longer returns the same numbers to everyone. Active tasks and finished processes are counted by the same rule the listing applies: the service reads UnifiedTaskService.visibilityOf(tenant, user) — the one place where "sees everything in the tenant" is written, and the same one InstanceVisibilityService consults. Manager, Admin and Super Admin still read the tenant total; an ordinary user counts only the tasks they are assignee or candidate for (directly or through a process-role group), and only the finished processes they held a task in or started.
The scope is applied as a predicate of the count itself, never by materialising the list of visible ids: DashboardCountRepository keeps the promise of two queries per load, however many instances the tenant holds. The definitions counter stays tenant-wide on purpose — it is the catalog of what you may start, and POST /a/instances remains open to an ordinary user.
4. Hierarchy Resolution for Business Keys (Note for AI)
The backend resolves initialization variables like the Business Key in a hierarchical order (Implemented in WorkflowDefinitionService):
- Explicit key in the request: a
businessKeysent toPOST /a/instanceswins over the template. The template is a default for callers that do not know the key, not an override of callers that do. A blank or absent value falls through to the template. - Tenant template (
PublishedDefinition.businessKeyTemplate): the Tenant Admin writes it through/a/definitions/{key}/config. - System Default: hardcoded fallback to
DOC-${date:yyyyMMdd}-${random:4}(WorkflowDefinitionService.DEFAULT_BUSINESS_KEY_TEMPLATE).
Those two steps are all there is. There is no global Business Key layer: GlobalProcessTemplate has no template field, and resolveBusinessKeyTemplate goes straight from a blank tenant template to the system default.
The template accepts ${name:-fallback}, which resolves to the process variable name when it is present and non-blank, and to the fallback otherwise. The fallback may itself contain a placeholder, so one template serves both an integration that knows the data and a start form that does not:
${tipoDoc:-DOC}-${emissao:-${date:yyyyMMdd}}-${cnpjEmissor:-${random:4}}
API, variables present -> NFSE-20260305-22060673000170
UI, variables missing -> DOC-20260807-9O63A placeholder with no fallback and no matching variable refuses the start. The request answers 422 Unprocessable Entity, and the instance is never created — no task, no history, nothing to cancel later. The error body names every missing variable under fieldErrors, so the client knows exactly what to send on the second attempt:
{
"status": 422,
"error": "Unprocessable Entity",
"fieldErrors": [
{ "field": "cnpjEmissor", "message": "variável exigida pelo businessKeyTemplate e não informada" }
]
}The same refusal applies when substitution empties the key entirely — a template that would resolve to NFSE--, all separators and no content, is rejected rather than stored. The business key is how the document is found later; a truncated key creates a record nobody can locate, and the 422 trades that silent problem for an error at the moment it happens.
If you need the process to start without every variable, there are two ways out: give the placeholder a fallback (${cnpjEmissor:-${random:4}}), or send an explicit businessKey in the body of POST /a/instances, which wins over the template.
5. CMS Data Confidentiality
Every CMS collection carries a confidentiality level. The level required to read or write a collection derives from the user's role in the tenant — there is no per-user grant.
| Level | Minimum role | Typical content |
|---|---|---|
PUBLIC | Tenant User | NCM, CFOP, CEST, cost centres, product catalog |
SECRET | Tenant Manager | Margins, commercial policy, supplier price lists |
TOP_SECRET | Tenant Admin | Payroll, legal, board material |
Derived rules:
USER → PUBLIC,MODELER → PUBLIC,MANAGER → SECRET,ADMIN → TOP_SECRET,SUPER_ADMIN → TOP_SECRET. The modeler stays atPUBLICon purpose: the CMS is tenant content, not process design, andCmsControllerrequires a Manager — giving them any other level would be a promise no route keeps.- Collection listing omits what the caller cannot read; direct access answers
403. - A manager cannot classify a collection as
TOP_SECRET— that would let them create data beyond their own reach. - AI agents: the
queryCollectionToolrespects the level configured on each agent (cmsClearance, defaultPUBLIC). BPMN processes have no logged-in user to inherit permission from, so the level is an explicit setting on the agent. In the playground the lower of the user's level and the agent's applies (AiPlaygroundControllercallsCmsClassification.lowest). The execution copilot does not do that arithmetic:AiExecutionCopilotControllerusescmsAccessService.currentClearance()alone, so there the ceiling is the user's role, not the agent's. - Robots (
RobotCmsApi) are not bounded by confidentiality: they are code published by tenant administrators and reach any collection in it.
Implementation reference: security/CmsAccessService.java, security/TenantSecurityService.java and the spec specs/content/cms.md.