> ## Documentation Index
> Fetch the complete documentation index at: https://docs.meridian.study/llms.txt
> Use this file to discover all available pages before exploring further.

# Session Management

> Multi-device session management for concurrent logins across devices

## Overview

Meridian supports multi-device session management, allowing users to be logged in from multiple devices simultaneously. Each device maintains its own session with independent refresh tokens. This replaces the previous single-session model where only one device could be logged in at a time.

When [multi-tenant identity](/backend/multi-tenant-identity) is enabled, sessions are stored in the **global database** and keyed by `globalUserId`, so token refresh works across all tenant subdomains (SSO). Otherwise, sessions may be stored per-tenant. The refresh flow validates the session and resolves the tenant user for the current `req.school` when issuing a new access token.

### Key Features

* **Multi-Device Support**: Users can be logged in from multiple devices simultaneously without invalidating other sessions
* **Device Tracking**: Each session tracks device information, IP address, and client type for security auditing
* **Session Management**: Users can view and revoke specific sessions from their account settings
* **Automatic Expiration**: Sessions automatically expire after 30 days and are cleaned up automatically

## How It Works

### Session Creation

When a user logs in (via email/password, Google OAuth, Apple Sign In, or SAML), a new session is created:

1. User provides credentials or completes OAuth flow
2. System creates a new session record with:
   * Unique refresh token
   * Device information (device type, user agent)
   * IP address
   * Client type (web, mobile, ios, android)
   * Expiration timestamp (30 days)
3. Access token (short-lived; default **15 minutes** unless overridden by env) and refresh token (default **30 days**) are set as HTTP-only cookies
4. Session is stored in database, allowing multiple concurrent sessions per user

### Session Validation

When refreshing tokens, the system validates against the session database:

1. Extract refresh token from HTTP-only cookie
2. Verify JWT signature
3. Look up session in database
4. Check if session is expired
5. Update last used timestamp
6. Issue new access token

### Session Deletion

When logging out, only the current session is deleted. Other devices remain logged in.

## API Endpoints

### List Active Sessions

Get all active sessions for the current user.

**Endpoint:** `GET /sessions`

**Response:**

```json theme={null}
{
  "success": true,
  "data": {
    "sessions": [
      {
        "id": "507f1f77bcf86cd799439011",
        "deviceInfo": "iPhone",
        "clientType": "ios",
        "ipAddress": "192.168.1.1",
        "lastUsed": "2024-01-15T10:30:00Z",
        "createdAt": "2024-01-01T08:00:00Z",
        "expiresAt": "2024-01-31T08:00:00Z",
        "isCurrent": true
      },
      {
        "id": "507f1f77bcf86cd799439012",
        "deviceInfo": "Chrome Browser",
        "clientType": "web",
        "ipAddress": "192.168.1.2",
        "lastUsed": "2024-01-14T15:20:00Z",
        "createdAt": "2023-12-20T09:00:00Z",
        "expiresAt": "2024-01-19T09:00:00Z",
        "isCurrent": false
      }
    ]
  }
}
```

**Response Fields:**

* `sessions` (array): Array of active sessions for the user
  * `id` (string): Unique session identifier
  * `deviceInfo` (string): Human-readable device description (e.g., "iPhone", "Chrome Browser")
  * `clientType` (string): Client type: `web`, `mobile`, `ios`, or `android`
  * `ipAddress` (string): IP address where session was created
  * `lastUsed` (string): ISO 8601 timestamp of last token refresh
  * `createdAt` (string): ISO 8601 timestamp of session creation
  * `expiresAt` (string): ISO 8601 timestamp when session expires
  * `isCurrent` (boolean): Whether this is the current session making the request

### Revoke Specific Session

Revoke a specific session by its ID. Only the session owner can revoke their own sessions.

**Endpoint:** `DELETE /sessions/:sessionId`

**Path Parameters:**

* `sessionId` (string, required): The ID of the session to revoke

**Response:**

```json theme={null}
{
  "success": true,
  "message": "Session revoked successfully"
}
```

**Error Response (404):**

```json theme={null}
{
  "success": false,
  "message": "Session not found or you do not have permission to revoke it"
}
```

### Revoke All Other Sessions

Revoke all sessions except the current one. Useful for "logout from all devices" functionality.

**Endpoint:** `POST /sessions/revoke-all-others`

**Response:**

```json theme={null}
{
  "success": true,
  "message": "All other sessions revoked successfully"
}
```

**Warning:** This will log out the user from all devices except the one making the request. Use with caution.

## Frontend Integration

### Displaying Sessions

Show users their active sessions in account settings:

**React (Web):**

```javascript theme={null}
import { useState, useEffect } from 'react';
import apiRequest from './utils/postRequest';

function SessionsList() {
  const [sessions, setSessions] = useState([]);

  useEffect(() => {
    fetchSessions();
  }, []);

  const fetchSessions = async () => {
    const response = await apiRequest('/sessions', null, { method: 'GET' });
    if (response.success) {
      setSessions(response.data.sessions);
    }
  };

  const revokeSession = async (sessionId) => {
    const response = await apiRequest(`/sessions/${sessionId}`, null, { 
      method: 'DELETE' 
    });
    if (response.success) {
      fetchSessions(); // Refresh list
    }
  };

  return (
    <div>
      <h2>Active Sessions</h2>
      {sessions.map(session => (
        <div key={session.id}>
          <p>{session.deviceInfo} - {session.clientType}</p>
          <p>Last used: {new Date(session.lastUsed).toLocaleString()}</p>
          {!session.isCurrent && (
            <button onClick={() => revokeSession(session.id)}>
              Revoke
            </button>
          )}
          {session.isCurrent && <span>Current Session</span>}
        </div>
      ))}
    </div>
  );
}
```

**React Native (Mobile):**

```tsx theme={null}
import { useState, useEffect } from 'react';
import { View, Text, Button } from 'react-native';
import { httpClient } from '@/services/api';

function SessionsList() {
  const [sessions, setSessions] = useState([]);

  useEffect(() => {
    fetchSessions();
  }, []);

  const fetchSessions = async () => {
    try {
      const response = await httpClient.get('/sessions');
      if (response.success) {
        setSessions(response.data.sessions);
      }
    } catch (error) {
      console.error('Failed to fetch sessions:', error);
    }
  };

  const revokeSession = async (sessionId: string) => {
    try {
      await httpClient.delete(`/sessions/${sessionId}`);
      fetchSessions(); // Refresh list
    } catch (error) {
      console.error('Failed to revoke session:', error);
    }
  };

  return (
    <View>
      <Text>Active Sessions</Text>
      {sessions.map(session => (
        <View key={session.id}>
          <Text>{session.deviceInfo} - {session.clientType}</Text>
          <Text>Last used: {new Date(session.lastUsed).toLocaleString()}</Text>
          {!session.isCurrent && (
            <Button 
              title="Revoke" 
              onPress={() => revokeSession(session.id)} 
            />
          )}
          {session.isCurrent && <Text>Current Session</Text>}
        </View>
      ))}
    </View>
  );
}
```

## Examples

### List All Sessions

```javascript theme={null}
// Fetch all active sessions
const response = await fetch('/sessions', {
  credentials: 'include'
});
const { sessions } = await response.json();

// Display sessions
sessions.forEach(session => {
  console.log(`${session.deviceInfo} - Last used: ${session.lastUsed}`);
});
```

### Revoke a Session

```javascript theme={null}
// Revoke a specific session
await fetch(`/sessions/${sessionId}`, {
  method: 'DELETE',
  credentials: 'include'
});

// Revoke all other sessions
await fetch('/sessions/revoke-all-others', {
  method: 'POST',
  credentials: 'include'
});
```

### Check Current Session

```javascript theme={null}
const response = await fetch('/sessions', {
  credentials: 'include'
});
const { sessions } = await response.json();

const currentSession = sessions.find(s => s.isCurrent);
console.log('Current session:', currentSession.deviceInfo);
```

## Security Considerations

### Session Expiration

* Sessions expire after **30 days** of inactivity
* Expired sessions are automatically cleaned up
* Users must re-authenticate after expiration

### Device Tracking

Each session stores:

* **Device Information**: Extracted from User-Agent header
* **IP Address**: For security auditing
* **Client Type**: web, mobile, ios, or android
* **Last Used**: Timestamp of last token refresh

### Best Practices

* **Session Monitoring**: Regularly review active sessions and revoke suspicious sessions immediately
* **User Education**: Encourage users to review sessions and provide "logout from all devices" option
* **Security Features**: Use HTTP-only cookies, validate sessions on each refresh, clean up expired sessions periodically

## Migration Notes

### Backward Compatibility

The `refreshToken` field remains in the User schema for backward compatibility but is **no longer used**. Existing users will need to log in again to create sessions.

### Database Schema

The new `Session` model includes:

* `userId`: Reference to User
* `refreshToken`: Unique refresh token for this session
* `deviceInfo`: Human-readable device description
* `userAgent`: Full user agent string
* `ipAddress`: IP address at creation
* `clientType`: web, mobile, ios, or android
* `lastUsed`: Last token refresh timestamp
* `expiresAt`: Session expiration timestamp

### Cleanup Job

Consider adding a periodic job to clean up expired sessions:

```javascript theme={null}
const { cleanupExpiredSessions } = require('./utilities/sessionUtils');

// Run daily
setInterval(async () => {
    const deleted = await cleanupExpiredSessions(req);
    console.log(`Cleaned up ${deleted} expired sessions`);
}, 24 * 60 * 60 * 1000);
```

## Troubleshooting

### Session Not Found

**Error:** `Session not found or you do not have permission to revoke it`

**Causes:**

* Session was already revoked
* Session expired
* User doesn't own the session

**Solution:**

* Refresh the sessions list
* Check session expiration dates
* Verify user authentication

### Multiple Sessions Not Working

**Issue:** New login invalidates previous session

**Causes:**

* Old code still using `refreshToken` field on User model
* Session creation not implemented in login endpoint

**Solution:**

* Verify all login endpoints use `createSession()`
* Check that `refreshToken` field is no longer updated on User model
* Ensure Session model is registered in `getModelService.js`

### Sessions Not Expiring

**Issue:** Sessions remain active after expiration

**Causes:**

* Cleanup job not running
* Expiration check not implemented

**Solution:**

* Implement periodic cleanup job
* Verify `expiresAt` is set correctly on session creation
* Check `validateSession()` includes expiration check

***

## Related pages

<CardGroup cols={2}>
  <Card title="Authentication overview" icon="shield" href="/backend/auth/overview">
    Email, Google, Apple, JWT cookies vs mobile headers, and how login reaches `issueTokens`.
  </Card>

  <Card title="SAML" icon="key" href="/backend/auth/SAML">
    University SSO; after callback, tokens and sessions align with the same global model.
  </Card>

  <Card title="Multi-tenant identity & SSO" icon="users" href="/backend/multi-tenant-identity">
    Why sessions live in the global DB and how refresh works across subdomains.
  </Card>

  <Card title="Backend best practices" icon="server" href="/backend/best-practices">
    Middleware, `getGlobalModelService`, and patterns next to session-aware routes.
  </Card>
</CardGroup>

## Notes

* Sessions work independently across devices - logging out from one device doesn't affect others
* Each session has its own refresh token stored securely in the database
* Session information is automatically tracked (device, IP, client type)
* Users can manage their sessions through the account settings page
* Expired sessions are automatically invalidated and cleaned up
