BaasixBaasix
GuidesAuthentication

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:

  1. default - Traditional sessions, unlimited by design for backward compatibility
  2. mobile - Sessions from mobile applications
  3. web - Sessions from web applications

Session Limit Enforcement

  • Session limits are enforced only for mobile and web session types
  • default sessions 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:

  1. Settings (baasix_Settings.session_limits) - global or per-tenant defaults, with optional per-role overrides
  2. 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 entry
  • roles - A map of role ID to { web, mobile } limits. Only roles listed here get a role-specific limit; unlisted roles fall back to default

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 type
  • 0 - 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:

  1. Administrator role - Always bypasses session limits entirely, regardless of any configuration
  2. User override - baasix_User.session_limits[type], if set
  3. Role limit - session_limits.roles[roleId][type], if the user's role has an entry
  4. Project default - session_limits.default[type]
  5. 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',
  }),
});
// 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():

  1. Skip validation for default session types
  2. Validate session type - must be mobile or web
  3. Bypass for administrators - the administrator role always passes
  4. Retrieve settings - tenant-specific session_limits takes precedence over global session_limits (full replacement, not a merge)
  5. Resolve the limit - user override → role entry → default → unlimited
  6. Check limits - compare active sessions count against the resolved limit
  7. 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:

  1. Logout from other devices - Free up session slots
  2. Use default session type - Not subject to limits
  3. Contact administrator - To adjust limits if needed

Multi-Tenant Behavior

Settings Priority

In multi-tenant mode, settings are resolved in this order:

  1. Tenant-specific session_limits - If the tenant has its own session_limits set, it fully replaces the global configuration (no deep merge)
  2. Global session_limits - Used if the tenant has no session_limits of its own
  3. 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 web and mobile
  • A per-role table to add rows mapping a role to its own web/mobile limits (the administrator role 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.default is included as-is
  • Derived legacy hint keys web_session_limit and mobile_session_limit are computed from session_limits.default and 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.js

Best 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_limits as 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

  1. "Session limit reached" but user has no active sessions

    • Check for expired sessions not yet cleaned up
    • Verify session cleanup job is running
  2. Limits not being enforced

    • Verify session_limits is properly configured and well-formed (malformed config fails open / is ignored)
    • Check that authType is being passed in requests
    • Confirm the 0.1.82 migration has run if you're upgrading from the legacy fields — see Migration Notes
  3. Multi-tenant limits not working

    • Ensure the tenant has its own session_limits set — 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
  4. A role-specific limit isn't applying

    • Confirm the role ID key in session_limits.roles exactly matches the user's role_Id
    • Check whether the user has a per-user session_limits override, which takes precedence over the role entry

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_roles was empty or null, the old mobile_session_limit / web_session_limit values become session_limits.default
  • If session_limit_roles listed specific role IDs, each listed role gets its own entry under session_limits.roles using the old limit values; roles that were not listed remain unlimited, preserving prior behavior
  • After conversion, the legacy mobile_session_limit, web_session_limit, and session_limit_roles columns are dropped
  1. Existing sessions continue to work (marked as default type)
  2. No immediate impact on current users once migrated
  3. Configure limits gradually - Start with high limits and reduce
  4. Update client applications to pass authType parameter

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

On this page