Session Limits Feature
Overview
The Session Limits feature in Baasix provides administrators with fine-grained control over user sessions across different device types, roles, and tenants. This feature allows you to:
- Limit the number of concurrent sessions per user per session type (
mobile,web) - Configure different default limits per role, with a project-wide default as the fallback
- Override limits for an individual user
- Configure different limits for different tenants in multi-tenant mode
- Block specific session types entirely by setting limits to
0 - Maintain unlimited default sessions for backward compatibility
Upgrading from mobile_session_limit / web_session_limit / session_limit_roles
These three legacy settings have been removed and replaced by a single session_limits JSON field. A startup
migration (version 0.1.82) converts existing values automatically — see Migration Notes below.
Startup migrations only run when MIGRATIONS_AUTO_RUN=true is set; otherwise run migrations manually. Until the
migration runs, previously configured limits are not enforced.
Key Concepts
Session Types
Baasix supports three session types:
default- Traditional sessions, unlimited by design for backward compatibilitymobile- Sessions from mobile applicationsweb- Sessions from web applications
Session Limit Enforcement
- Session limits are enforced only for
mobileandwebsession types defaultsessions are always allowed regardless of configured limits- Limits are checked during login, registration, magic link authentication, and tenant switching
- When a limit is reached, new logins of that type are blocked with a descriptive error message
Configuration
Session limits are configured through the session_limits JSON field, which can be set at two levels:
- Settings (
baasix_Settings.session_limits) - global or per-tenant defaults, with optional per-role overrides - User record (
baasix_User.session_limits) - an override for a single user
Settings Shape
{
"default": { "web": 2, "mobile": 1 },
"roles": {
"<role-id>": { "web": 5, "mobile": 2 }
}
}default- Applied to any user whose role doesn't have a more specific entryroles- A map of role ID to{ web, mobile }limits. Only roles listed here get a role-specific limit; unlisted roles fall back todefault
Setting Session Limits
Global Settings (Non-Multi-Tenant)
// Set global session limits
await SettingsModel.create({
tenant_Id: null,
project_name: 'My Project',
session_limits: {
default: { web: 3, mobile: 1 }, // Fallback for any role without a specific entry
roles: {
'role-uuid-for-sales': { web: 5, mobile: 2 },
},
},
});Tenant-Specific Settings (Multi-Tenant Mode)
// Set session limits for a specific tenant
await SettingsModel.create({
tenant_Id: tenantId,
project_name: 'Tenant Project',
session_limits: {
default: { web: 2, mobile: 0 }, // Block all mobile sessions for this tenant by default
},
});A tenant's session_limits fully replaces the global session_limits when present — there is no deep merge
between global and tenant-specific configuration. Set every level (default and any roles entries) you need in the
tenant's own session_limits.
Per-User Override
Set session_limits directly on the user record to override both the role and default limits for that user:
// Give a specific user a higher web limit and unlimited mobile sessions
await UserModel.update(userId, {
session_limits: { web: 10, mobile: -1 },
});A null or absent session_limits on the user means "inherit from the role/default resolution below."
Special Values
-1- Unlimited sessions of this type0- Block all sessions of this type- Positive integer - Maximum number of concurrent sessions allowed
- Missing key - Falls through to the next level in the resolution order
Resolution Order
For each session type (web or mobile), the limit is resolved in this order, stopping at the first level that applies:
- Administrator role - Always bypasses session limits entirely, regardless of any configuration
- User override -
baasix_User.session_limits[type], if set - Role limit -
session_limits.roles[roleId][type], if the user's role has an entry - Project default -
session_limits.default[type] - Unlimited - If none of the above apply
A missing key at any level (rather than the whole object being absent) simply falls through to the next level — for example, if roles[roleId] only defines web, the user's mobile limit still falls through to default.mobile.
Malformed session_limits configuration (wrong shape, non-numeric values, etc.) fails open — it is treated as unset,
and the resolution falls through as if that level were absent.
API Integration
Login with Session Type
Include the authType parameter in your login requests:
// Mobile login
const response = await fetch('/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: 'user@example.com',
password: 'password123',
authType: 'mobile', // Specify session type
}),
});Registration with Session Type
// Mobile registration
const response = await fetch('/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: 'user@example.com',
password: 'password123',
firstName: 'John',
lastName: 'Doe',
authType: 'mobile',
}),
});Magic Link Authentication
// Request magic link with session type
const response = await fetch('/auth/magiclink', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: 'user@example.com',
link: 'https://myapp.com',
authType: 'web',
}),
});Tenant Switching with Session Type
// Switch tenant with session type validation
const response = await fetch('/auth/switch-tenant', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
tenant_Id: 'tenant-uuid',
authType: 'mobile',
}),
});Implementation Details
Validation Logic
The session validation logic is implemented in baasix/utils/auth.js:validateSessionLimits():
- Skip validation for
defaultsession types - Validate session type - must be
mobileorweb - Bypass for administrators - the
administratorrole always passes - Retrieve settings - tenant-specific
session_limitstakes precedence over globalsession_limits(full replacement, not a merge) - Resolve the limit - user override → role entry →
default→ unlimited - Check limits - compare active sessions count against the resolved limit
- Return validation result with appropriate error messages
Database Schema
Sessions are stored in the baasix_Sessions table with the following relevant fields:
CREATE TABLE baasix_Sessions (
id UUID PRIMARY KEY,
token VARCHAR(255) UNIQUE NOT NULL,
user_Id UUID REFERENCES baasix_User(id),
type VARCHAR(50) DEFAULT 'default', -- Session type
expiresAt TIMESTAMP NOT NULL,
createdAt TIMESTAMP DEFAULT NOW(),
updatedAt TIMESTAMP DEFAULT NOW()
);baasix_Settings.session_limits and baasix_User.session_limits are both nullable JSON columns.
Active Session Counting
Active sessions are counted using:
const activeSessions = await SessionModel.findAll({
where: {
user_Id: userId,
type: sessionType,
expiresAt: {
[Op.gt]: new Date(), // Only non-expired sessions
},
},
});Error Handling
Common Error Responses
Session Limit Reached
{
"message": "Maximum mobile session limit (2) reached. Please logout from another device."
}Sessions Blocked
{
"message": "Mobile sessions are not allowed."
}Invalid Session Type
{
"message": "Invalid session type. Must be 'mobile' or 'web'"
}Error Recovery
When users encounter session limit errors, they have several options:
- Logout from other devices - Free up session slots
- Use default session type - Not subject to limits
- Contact administrator - To adjust limits if needed
Multi-Tenant Behavior
Settings Priority
In multi-tenant mode, settings are resolved in this order:
- Tenant-specific
session_limits- If the tenant has its ownsession_limitsset, it fully replaces the global configuration (no deep merge) - Global
session_limits- Used if the tenant has nosession_limitsof its own - No limits - If neither is configured
Tenant Isolation
Session limits are enforced per tenant:
- Users with roles in multiple tenants have separate session counts per tenant
- Switching tenants may create new sessions subject to the target tenant's limits
- Session limits only apply to tenant-specific roles
Admin UI
Settings → Sessions tab provides a structured editor for session_limits:
- A default limits section for
webandmobile - A per-role table to add rows mapping a role to its own
web/mobilelimits (theadministratorrole is excluded from this list, since it always bypasses limits)
Per-user overrides are not part of this screen — they're edited directly on the user record's session_limits JSON field (e.g. via the Users collection editor or the API).
Public Settings Exposure
The public (unauthenticated) GET /settings endpoint only exposes the project-wide default limits, never per-role or per-user values:
session_limits.defaultis included as-is- Derived legacy hint keys
web_session_limitandmobile_session_limitare computed fromsession_limits.defaultand included for older clients that still read those fields
Role IDs, per-role limits, and per-user overrides are never included in the public response — only authenticated admin requests to the full settings resource can see the roles map.
Use Cases
Mobile App Restriction
// Allow only 1 mobile session per user (single device), unlimited web
{
"default": { "mobile": 1, "web": -1 }
}Role-Specific Limits
// Sales reps get more concurrent web sessions than the general default
{
"default": { "web": 2, "mobile": 1 },
"roles": {
"sales-role-uuid": { "web": 5, "mobile": 2 }
}
}Per-User Override
// One power user gets unlimited sessions of both types
await UserModel.update(userId, {
session_limits: { web: -1, mobile: -1 },
});Enterprise Security
// Strict limits for enterprise tenant
{
"default": { "web": 1, "mobile": 2 }
}Blocking Device Types
// Block mobile access entirely, keep web at 5
{
"default": { "mobile": 0, "web": 5 }
}Development Environment
// No limits for development
{
"default": { "web": -1, "mobile": -1 }
}Testing
The feature includes comprehensive test coverage:
Session Type Tests (test/session-types.test.js)
- Default session creation
- Mobile/web session creation
- Invalid session type rejection
- Session limit enforcement (default, per-role, per-user)
- Administrator bypass
- Zero limit blocking
- Default session bypass
Multi-Tenant Tests (test/switch-tenant-session-limits.test.js)
- Tenant switching with limits
- Cross-tenant session isolation
- Tenant-specific limit configuration (full replacement, not merge)
Running Tests
cd api/
npm test -- session-types.test.js
npm test -- switch-tenant-session-limits.test.jsBest Practices
Limit Configuration
- Start with reasonable limits - Consider user workflows
- Use role-specific limits sparingly - Reserve them for roles that genuinely need different behavior; otherwise rely on
default - Monitor usage patterns - Adjust limits based on real usage
- Communicate limits - Let users know about session restrictions
Error Handling
- Provide clear messages - Help users understand what to do
- Offer alternatives - Suggest logout or contact support
- Log limit violations - Monitor for potential abuse
Security Considerations
- Regular cleanup - Expired sessions are automatically cleaned
- Audit sessions - Monitor active sessions per user
- Adjust for risk - Stricter limits for sensitive applications
- Remember the administrator bypass - Administrators are never limited; don't rely on
session_limitsas a control on admin accounts
Monitoring and Analytics
Session Metrics
Track these metrics for insights:
- Active sessions per user per type
- Session limit violations per tenant
- Peak concurrent sessions
- Average session duration by type
Database Queries
-- Count active sessions by type
SELECT type, COUNT(*) as active_count
FROM baasix_Sessions
WHERE expiresAt > NOW()
GROUP BY type;
-- Users hitting session limits (project default, mobile)
SELECT user_Id, type, COUNT(*) as session_count
FROM baasix_Sessions
WHERE expiresAt > NOW()
GROUP BY user_Id, type
HAVING COUNT(*) >= (
SELECT CAST(session_limits->'default'->>'mobile' AS INTEGER)
FROM baasix_Settings
WHERE tenant_Id IS NULL
);Troubleshooting
Common Issues
-
"Session limit reached" but user has no active sessions
- Check for expired sessions not yet cleaned up
- Verify session cleanup job is running
-
Limits not being enforced
- Verify
session_limitsis properly configured and well-formed (malformed config fails open / is ignored) - Check that
authTypeis being passed in requests - Confirm the
0.1.82migration has run if you're upgrading from the legacy fields — see Migration Notes
- Verify
-
Multi-tenant limits not working
- Ensure the tenant has its own
session_limitsset — remember it fully replaces the global config, so a partial tenant config won't inherit missing keys from global - Verify user has tenant-specific roles
- Ensure the tenant has its own
-
A role-specific limit isn't applying
- Confirm the role ID key in
session_limits.rolesexactly matches the user'srole_Id - Check whether the user has a per-user
session_limitsoverride, which takes precedence over the role entry
- Confirm the role ID key in
Debug Information
Enable debug logging to troubleshoot:
// In validateSessionLimits function
console.log('Session validation:', {
userId,
sessionType,
tenantId,
roleId,
activeSessions: activeSessions.length,
resolvedLimit,
isValid: activeSessions.length < resolvedLimit,
});Migration Notes
Upgrading Existing Installations
Startup migrations require MIGRATIONS_AUTO_RUN
Startup migrations only run when MIGRATIONS_AUTO_RUN=true is set in your environment. If it isn't set, run
migrations manually. Until the migration runs, previously configured mobile_session_limit / web_session_limit /
session_limit_roles values are not enforced — the columns are gone, and session_limits hasn't been populated
yet.
Version 0.1.82 includes a startup migration that automatically converts the legacy settings to the new session_limits shape:
- If
session_limit_roleswas empty ornull, the oldmobile_session_limit/web_session_limitvalues becomesession_limits.default - If
session_limit_roleslisted specific role IDs, each listed role gets its own entry undersession_limits.rolesusing the old limit values; roles that were not listed remain unlimited, preserving prior behavior - After conversion, the legacy
mobile_session_limit,web_session_limit, andsession_limit_rolescolumns are dropped
- Existing sessions continue to work (marked as
defaulttype) - No immediate impact on current users once migrated
- Configure limits gradually - Start with high limits and reduce
- Update client applications to pass
authTypeparameter
Database Migration
The type field in baasix_Sessions defaults to 'default' for backward compatibility. No additional data migration is required for sessions themselves — only the session_limits conversion described above.
API Reference
POST /auth/login
Parameters:
authType(optional):'mobile'|'web'|'default'
POST /auth/register
Parameters:
authType(optional):'mobile'|'web'|'default'
POST /auth/magiclink
Parameters:
authType(optional):'mobile'|'web'|'default'
POST /auth/switch-tenant
Parameters:
authType(optional):'mobile'|'web'|'default'
All endpoints return standard authentication responses with session limit validation applied when authType is specified and not 'default'.
See also
- Authentication Routes - Endpoints and usage for auth, login, register
- Settings Routes - Configure global and tenant settings for session limits
- Multi-tenant Guide - How tenant-specific settings and isolation work
- Error Handling Guide - Handling authentication and session errors gracefully
- Integration Guide - Client-side considerations and passing
authType