Technical
Service follows vyndara base structures: tenant based requests, app based requests
The permission server is a service that handles the permissions for the Vyndara Platform. It is responsible for storing roles and rights and mapping them to roles.
Server
The permission server acts as a central authority for managing and validating action-based rights across all Vyndara services. It exposes a lightweight gRPC interface that allows other services to validate whether a token has the rights to execute a specific action, without each service needing to replicate permission data or logic.
The system is designed for maximum performance and security:
- All requests must include a valid authorization token, which is introspected and validated by the auth system.
- The permission server validates token-based requests against stored roles and action permissions, using a deny-by-default principle.
- The typical request payload includes a list of requested actions, each optionally containing a
contextobject for condition validation. Each action check triggers a hash lookup likeentity.invoice.write, but the server also evaluates fallback keys likeentity.write. Conditions on rights are dynamically evaluated against the provided metadata. - Rights are defined based on
ScopeType,Scope, andAction, optionally withConditionsfor fine-grained control. WhileConditionsare not passed as part of the permission request, the request must providecontext(e.g., entity data, attributes, or field values) required for evaluating condition expressions against the user’s permissions. The server uses this metadata to evaluate whether the condition expressions tied to each permission resolve to true. - The permission system does not support updates to existing rights other than their name (display). This prevents scenarios where users gain unintended rights due to later changes.
To reduce latency, the server may implement internal caching and access hashing logic per tenant. Rights are never cached in external systems; instead, all access checks are performed live against the permission server's in-memory or local store. This avoids the need for external synchronization or Pub/Sub.
Scope Registration
The permission server supports a flexible scope registration system, allowing services to register their own scope types
and actions.
This enables dynamic permission management without hardcoding specific rights into the server. For that the server
provides a registerRightDefinition endpoint that allows services to register their own right structures, including
scope types and actions.
A right definition request looks like this:
{
"service_version": "1.0.0",
"rights": [
{
"scope_type": "entity",
"actions": [
'read',
'write',
'delete'
]
}
]
}
Those rights are then stored in the authorization server cache only and are not stored in the database. Overall right validation is done against the registered rights in the cache and not against the database. This allows for fast validation of rights without the need for database lookups and the database stores rights without knowing the registered rights. This is helpful for the support of versioned rights, where the rights can change over time.
The service version is always the env variable SERVICE_VERSION that is set in the service.
To inform tenants about rights that are not used by the system, the permission server at first place logs information about the access validation requests and which rights are requested. If a right is not registered anymore the last used time will not increase, which indicates that the right is no longer used. If a right is not used for a longer time, tenant can set a value between 7 - 90 days while default is 30 days, it is marked as deprecated. Also a tenant can enable auto remove so the right is removed from the database and any association of it with roles.
Virtual Users
The server stores information about virtual users in a simple way.
when a request has a virtual user key it checks against this key and sends it to the authorization server via grpc. The authorization server then unpacks the virtual user and checks his rights as usual as it does it for JWT's.
When a system wants to use the the virtual user it needs to follow the basic auth flow where the using systems needs to request a JWT from the authorization server the request for that needs to contain the id and the key of the user optional it needs to have a allowed ip/mac address from the requesting system.
The jwt response directly contains a right claim with all rights from the virtual user or its roles that do not have a condition so its faster to authorize it for these rights, all rights with conditions still need to be checked against the authorization server.
The authorization server provides a public rolling key that can be used to verify generated JWT tokens. This key can be used to check the JWT on using systems and verify its integrity.
Client
There are two types of clients that interact with the permission server: a backend client and a frontend client. The backend client is used by services to validate permissions for actions or register stuff, while the frontend client is used by the user interface to check permissions for UI elements.
Backend Client
The permission client is a lightweight SDK available to all services. Instead of replicating the full permission logic, clients only need to send gRPC requests to the centralized permission service to verify access for specific actions.
The client does not perform any permission evaluation itself. For every access check, it delegates validation to the
permission server, which is responsible for evaluating the complete permission logic—including fallback keys and
condition expressions. When a client requests to check permission for a specific action (such asentity.invoice.write),
the server will automatically look for broader matching permissions (like entity.write orentity.read if implicit
read is allowed). If the permission being checked includes conditions, the client must supply the appropriate context
so the server can evaluate these conditions in real time. The client itself does not cache, sync, or store any
permission data, ensuring that all decisions are made against the latest server-side configuration.
Key features of the client:
- Stateless validation: The client performs no permission evaluation itself but delegates all checks to the central permission server via gRPC.
- Multi-action validation: Clients can batch check multiple actions in one call using the
hasPermissionsendpoint, sending the action list (with optional per-actioncontext), and authenticating via Authorization header. - Fallback logic: Clients should expect the server to automatically evaluate broader permissions (e.g.,
entity.writeincludesentity.invoice.write) and implicit read access for higher privileges (e.g.,writeimpliesread). - Condition support: If a permission includes conditions, the client must provide
contextfor the permission server to evaluate these conditions. This metadata must contain the values (e.g., field contents, roles, user attributes) needed by any condition expression. - No caching or syncing: Clients do not maintain any local permission cache or use pub/sub synchronization. All decisions are made live against the permission server to ensure up-to-date evaluations.
This architecture ensures stateless validation with centralized authority, reducing synchronization complexity while maintaining high availability and strict access control.
Note: The JWT token is sent via the
Authorizationheader and is not included in the request body.
Example Request Payload:
{
"actions": [
{
"scope_type": "entity",
"scope": "invoice",
"action": "write",
"context": {
"user_id": "abc123",
"invoice_status": "draft",
"fields": {
"total_amount": 1200
}
}
},
{
"scope_type": "app",
"scope": "crm",
"action": "read",
"context": {
"user_id": "abc123"
}
}
]
}
Frontend Client
The frontend client is a lightweight SDK that allows the user interface to check permissions for UI elements. It provides a simple API to determine whether a user has the necessary rights to perform actions or view specific components.
It loads all rights from the permission server and caches them in memory on the nuxt proxy server to provide fast access to the rights since each tenant has its own frontend pod.
A response from the permission server might look like this:
{
"permissions": [
{
"scope_type": "entity",
"scope": "invoice",
"action": "write",
"roles": [
"admin",
"editor"
],
"conditions": []
},
{
"scope_type": "app",
"scope": "crm",
"action": "read",
"roles": [
"admin",
"user"
],
"conditions": []
}
]
}
the client then checks if the user has the required rights for the requested action and returns a boolean value with
the function hasPermission:
const hasPermission = await permissionClient.hasPermission({
permission: 'entity.invoice.write',
context: {
user_id: 'abc123',
invoice_status: 'draft',
fields: {total_amount: 1200}
}
});