Saltar al contenido principal

Users & RBAC

Every tenant has its own user table, completely isolated from other tenants. Users authenticate with email + password and get a JWT. Roles control what they can do.

System Roles​

Four built-in roles ship with every tenant. You cannot modify or delete them.

RoleWhat it can do
ownerEverything. Manage entities, users, roles, settings, records. Full control.
adminManage entities, users, and settings. CRUD all records across all entities.
memberCRUD records on entities they have permission for.
viewerRead-only on entities they have permission for.

When you create a user without specifying a role, they get member by default.

Custom Roles​

System roles are coarse. Custom roles let you define exactly what a user can do, down to per-entity CRUD operations.

Creating a Role​

MCP:

create_role({
name: "support-agent",
permissions: {
entities: {
"tickets": ["create", "read", "update"],
"customers": ["read"],
"internal-notes": ["create", "read", "update", "delete"]
},
canManageUsers: false,
canManageRoles: false,
canManageSettings: false
}
})

REST:

curl -X POST https://api.fyso.dev/api/roles \
-H "Authorization: Bearer <admin-token>" \
-H "Content-Type: application/json" \
-d '{
"name": "support-agent",
"permissions": {
"entities": {
"tickets": ["create", "read", "update"],
"customers": ["read"]
},
"canManageUsers": false,
"canManageRoles": false,
"canManageSettings": false
}
}'

Permission Structure​

{
"entities": {
"<entity-name>": ["create", "read", "update", "delete"]
},
"canManageUsers": false,
"canManageRoles": false,
"canManageSettings": false
}

Valid actions per entity: create, read, update, delete. That's it -- no wildcards in custom roles. You must list each entity explicitly.

The management flags:

FlagWhat it grants
canManageUsersCreate, list, update users in the tenant
canManageRolesCreate, update, delete custom roles; assign/revoke roles
canManageSettingsUpdate tenant settings

Multiple Roles per User​

Users can have multiple roles. Permissions merge with union semantics -- if any role grants an action, the user has it.

Example: a user with roles support-agent (read tickets) and billing-viewer (read invoices) can read both tickets and invoices.

Assigning a Role​

curl -X POST https://api.fyso.dev/api/roles/users/<user-id>/roles \
-H "Authorization: Bearer <admin-token>" \
-H "Content-Type: application/json" \
-d '{"roleId": "<role-id>"}'

Assigning the same role twice is idempotent -- no error, no duplicate.

MCP:

assign_role({
userId: "uuid-of-user",
roleId: "uuid-of-role"
})

Revoking a Role​

curl -X DELETE https://api.fyso.dev/api/roles/users/<user-id>/roles/<role-id> \
-H "Authorization: Bearer <admin-token>"

MCP:

revoke_role({
userId: "uuid-of-user",
roleId: "uuid-of-role"
})

Checking Effective Permissions​

To see the merged result of all roles assigned to a user:

curl https://api.fyso.dev/api/roles/users/<user-id>/permissions \
-H "Authorization: Bearer <admin-token>"

Response:

{
"success": true,
"data": {
"entities": {
"tickets": ["create", "read", "update"],
"customers": ["read"]
},
"canManageUsers": false,
"canManageRoles": false,
"canManageSettings": false
}
}

Managing Roles​

OperationRESTRequires
List all rolesGET /api/rolesAuth
Get role by IDGET /api/roles/:roleIdAuth
Create rolePOST /api/rolescanManageRoles
Update rolePUT /api/roles/:roleIdcanManageRoles
Delete roleDELETE /api/roles/:roleIdcanManageRoles

System roles (isSystem: true) cannot be modified or deleted.

Entity-Level Permissions​

Each entity in a role's permission object maps to an array of allowed actions:

{
"entities": {
"products": ["create", "read", "update", "delete"],
"invoices": ["read", "create"],
"reports": ["read"]
}
}

No wildcard entity ("*") support in custom roles. You list each entity by name.

You can also set per-entity permissions directly on a user (legacy approach, still works):

create_user({
tenantSlug: "my-app",
email: "[email protected]",
password: "securepass123",
name: "Warehouse Operator",
role: "member",
permissions: {
entities: {
inventory: ["create", "read", "update"],
shipments: ["read"]
}
}
})

Legacy user-level permissions merge with role-based permissions using the same union semantics.

Advanced: Field-Level Control​

Custom roles support field-level restrictions on reads:

{
"entities": {
"employees": {
"actions": ["read"],
"fields": ["name", "department", "title"],
"excludeFields": ["salary", "ssn"]
}
}
}
PropertyTypeDescription
actionsstring[]CRUD actions allowed
fieldsstring[]Whitelist -- only these fields are returned. Omit for all fields.
excludeFieldsstring[]Blacklist -- these fields are stripped from responses.
rowFilterstring or objectFilter records (see next section).

If one role has a field whitelist and another doesn't, the user sees all fields (union semantics).

Row-Level Filtering​

Restrict which records a user can see by attaching a rowFilter to their role's entity permission.

Via Entity Views​

The cleanest approach: create a view with a filter, then grant role access to the view.

Step 1: Create the view

create_view({
entitySlug: "tickets",
slug: "my-tickets",
name: "My Tickets",
filterDsl: {
validate: [
{ condition: "reporter == $currentUser" }
]
}
})

Step 2: Grant the role access to the view

{
"entities": {
"view:my-tickets": ["read"]
}
}

Now users with that role can only query GET /api/views/my-tickets/records and they only see tickets where reporter matches their user ID.

Filter Variables​

VariableResolves to
$currentUserThe authenticated user's ID
$currentTenantThe current tenant's ID

Admin API key access bypasses view filters entirely -- admins see all records.

Filter Syntax​

Same syntax as business rule conditions:

status == 'open' and priority > 3
(department == 'sales' or department == 'support') and region == $currentTenant
owner_id == $currentUser

Operators: ==, !=, >, >=, <, <=. Logical: and, or, not. Grouping with parentheses.

Creating Users​

MCP:

create_user({
tenantSlug: "my-app",
email: "[email protected]",
password: "securepass123",
name: "Support Agent",
role: "member",
permissions: {
entities: {
tickets: ["create", "read", "update"],
customers: ["read"]
}
},
metadata: {
department: "Support",
phone: "+1-555-0123"
}
})

REST:

curl -X POST https://api.fyso.dev/api/auth/tenant/users \
-H "Authorization: Bearer <admin-token>" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"password": "securepass123",
"name": "Support Agent",
"role": "member",
"permissions": {
"entities": {
"tickets": ["create", "read", "update"]
}
}
}'
ParameterTypeRequiredDescription
emailstringYesUnique within the tenant
passwordstringYesMinimum 8 characters, stored hashed
namestringYesFull name
rolestringNoowner, admin, member, viewer. Default: member
permissionsobjectNoPer-entity permissions (legacy, works alongside roles)
metadataobjectNoArbitrary data: phone, department, avatar, etc.

After creation, assign custom roles separately via POST /api/roles/users/:userId/roles.

User Login & Sessions​

MCP:

tenant_login({
tenantSlug: "my-app",
email: "[email protected]",
password: "securepass123"
})

REST:

curl -X POST https://api.fyso.dev/api/auth/tenant/login \
-H "Content-Type: application/json" \
-H "X-Tenant-ID: my-app" \
-d '{"email":"[email protected]","password":"securepass123"}'

Response:

{
"success": true,
"token": "eyJhbGci...",
"user": {
"id": "uuid",
"email": "[email protected]",
"name": "Support Agent",
"role": "member"
},
"usage": {
"header": "Authorization",
"value": "Bearer eyJhbGci...",
"note": "Use this token in the Authorization header for REST API calls"
}
}

The JWT goes in the Authorization: Bearer <token> header for all subsequent REST calls.

Self-Service Flows​

These are disabled by default. Enable per tenant:

curl -X PUT https://api.fyso.dev/api/auth/tenants/<tenant-id>/settings \
-H "Authorization: Bearer <admin-token>" \
-H "Content-Type: application/json" \
-d '{"selfRegistrationEnabled": true, "passwordResetEnabled": true}'
FeatureEndpointNotes
Self-registrationPOST /api/auth/tenant/registerCreates user with viewer role. Rate-limited: 5/hour per IP.
Forgot passwordPOST /api/auth/tenant/forgot-passwordSends reset email. Never reveals if email exists. 3 per 15min.
Reset passwordPOST /api/auth/tenant/reset-passwordOne-time token, expires 1 hour. Invalidates all sessions.
Change passwordPOST /api/auth/tenant/change-passwordAuthenticated. Validates current password first.

Admin can also force-reset any user's password:

curl -X PATCH https://api.fyso.dev/api/auth/tenant/users/<user-id>/reset-password \
-H "Authorization: Bearer <admin-token>" \
-H "Content-Type: application/json" \
-d '{"new_password": "newpassword123"}'

Invitation Flows​

Two systems: invitation codes (gate registration) and tenant member invitations (invite specific people).

Invitation Codes​

Codes control who can register when self-registration is disabled. Can be single-use or multi-use.

Create a code:

curl -X POST https://api.fyso.dev/api/invitations \
-H "Authorization: Bearer <admin-token>" \
-H "Content-Type: application/json" \
-d '{"note": "for contractor team", "maxUses": 5, "expiresAt": "2026-06-30T00:00:00Z"}'

Response:

{
"success": true,
"data": {
"token": "FYSO-A3B4-C5D6",
"inviteUrl": "https://app.example.com/invite/FYSO-A3B4-C5D6"
}
}

Validate a code (public, no auth):

curl -X POST https://api.fyso.dev/api/invitations/validate \
-H "Content-Type: application/json" \
-d '{"code": "FYSO-A3B4-C5D6"}'

Invalidate a code:

curl -X DELETE https://api.fyso.dev/api/invitations/FYSO-A3B4-C5D6 \
-H "Authorization: Bearer <admin-token>"

Tenant Member Invitations​

Invite specific people by email. Each invitation is single-use with a configurable TTL.

Create an invitation:

curl -X POST https://api.fyso.dev/api/invitations \
-H "Authorization: Bearer <admin-token>" \
-H "X-Tenant-Slug: my-app" \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "expiresInDays": 7}'

Accept an invitation (public, no auth required):

curl -X POST https://api.fyso.dev/auth/invite/accept \
-H "Content-Type: application/json" \
-d '{
"token": "<invite-token>",
"email": "[email protected]",
"name": "Jane Doe",
"password": "securepassword"
}'

The email must match the invitation if one was set. Creates the user as member. The claim is atomic -- no double-use possible.

Invitation statuses: pending, accepted, expired, revoked.

Revoke:

curl -X DELETE https://api.fyso.dev/api/invitations/<token> \
-H "Authorization: Bearer <admin-token>" \
-H "X-Tenant-Slug: my-app"

View-Based Access Control​

Views are the recommended way to implement row-level security. The pattern:

  1. Create an entity view with a filter
  2. Create a custom role with view:<slug> permission
  3. Assign the role to users

Full Example​

Create a view that shows only records owned by the current user:

curl -X POST https://api.fyso.dev/api/views \
-H "Authorization: Bearer <admin-token>" \
-H "Content-Type: application/json" \
-d '{
"entitySlug": "orders",
"slug": "my-orders",
"name": "My Orders",
"filterDsl": {
"validate": [{"condition": "owner_id == $currentUser"}]
}
}'

Create a role that can only access records through this view:

curl -X POST https://api.fyso.dev/api/roles \
-H "Authorization: Bearer <admin-token>" \
-H "Content-Type: application/json" \
-d '{
"name": "order-viewer",
"permissions": {
"entities": {
"view:my-orders": ["read"]
}
}
}'

Assign the role to a user:

curl -X POST https://api.fyso.dev/api/roles/users/<user-id>/roles \
-H "Authorization: Bearer <admin-token>" \
-H "Content-Type: application/json" \
-d '{"roleId": "<order-viewer-role-id>"}'

Now the user queries records through the view:

curl https://api.fyso.dev/api/views/my-orders/records \
-H "Authorization: Bearer <user-token>"

They only see their own orders. Users without the view:my-orders permission get 403.

View Query Parameters​

Views support the same query parameters as entity records:

ParameterTypeDefaultDescription
pagenumber1Page number
limitnumber20Items per page
sortstring-Sort field
orderstringascasc or desc
searchstring-Full-text search
filter.{field}string-Additional filter (AND with view filter)

Additional query filters compose with the view's base filter using AND logic.