Users, roles and tenants
This chapter explains how people and programs sign in to LiteBot, what each role may do, and how tenants keep one customer's bots and data apart from another's. Read it before you create the first real user, before you hand out an API key, and before you host bots for more than one customer on one server.
How access works
| Who | Signs in with | Sent as |
|---|---|---|
| A person using the dashboard | Username and password, exchanged for a JWT | Authorization: Bearer <token> |
| A program (CRM, gateway, script) | An API key that starts with lbk_ |
X-API-Key: lbk_... |
A browser opening /voice |
The same JWT or API key | ?token=<token> or ?api_key=lbk_... in the URL |
Every user and every API key has exactly one role and belongs to exactly one tenant. The role says what it may do; the tenant says which bots and data it may do it to.
Some endpoints need no sign-in at all: POST /api/chat, the /chat WebSocket and /health. See Security.
Logging in
POST /api/auth/login takes a username (3 to 50 characters) and a password (at least 6). Usernames are matched in lower case.
curl -s -X POST http://127.0.0.1:3345/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"ops","password":"<password>"}'
{
"success": true,
"data": {
"user": { "id": "6717...", "username": "ops", "role": "tenant-admin", "tenantId": "acme",
"permissions": ["bots:create", "bots:read", "..."] },
"token": "<token>",
"refreshToken": "<refresh-token>",
"expiresIn": "24h"
}
}
A wrong password, an unknown user and a deactivated user all get 401 Invalid credentials.
| Token | Lifetime | Use |
|---|---|---|
Access token (token) |
JWT_EXPIRES_IN, default 24h |
Every authenticated request |
| Refresh token | 7 days, fixed | POST /api/auth/refresh with {"refreshToken":"..."} returns a new pair |
On refresh, LiteBot re-reads the user's role, tenant and status from the database, so a role change, a tenant move or a deactivation takes effect at the next refresh. An access token already issued stays valid until it expires: to lock a user out at once, deactivate them and keep JWT_EXPIRES_IN short.
GET /api/auth/me returns the caller's id, username, role, tenant, permissions and authMethod (jwt or api-key).
Both tokens are signed with JWT_SECRET. Changing it logs everyone out; see JWT_SECRET.
The bootstrap admin
On a new install there are no users in MongoDB. You sign in the first time with an admin defined in .env:
| Variable | Default | Meaning |
|---|---|---|
ADMIN_USERNAME |
admin |
Username of the bootstrap admin |
ADMIN_PASSWORD_HASH |
unset | scrypt hash of its password, as hex salt:hash. Unset disables the bootstrap login |
The hash uses scrypt with N=16384, r=8, p=1, a 32-byte salt and a 64-byte key. Generate one with Python 3:
python3 -c "import hashlib,os,getpass;s=os.urandom(32);print(s.hex()+':'+hashlib.scrypt(getpass.getpass().encode(),salt=s,n=16384,r=8,p=1,dklen=64).hex())"
Paste the output into .env and restart. The bootstrap admin has role admin and no tenant.
The bootstrap login is used only while no database user has the same username. Once you create a database user called admin, the .env entry stops working for login. Create a named database admin for each operator, then clear ADMIN_PASSWORD_HASH.
Built-in roles
LiteBot seeds three system roles at every start-up and resets their permissions to the shipped list. You cannot edit or delete them.
| Role | Scope | Permissions |
|---|---|---|
admin |
Global: every tenant | Everything. The server skips permission checks for this role |
tenant-admin |
Own tenant | create, read, update, delete on bots, conversations, api-keys, documents, collections, users, tools, campaigns; plus roles:read |
user |
Own tenant | bots:read, documents:read, collections:read |
A permission is <resource>:<action>. The resources are bots, documents, collections, admin, api-keys, roles, conversations, campaigns, users and tools; the actions are create, read, update and delete. GET /api/auth/roles/list/permissions returns the full list.
Some things only the admin role can do, whatever permissions another role holds:
- create tenants and change a tenant's shared tools;
- create, change and delete roles;
- everything under
/api/admin(providers, hot swap, config files) and/health/detail; - reload tools and bots from disk, write disk tools, run the provider health check;
- change the dialer's default endpoint and the voice catalog;
- give a user or an API key the
adminrole; - move a bot to another tenant.
Custom roles
A global admin can create roles that grant any mix of the permissions above. Custom roles are global: one role list serves all tenants.
curl -s -X POST http://127.0.0.1:3345/api/auth/roles \
-H 'Authorization: Bearer <token>' -H 'Content-Type: application/json' \
-d '{"name":"analyst","description":"Read bots and conversations","permissions":["bots:read","conversations:read"]}'
| Method and path | Needs | Notes |
|---|---|---|
GET /api/auth/roles |
roles:read |
All roles with their permissions |
GET /api/auth/roles/:id |
roles:read |
:id is the role's database id |
POST /api/auth/roles |
admin |
Name: 2 to 50 letters, digits, -, _. An unknown permission gets 400; a taken name gets 409 |
PUT /api/auth/roles/:id |
admin |
Change description or permissions. A system role gets 403 |
DELETE /api/auth/roles/:id |
admin |
409 while any user or unrevoked API key has the role |
Role permissions are cached for ROLE_CACHE_TTL seconds (default 30). A change can take that long to reach users who are signed in.
Users
| Method and path | Needs | Notes |
|---|---|---|
GET /api/users |
users:read |
A tenant caller sees only its own tenant, and never admin users |
GET /api/users/:id |
users:read |
|
POST /api/users |
users:create |
See below |
PUT /api/users/:id |
users:update |
role, email, displayName, active, tenantId |
PUT /api/users/:id/password |
users:update |
{"password":"..."}, 6 to 100 characters |
DELETE /api/users/:id |
users:delete |
curl -s -X POST http://127.0.0.1:3345/api/users \
-H 'Authorization: Bearer <token>' -H 'Content-Type: application/json' \
-d '{"username":"reception","password":"<password>","role":"user","email":"desk@example.com","tenantId":"acme"}'
{ "success": true, "message": "User created successfully",
"data": { "id": "6717...", "username": "reception", "role": "user", "active": true, "tenantId": "acme" } }
Rules the server enforces:
- Usernames are 3 to 50 letters, digits,
-and_, stored in lower case. A taken username gets 409. - The role and the tenant must exist (400 otherwise).
- Only a global admin may set
tenantId. A tenant admin's new users always land in the tenant admin's own tenant, and atenantIdthey send on update is ignored. A global admin who leaves it out gets tenantdefault. - Only a global admin may give the
adminrole (403 otherwise). - You cannot change your own role, deactivate yourself, or delete yourself.
- A user outside the caller's tenant reads as 404.
- Passwords are stored as scrypt hashes and are never returned.
Set active to false to block a login without deleting the user's history.
API keys
API keys let programs call LiteBot without a password. Each key has a name, a role and a tenant, and acts with that role's permissions inside that tenant.
curl -s -X POST http://127.0.0.1:3345/api/auth/api-keys \
-H 'Authorization: Bearer <token>' -H 'Content-Type: application/json' \
-d '{"name":"crm-integration","role":"tenant-admin","tenantId":"acme"}'
{
"success": true,
"message": "API key created successfully. Copy it now - it won't be shown again!",
"data": { "id": "6718...", "name": "crm-integration", "role": "tenant-admin",
"key": "lbk_3f9c...", "tenantId": "acme" }
}
- The key is
lbk_followed by 32 hex characters. It appears only in this response. LiteBot stores its first 8 characters and a scrypt hash, so a lost key cannot be recovered: revoke it and create a new one. - Send it as
X-API-Key: lbk_...on HTTP requests, or?api_key=lbk_...when opening/voice. tenantIdis honoured only for a global admin; a tenant caller's keys always belong to its own tenant. Only a global admin may create anadmin-role key, because such a key is unrestricted.
| Method and path | Needs | Notes |
|---|---|---|
GET /api/auth/api-keys |
api-keys:read |
Unrevoked keys in scope: name, role, tenant, created, last used, created by. Never the key |
POST /api/auth/api-keys |
api-keys:create |
name (3 to 100 characters), role, optional tenantId |
DELETE /api/auth/api-keys/:id |
api-keys:delete |
Marks the key revoked |
Known limits:
- A verified key is cached for
API_KEY_CACHE_TTLseconds (default 60). A revoked key keeps working until its cache entry expires. - The create request accepts
expiresAtand stores it, but the server does not check it when the key is used. Revoke keys you no longer need. - After
API_KEY_FAIL_LIMITbad keys (default 20) from one address withinAPI_KEY_FAIL_WINDOW_MS(default 60000), further key attempts from that address are refused until the window passes. - Each key may hold
API_KEY_DEFAULT_MAX_VOICE_CONCURRENTvoice sessions at once (default 50).
Tenants
A tenant is a workspace: one customer's bots and everything that hangs off them. Tenant ids are 2 to 40 characters of lower-case letters, digits and -, starting with a letter or digit. LiteBot creates the tenant default at start-up; bots, users and keys without a tenant belong to it.
What is scoped to a tenant:
| Data | How it is scoped |
|---|---|
| Bots | Each bot has a tenantId (default default) |
| Conversations, traces, analytics | Through the bot that handled them, or a tenantId stamped on the row |
| Tools | Database tools belong to one tenant or to all (*). Global tools reach a tenant only through its shared-tools list |
| Knowledge base collections | A tenant sees the global collections and its own, and writes only to its own |
| Users and API keys | Each has a tenantId |
| Dialer campaigns | Each campaign belongs to a tenant |
| LLM provider keys | Optional per-tenant keys; see below |
A tenant caller never sees another tenant's rows: a bot, user, key or campaign outside its scope reads as 404.
Global admin and tenant admin
Global admin (admin) |
Tenant admin (tenant-admin) |
|
|---|---|---|
| Sees | Every tenant, or one tenant with X-Tenant-Id |
Own tenant only |
| Creates tenants | Yes | No |
| Moves a bot to another tenant | Yes, by setting tenantId on the bot |
No: its bots always stay in its tenant |
| Sets the tenant's LLM keys | Any tenant | Own tenant (needs api-keys:update) |
| Chooses shared global tools | Yes | No, read only |
A global admin can narrow one request to a tenant by sending X-Tenant-Id: acme. The dashboard's tenant switcher does this (Viewing as a tenant). The header is ignored for everyone else; a malformed value gets 400.
Tenant endpoints
| Method and path | Needs | Notes |
|---|---|---|
GET /api/tenants |
signed in | Global admin: all tenants. Others: their own |
POST /api/tenants |
admin |
{"id":"acme","name":"Acme Clinics"}. A taken id gets 409 |
GET /api/tenants/:id/providers |
api-keys:read |
Supported providers and the configured ones, masked |
PUT /api/tenants/:id/providers/:provider |
api-keys:update |
{"apiKey":"...","model":"..."}. Returns only the last 4 characters |
DELETE /api/tenants/:id/providers/:provider |
api-keys:update |
Remove the tenant's key |
GET /api/tenants/:id/usage?days=7 |
api-keys:read |
Spend and usage over 1 to 90 days (default 7) |
GET /api/tenants/:id/shared-tools |
tools:read |
The tenant's shared-tools list |
PUT /api/tenants/:id/shared-tools |
admin |
{"tools":["bookAppointment"]} or {"mode":"all"} |
There is no endpoint to rename or delete a tenant.
curl -s -X POST http://127.0.0.1:3345/api/tenants \
-H 'Authorization: Bearer <token>' -H 'Content-Type: application/json' \
-d '{"id":"acme","name":"Acme Clinics"}'
Tenant LLM keys
Per-tenant keys are supported for groq, openrouter, gemini, openai and sarvam. A tenant's bots use its own key for a provider when one is set, and the platform key from .env otherwise. Tenant keys are stored encrypted (see Encryption at rest) and are never returned in full.
Shared tools
A new tenant starts with an empty shared-tools list: none of the global tools are available to its bots until a global admin grants them. {"mode":"all"} gives the tenant every global tool, now and in future. The default tenant, created without a list, sees every global tool. The tenant's own database tools are always available to it.
Putting a bot in a tenant
- A tenant user's new bots go into their own tenant.
- A global admin picks the tenant in the bot form (or sends
tenantIdinPOST /api/botsorPUT /api/bots/:botId). Leaving it out uses the admin's current tenant, which isdefaultunlessX-Tenant-Idis set. - Bot files under
config/bots/without atenantIdbelong todefault.
Where to go next
- Dashboard: the pages each role sees.
- Security: protect tokens, keys and
JWT_SECRET. - Tools: global tools, tenant tools and the shared-tools list.
- HTTP API reference: every endpoint in one place.