Container Session API Integration Guide
Task ID: F.5.1 Version: 1.0.0 Last Updated: January 5, 2026 Status: Production Ready
Table of Contents
- Overview
- Architecture
- Authentication
- API Endpoints
- Common Workflows
- Error Handling
- Rate Limiting
- Multi-Tenant Considerations
- TypeScript Integration
- Testing
Overview
The Container Session Lifecycle API manages licensed container sessions for CODITECT deployments. It supports:
- License validation for Docker, Cloud Workstation, and Kubernetes containers
- Heartbeat-based session management with automatic expiry
- Multi-user session tracking for shared containers (Cloud Workstations)
- Admin controls for session termination and user management
- Real-time statistics for license utilization monitoring
Key Concepts
| Concept | Description |
|---|---|
| Container Session | A license-validated container instance tracked by heartbeats |
| User Session | An individual user within a multi-user container |
| Session Token | Opaque token used for container-level operations |
| User Token | Opaque token used for user-level operations |
| Heartbeat | Periodic signal to keep session active (default: 60s) |
| TTL | Time-to-live before session expires without heartbeat (default: 6 min) |
Container Types
| Type | Max Users | Use Case |
|---|---|---|
docker | 1 | Local developer containers |
cloud_workstation | 1-100 | GCP Cloud Workstations (shared) |
kubernetes | 1-10 | Kubernetes pod deployments |
Architecture
Session Lifecycle
┌─────────────────────────────────────────────────────────────────┐
│ CONTAINER SESSION LIFECYCLE │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Container Start │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Validate │ ◄── POST /api/v1/sessions/validate/ │
│ │ License │ │
│ └──────┬───────┘ │
│ │ │
│ ┌────┴────┐ │
│ Valid Invalid │
│ │ │ │
│ ▼ ▼ │
│ ┌────┐ ┌───────┐ │
│ │RUN │ │ EXIT │ │
│ └─┬──┘ └───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Heartbeat │ ◄── POST /api/v1/sessions/heartbeat/ │
│ │ Loop │ (every 60s) │
│ └──────┬───────┘ │
│ │ │
│ ┌────┴────┐ │
│ Continue Shutdown │
│ │ │ │
│ │ ▼ │
│ │ ┌──────────┐ │
│ │ │ Release │ ◄── POST /api/v1/sessions/release/ │
│ │ └──────────┘ │
│ │ │
│ └──► (Loop) │
│ │
└─────────────────────────────────────────────────────────────────┘
Multi-User Session Flow
┌─────────────────────────────────────────────────────────────────┐
│ MULTI-USER SESSION FLOW │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Container Session (Cloud Workstation) │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ User Joins │ ◄── POST /api/v1/sessions/user/join/ │
│ │ (User 1) │ │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ User Joins │ ◄── POST /api/v1/sessions/user/join/ │
│ │ (User 2) │ │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ User Leaves │ ◄── POST /api/v1/sessions/user/leave/ │
│ │ (User 1) │ │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ Container Session Remains Active for User 2 │
│ │
└─────────────────────────────────────────────────────────────────┘
Authentication
All API requests require a valid JWT Bearer token.
Request Headers
Authorization: Bearer <JWT_TOKEN>
Content-Type: application/json
Obtaining JWT Token
1. User Login (OAuth2 Password Flow):
curl -X POST https://api.coditect.ai/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "SecurePassword123!"
}'
Response:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600
}
2. Use Access Token:
export JWT_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
API Endpoints
Base URL
Production: https://api.coditect.ai/api/v1/sessions
Staging: https://staging-api.coditect.ai/api/v1/sessions
1. Container Validation
Endpoint: POST /api/v1/sessions/validate/
Validates a container's license key and creates or retrieves a container session.
Request
curl -X POST https://api.coditect.ai/api/v1/sessions/validate/ \
-H "Authorization: Bearer $JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"license_key": "CODITECT-XXXX-XXXX-XXXX-XXXX",
"container_id": "container-abc123",
"container_type": "cloud_workstation",
"container_name": "my-dev-environment",
"hostname": "workstation-01.example.com",
"client_version": "1.0.0",
"max_users": 10
}'
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
license_key | string | Yes | Customer license key (format: CODITECT-XXXX-...) |
container_id | string | Yes | Unique container identifier (hostname, pod name, etc.) |
container_type | string | No | docker, cloud_workstation, kubernetes (default: docker) |
container_name | string | No | Human-readable container name |
hostname | string | No | Container hostname |
client_version | string | No | CODITECT client version |
max_users | integer | No | Max concurrent users (default: 1 for docker, 10 for workstation) |
workstation_jwt | string | No | GCP workstation JWT for OAuth validation |
Response (200 OK)
{
"session_id": "cs_1234567890abcdef",
"session_token": "tok_container_abc123xyz...",
"status": "active",
"license_key": "CODITECT-XXXX-XXXX-XXXX-XXXX",
"max_users": 10,
"current_user_count": 0,
"heartbeat_interval_seconds": 60,
"ttl_seconds": 360,
"expires_at": "2026-01-05T12:15:00Z",
"message": "Container session created successfully"
}
Response Fields
| Field | Type | Description |
|---|---|---|
session_id | string | Unique session ID (database primary key) |
session_token | string | Opaque token for heartbeat/release operations |
status | string | active, expired, or released |
license_key | string | Validated license key |
max_users | integer | Maximum concurrent users for this session |
current_user_count | integer | Number of users currently in session |
heartbeat_interval_seconds | integer | Recommended heartbeat interval (default: 60) |
ttl_seconds | integer | Session expiry TTL without heartbeat (default: 360) |
expires_at | string | ISO 8601 timestamp when session will expire |
message | string | Human-readable status message |
Error Responses
401 Unauthorized - Invalid License Key:
{
"error": "Invalid license key",
"code": "INVALID_LICENSE",
"details": {
"license_key": "CODITECT-XXXX-XXXX-XXXX-XXXX"
}
}
403 Forbidden - License Expired:
{
"error": "License has expired",
"code": "LICENSE_EXPIRED",
"details": {
"expired_at": "2025-12-31T23:59:59Z"
}
}
409 Conflict - Container Limit Reached:
{
"error": "Container limit reached for this license",
"code": "CONTAINER_LIMIT_REACHED",
"details": {
"limit": 5,
"active_containers": 5
}
}
TypeScript Example
import { containerSessionService } from '@/services/containerSession.service';
import type { ContainerValidateRequest } from '@/types/containerSession';
async function validateContainer() {
try {
const request: ContainerValidateRequest = {
license_key: 'CODITECT-XXXX-XXXX-XXXX-XXXX',
container_id: 'container-abc123',
container_type: 'cloud_workstation',
container_name: 'my-dev-environment',
hostname: 'workstation-01.example.com',
max_users: 10,
};
const response = await containerSessionService.validateContainer(request);
console.log('Session created:', response.session_id);
console.log('Session token:', response.session_token);
console.log('Expires at:', response.expires_at);
// Store session_token for heartbeat operations
localStorage.setItem('container_session_token', response.session_token);
return response;
} catch (error) {
if (error.response?.status === 401) {
console.error('Invalid license key');
} else if (error.response?.status === 403) {
console.error('License expired');
} else if (error.response?.status === 409) {
console.error('Container limit reached');
}
throw error;
}
}
2. Heartbeat
Endpoint: POST /api/v1/sessions/heartbeat/
Sends a heartbeat to keep the container session active. Must be called at least every heartbeat_interval_seconds (default: 60s).
Request
curl -X POST https://api.coditect.ai/api/v1/sessions/heartbeat/ \
-H "Authorization: Bearer $JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"session_token": "tok_container_abc123xyz...",
"active_user_count": 3,
"metadata": {
"cpu_usage": 45.2,
"memory_usage": 2048,
"active_processes": 12
}
}'
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
session_token | string | Yes | Session token from validation response |
active_user_count | integer | No | Current number of active users (for multi-user containers) |
metadata | object | No | Optional metadata (CPU, memory, custom metrics) |
Response (200 OK)
{
"status": "active",
"expires_at": "2026-01-05T12:20:00Z",
"current_user_count": 3,
"message": "Heartbeat received successfully"
}
Error Responses
401 Unauthorized - Invalid Session Token:
{
"error": "Invalid or expired session token",
"code": "INVALID_SESSION_TOKEN"
}
404 Not Found - Session Not Found:
{
"error": "Container session not found",
"code": "SESSION_NOT_FOUND"
}
410 Gone - Session Expired:
{
"error": "Container session has expired",
"code": "SESSION_EXPIRED",
"details": {
"expired_at": "2026-01-05T12:00:00Z"
}
}
TypeScript Example
import { containerSessionService } from '@/services/containerSession.service';
// Heartbeat loop
let heartbeatInterval: NodeJS.Timeout;
function startHeartbeat(sessionToken: string, intervalSeconds: number = 60) {
heartbeatInterval = setInterval(async () => {
try {
const response = await containerSessionService.sendHeartbeat({
session_token: sessionToken,
active_user_count: 3,
metadata: {
cpu_usage: getCPUUsage(),
memory_usage: getMemoryUsage(),
},
});
console.log('Heartbeat sent. Expires at:', response.expires_at);
} catch (error) {
console.error('Heartbeat failed:', error);
if (error.response?.status === 410) {
console.error('Session expired. Stopping heartbeat.');
stopHeartbeat();
}
}
}, intervalSeconds * 1000);
}
function stopHeartbeat() {
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
}
}
// Start heartbeat after validation
const validationResponse = await validateContainer();
startHeartbeat(validationResponse.session_token, validationResponse.heartbeat_interval_seconds);
3. Release Session
Endpoint: POST /api/v1/sessions/release/
Gracefully releases a container session. Call this on container shutdown to immediately free the license seat.
Request
curl -X POST https://api.coditect.ai/api/v1/sessions/release/ \
-H "Authorization: Bearer $JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"session_token": "tok_container_abc123xyz...",
"force": false
}'
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
session_token | string | Yes | Session token from validation response |
force | boolean | No | Force release even if users are active (default: false) |
Response (200 OK)
{
"released": true,
"session_id": "cs_1234567890abcdef",
"message": "Container session released successfully"
}
TypeScript Example
import { containerSessionService } from '@/services/containerSession.service';
async function releaseSession(sessionToken: string) {
try {
const response = await containerSessionService.releaseContainer({
session_token: sessionToken,
force: false,
});
console.log('Session released:', response.session_id);
// Clean up heartbeat
stopHeartbeat();
// Clear stored token
localStorage.removeItem('container_session_token');
} catch (error) {
console.error('Failed to release session:', error);
throw error;
}
}
// Call on container shutdown
window.addEventListener('beforeunload', () => {
const sessionToken = localStorage.getItem('container_session_token');
if (sessionToken) {
releaseSession(sessionToken);
}
});
4. User Join
Endpoint: POST /api/v1/sessions/user/join/
Adds a user to a multi-user container session (e.g., Cloud Workstation).
Request
curl -X POST https://api.coditect.ai/api/v1/sessions/user/join/ \
-H "Authorization: Bearer $JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"session_token": "tok_container_abc123xyz...",
"username": "john.doe",
"display_name": "John Doe",
"metadata": {
"client_ip": "192.168.1.100",
"client_version": "1.0.0"
}
}'
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
session_token | string | Yes | Container session token |
username | string | Yes | Unique username for this user |
display_name | string | No | Human-readable display name |
metadata | object | No | Optional user metadata |
Response (200 OK)
{
"user_session_id": "us_abcdef1234567890",
"user_token": "tok_user_xyz789...",
"username": "john.doe",
"display_name": "John Doe",
"status": "active",
"joined_at": "2026-01-05T12:10:00Z",
"message": "User joined container session successfully"
}
Error Responses
409 Conflict - User Limit Reached:
{
"error": "Container user limit reached",
"code": "USER_LIMIT_REACHED",
"details": {
"max_users": 10,
"current_users": 10
}
}
409 Conflict - User Already in Session:
{
"error": "User already in this container session",
"code": "USER_ALREADY_IN_SESSION",
"details": {
"username": "john.doe"
}
}
TypeScript Example
import { containerSessionService } from '@/services/containerSession.service';
async function joinSession(sessionToken: string, username: string) {
try {
const response = await containerSessionService.userJoin({
session_token: sessionToken,
username: username,
display_name: 'John Doe',
metadata: {
client_ip: '192.168.1.100',
},
});
console.log('User joined:', response.user_session_id);
// Store user_token for user-level operations
localStorage.setItem('user_session_token', response.user_token);
return response;
} catch (error) {
if (error.response?.status === 409) {
const code = error.response?.data?.code;
if (code === 'USER_LIMIT_REACHED') {
console.error('Container is full');
} else if (code === 'USER_ALREADY_IN_SESSION') {
console.error('User already joined');
}
}
throw error;
}
}
5. User Leave
Endpoint: POST /api/v1/sessions/user/leave/
Removes a user from a multi-user container session.
Request
curl -X POST https://api.coditect.ai/api/v1/sessions/user/leave/ \
-H "Authorization: Bearer $JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"user_token": "tok_user_xyz789..."
}'
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
user_token | string | Yes | User token from join response |
Response (200 OK)
{
"left": true,
"user_session_id": "us_abcdef1234567890",
"message": "User left container session successfully"
}
TypeScript Example
import { containerSessionService } from '@/services/containerSession.service';
async function leaveSession(userToken: string) {
try {
const response = await containerSessionService.userLeave({
user_token: userToken,
});
console.log('User left:', response.user_session_id);
// Clear stored token
localStorage.removeItem('user_session_token');
} catch (error) {
console.error('Failed to leave session:', error);
throw error;
}
}
// Call on user disconnect
window.addEventListener('beforeunload', () => {
const userToken = localStorage.getItem('user_session_token');
if (userToken) {
leaveSession(userToken);
}
});
6. List Sessions (Admin)
Endpoint: GET /api/v1/sessions/
Lists all container sessions with optional filtering. Access controlled by role.
Request
curl -X GET "https://api.coditect.ai/api/v1/sessions/?status=active&container_type=cloud_workstation&page=1&page_size=20" \
-H "Authorization: Bearer $JWT_TOKEN"
Query Parameters
| Parameter | Type | Description |
|---|---|---|
status | string | Filter by status: active, expired, released |
container_type | string | Filter by type: docker, cloud_workstation, kubernetes |
license_id | string | Filter by license ID |
tenant_id | string | Filter by tenant ID (System Admin only) |
search | string | Search by container ID, name, or hostname |
page | integer | Page number (default: 1) |
page_size | integer | Results per page (default: 20, max: 100) |
Response (200 OK)
{
"count": 42,
"next": "https://api.coditect.ai/api/v1/sessions/?page=2",
"previous": null,
"results": [
{
"id": "cs_1234567890abcdef",
"container_id": "container-abc123",
"container_type": "cloud_workstation",
"container_name": "my-dev-environment",
"license_id": "lic_xyz789",
"license_key": "CODITECT-XXXX-XXXX-XXXX-XXXX",
"session_token": "tok_container_...",
"status": "active",
"max_users": 10,
"current_user_count": 3,
"hostname": "workstation-01.example.com",
"client_version": "1.0.0",
"last_heartbeat": "2026-01-05T12:14:00Z",
"heartbeat_interval_seconds": 60,
"ttl_seconds": 360,
"created_at": "2026-01-05T10:00:00Z",
"updated_at": "2026-01-05T12:14:00Z",
"expires_at": "2026-01-05T12:20:00Z",
"tenant_id": "ten_abc123",
"tenant_name": "Acme Corporation"
}
]
}
TypeScript Example
import { containerSessionService } from '@/services/containerSession.service';
import type { ContainerSessionFilters } from '@/types/containerSession';
async function listSessions() {
try {
const filters: ContainerSessionFilters = {
status: 'active',
container_type: 'cloud_workstation',
page: 1,
page_size: 20,
};
const response = await containerSessionService.listSessions(filters);
console.log(`Total sessions: ${response.count}`);
console.log(`Sessions on this page: ${response.results.length}`);
response.results.forEach(session => {
console.log(`${session.container_name}: ${session.current_user_count}/${session.max_users} users`);
});
return response;
} catch (error) {
console.error('Failed to list sessions:', error);
throw error;
}
}
7. Get Session (Admin)
Endpoint: GET /api/v1/sessions/{id}/
Retrieves a single container session with nested user sessions.
Request
curl -X GET https://api.coditect.ai/api/v1/sessions/cs_1234567890abcdef/ \
-H "Authorization: Bearer $JWT_TOKEN"
Response (200 OK)
{
"id": "cs_1234567890abcdef",
"container_id": "container-abc123",
"container_type": "cloud_workstation",
"container_name": "my-dev-environment",
"license_id": "lic_xyz789",
"license_key": "CODITECT-XXXX-XXXX-XXXX-XXXX",
"session_token": "tok_container_...",
"status": "active",
"max_users": 10,
"current_user_count": 3,
"hostname": "workstation-01.example.com",
"client_version": "1.0.0",
"last_heartbeat": "2026-01-05T12:14:00Z",
"heartbeat_interval_seconds": 60,
"ttl_seconds": 360,
"created_at": "2026-01-05T10:00:00Z",
"updated_at": "2026-01-05T12:14:00Z",
"expires_at": "2026-01-05T12:20:00Z",
"tenant_id": "ten_abc123",
"tenant_name": "Acme Corporation",
"user_sessions": [
{
"id": "us_abc123",
"container_session_id": "cs_1234567890abcdef",
"user_id": "user_xyz789",
"user_token": "tok_user_...",
"username": "john.doe",
"display_name": "John Doe",
"status": "active",
"metadata": {},
"last_activity": "2026-01-05T12:14:00Z",
"joined_at": "2026-01-05T10:05:00Z",
"left_at": null
}
]
}
TypeScript Example
import { containerSessionService } from '@/services/containerSession.service';
async function getSession(sessionId: string) {
try {
const session = await containerSessionService.getSession(sessionId);
console.log(`Session: ${session.container_name}`);
console.log(`Users: ${session.current_user_count}/${session.max_users}`);
session.user_sessions?.forEach(userSession => {
console.log(` - ${userSession.display_name} (${userSession.status})`);
});
return session;
} catch (error) {
if (error.response?.status === 404) {
console.error('Session not found');
}
throw error;
}
}
8. Terminate Session (Admin)
Endpoint: DELETE /api/v1/sessions/{id}/
Force-terminates a container session. Disconnects all users immediately.
Request
curl -X DELETE https://api.coditect.ai/api/v1/sessions/cs_1234567890abcdef/ \
-H "Authorization: Bearer $JWT_TOKEN"
Response (204 No Content)
No response body. HTTP 204 indicates successful termination.
Error Responses
403 Forbidden - Insufficient Permissions:
{
"error": "You do not have permission to terminate this session",
"code": "PERMISSION_DENIED"
}
404 Not Found:
{
"error": "Container session not found",
"code": "SESSION_NOT_FOUND"
}
TypeScript Example
import { containerSessionService } from '@/services/containerSession.service';
async function terminateSession(sessionId: string) {
try {
await containerSessionService.terminateSession(sessionId);
console.log('Session terminated successfully');
} catch (error) {
if (error.response?.status === 403) {
console.error('Insufficient permissions to terminate session');
} else if (error.response?.status === 404) {
console.error('Session not found');
}
throw error;
}
}
9. Kick User (Admin)
Endpoint: DELETE /api/v1/sessions/{id}/users/{user_id}/
Force-disconnects a specific user from a container session. The container session remains active for other users.
Request
curl -X DELETE https://api.coditect.ai/api/v1/sessions/cs_1234567890abcdef/users/us_abc123/ \
-H "Authorization: Bearer $JWT_TOKEN"
Response (204 No Content)
No response body. HTTP 204 indicates successful kick.
TypeScript Example
import { containerSessionService } from '@/services/containerSession.service';
async function kickUser(containerSessionId: string, userSessionId: string) {
try {
await containerSessionService.kickUser(containerSessionId, userSessionId);
console.log('User kicked successfully');
} catch (error) {
if (error.response?.status === 403) {
console.error('Insufficient permissions to kick user');
} else if (error.response?.status === 404) {
console.error('Session or user not found');
}
throw error;
}
}
10. Statistics
Endpoint: GET /api/v1/sessions/stats/
Returns aggregate statistics for the current tenant's container sessions.
Request
curl -X GET https://api.coditect.ai/api/v1/sessions/stats/ \
-H "Authorization: Bearer $JWT_TOKEN"
Response (200 OK)
{
"total_sessions": 42,
"active_sessions": 35,
"expired_sessions": 5,
"released_sessions": 2,
"total_users": 127,
"active_users": 104,
"by_container_type": {
"docker": 10,
"cloud_workstation": 20,
"kubernetes": 5
},
"by_status": {
"active": 35,
"expired": 5,
"released": 2
},
"avg_users_per_session": 3.6,
"avg_session_duration_minutes": 180.5
}
TypeScript Example
import { containerSessionService } from '@/services/containerSession.service';
async function getStats() {
try {
const stats = await containerSessionService.getStats();
console.log(`Active Sessions: ${stats.active_sessions}`);
console.log(`Active Users: ${stats.active_users}`);
console.log(`Avg Users/Session: ${stats.avg_users_per_session.toFixed(1)}`);
return stats;
} catch (error) {
console.error('Failed to fetch stats:', error);
throw error;
}
}
Common Workflows
Workflow 1: Container Startup and Validation
Code Example:
import { containerSessionService } from '@/services/containerSession.service';
async function startupContainer() {
// Step 1: Validate container
const validationResponse = await containerSessionService.validateContainer({
license_key: process.env.CODITECT_LICENSE_KEY!,
container_id: process.env.HOSTNAME!,
container_type: 'cloud_workstation',
max_users: 10,
});
// Step 2: Store session token
const sessionToken = validationResponse.session_token;
localStorage.setItem('container_session_token', sessionToken);
// Step 3: Start heartbeat loop
const heartbeatInterval = validationResponse.heartbeat_interval_seconds;
startHeartbeat(sessionToken, heartbeatInterval);
console.log('Container started successfully');
}
Workflow 2: User Join Multi-User Container
Code Example:
import { containerSessionService } from '@/services/containerSession.service';
async function userConnect(username: string) {
const containerSessionToken = localStorage.getItem('container_session_token');
if (!containerSessionToken) {
throw new Error('Container session not initialized');
}
// Step 1: User joins session
const userJoinResponse = await containerSessionService.userJoin({
session_token: containerSessionToken,
username: username,
display_name: getUserDisplayName(username),
});
// Step 2: Store user token
const userToken = userJoinResponse.user_token;
localStorage.setItem('user_session_token', userToken);
console.log(`User ${username} joined successfully`);
// Step 3: Setup cleanup on disconnect
window.addEventListener('beforeunload', async () => {
await containerSessionService.userLeave({ user_token: userToken });
});
}
Workflow 3: Heartbeat Loop with Reconnection
import { containerSessionService } from '@/services/containerSession.service';
let heartbeatInterval: NodeJS.Timeout | null = null;
let heartbeatFailureCount = 0;
const MAX_FAILURES = 3;
async function sendHeartbeat(sessionToken: string) {
try {
const response = await containerSessionService.sendHeartbeat({
session_token: sessionToken,
active_user_count: getCurrentUserCount(),
});
// Reset failure count on success
heartbeatFailureCount = 0;
console.log(`Heartbeat sent. Expires at: ${response.expires_at}`);
} catch (error) {
heartbeatFailureCount++;
console.error(`Heartbeat failed (${heartbeatFailureCount}/${MAX_FAILURES}):`, error);
if (error.response?.status === 410) {
// Session expired
console.error('Session expired. Stopping heartbeat.');
stopHeartbeat();
await handleSessionExpiry();
} else if (heartbeatFailureCount >= MAX_FAILURES) {
// Too many failures
console.error('Max heartbeat failures reached. Stopping.');
stopHeartbeat();
await handleHeartbeatFailure();
}
}
}
function startHeartbeat(sessionToken: string, intervalSeconds: number = 60) {
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
}
heartbeatInterval = setInterval(() => {
sendHeartbeat(sessionToken);
}, intervalSeconds * 1000);
// Send initial heartbeat immediately
sendHeartbeat(sessionToken);
}
function stopHeartbeat() {
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
heartbeatInterval = null;
}
}
async function handleSessionExpiry() {
// Attempt to re-validate
try {
const validationResponse = await containerSessionService.validateContainer({
license_key: process.env.CODITECT_LICENSE_KEY!,
container_id: process.env.HOSTNAME!,
container_type: 'cloud_workstation',
});
// Restart heartbeat with new session
startHeartbeat(validationResponse.session_token, validationResponse.heartbeat_interval_seconds);
console.log('Session re-validated successfully');
} catch (error) {
console.error('Failed to re-validate session:', error);
// Shutdown container
process.exit(1);
}
}
async function handleHeartbeatFailure() {
// Log error and attempt recovery
console.error('Heartbeat loop failed. Attempting recovery...');
// Implement retry logic or alert admin
}
Workflow 4: Graceful Container Shutdown
import { containerSessionService } from '@/services/containerSession.service';
async function shutdownContainer() {
const sessionToken = localStorage.getItem('container_session_token');
if (!sessionToken) {
console.warn('No active session to release');
return;
}
try {
// Step 1: Stop heartbeat
stopHeartbeat();
// Step 2: Disconnect all users
const userToken = localStorage.getItem('user_session_token');
if (userToken) {
await containerSessionService.userLeave({ user_token: userToken });
}
// Step 3: Release container session
await containerSessionService.releaseContainer({
session_token: sessionToken,
force: false,
});
// Step 4: Cleanup
localStorage.removeItem('container_session_token');
localStorage.removeItem('user_session_token');
console.log('Container shutdown complete');
} catch (error) {
console.error('Error during shutdown:', error);
// Continue shutdown even if release fails
}
}
// Register shutdown handler
process.on('SIGTERM', shutdownContainer);
process.on('SIGINT', shutdownContainer);
window.addEventListener('beforeunload', shutdownContainer);
Error Handling
Error Response Format
All API errors follow this structure:
{
"error": "Human-readable error message",
"code": "ERROR_CODE",
"details": {
"key": "Additional context"
}
}
Error Codes
| HTTP | Code | Description | Recommended Action |
|---|---|---|---|
| 400 | VALIDATION_ERROR | Invalid request data | Fix request payload |
| 401 | INVALID_LICENSE | Invalid license key | Check license key format |
| 401 | INVALID_SESSION_TOKEN | Invalid or expired session token | Re-validate container |
| 401 | INVALID_USER_TOKEN | Invalid or expired user token | Re-join session |
| 403 | LICENSE_EXPIRED | License has expired | Renew license |
| 403 | PERMISSION_DENIED | Insufficient permissions | Check user role |
| 404 | SESSION_NOT_FOUND | Session doesn't exist | Verify session ID |
| 409 | CONTAINER_LIMIT_REACHED | Too many containers | Release inactive sessions |
| 409 | USER_LIMIT_REACHED | Too many users in container | Wait for users to leave |
| 409 | USER_ALREADY_IN_SESSION | User already joined | Use existing user session |
| 410 | SESSION_EXPIRED | Session expired (TTL) | Re-validate container |
| 429 | RATE_LIMIT_EXCEEDED | Too many requests | Implement backoff |
| 500 | INTERNAL_SERVER_ERROR | Server error | Retry with exponential backoff |
Error Handling Best Practices
import { AxiosError } from 'axios';
import type { ContainerSessionError } from '@/types/containerSession';
async function handleAPICall<T>(apiCall: () => Promise<T>): Promise<T> {
try {
return await apiCall();
} catch (error) {
if (error instanceof AxiosError) {
const apiError = error.response?.data as ContainerSessionError;
switch (apiError.code) {
case 'INVALID_LICENSE':
case 'LICENSE_EXPIRED':
// Critical: Cannot continue
console.error('License validation failed:', apiError.error);
await shutdownContainer();
throw error;
case 'SESSION_EXPIRED':
case 'INVALID_SESSION_TOKEN':
// Recoverable: Re-validate
console.warn('Session expired. Re-validating...');
await revalidateSession();
return await apiCall(); // Retry
case 'CONTAINER_LIMIT_REACHED':
case 'USER_LIMIT_REACHED':
// User action required
console.error('Capacity limit reached:', apiError.error);
throw error;
case 'RATE_LIMIT_EXCEEDED':
// Implement backoff
const retryAfter = parseInt(error.response?.headers['retry-after'] || '60');
console.warn(`Rate limited. Retrying after ${retryAfter}s`);
await sleep(retryAfter * 1000);
return await apiCall(); // Retry
default:
// Unknown error
console.error('API error:', apiError);
throw error;
}
}
// Non-Axios error
throw error;
}
}
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
Rate Limiting
Rate Limits
| Endpoint | Limit | Window | Scope |
|---|---|---|---|
/validate/ | 10 requests | 1 minute | Per license key |
/heartbeat/ | 120 requests | 1 minute | Per session token |
/release/ | 10 requests | 1 minute | Per session token |
/user/join/ | 30 requests | 1 minute | Per session token |
/user/leave/ | 30 requests | 1 minute | Per user token |
| Admin endpoints | 60 requests | 1 minute | Per user |
Rate Limit Headers
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 115
X-RateLimit-Reset: 1736087400
Retry-After: 45
Handling Rate Limits
import { containerSessionService } from '@/services/containerSession.service';
async function sendHeartbeatWithBackoff(sessionToken: string) {
const maxRetries = 3;
let retryCount = 0;
while (retryCount < maxRetries) {
try {
const response = await containerSessionService.sendHeartbeat({
session_token: sessionToken,
});
return response;
} catch (error) {
if (error.response?.status === 429) {
const retryAfter = parseInt(error.response?.headers['retry-after'] || '60');
console.warn(`Rate limited. Retrying after ${retryAfter}s (attempt ${retryCount + 1}/${maxRetries})`);
await sleep(retryAfter * 1000);
retryCount++;
} else {
throw error;
}
}
}
throw new Error('Max retries exceeded for rate limit');
}
Multi-Tenant Considerations
Tenant Isolation
All API responses are automatically filtered by the authenticated user's tenant. Requests cannot access data from other tenants.
| Role | Access Scope |
|---|---|
| System Admin | All tenants (requires tenant_id filter) |
| Tenant Admin | Own tenant only |
| Team Manager | Own team's sessions only |
| User | Own sessions only |
Tenant Context Headers
X-Tenant-ID: ten_abc123
X-Tenant-Name: Acme Corporation
System Admin Cross-Tenant Access
System admins can query across tenants by specifying tenant_id:
curl -X GET "https://api.coditect.ai/api/v1/sessions/?tenant_id=ten_xyz789" \
-H "Authorization: Bearer $SYSTEM_ADMIN_JWT_TOKEN"
TypeScript Example:
import { containerSessionService } from '@/services/containerSession.service';
import { useAuth } from '@/hooks/useAuth';
async function listSessionsForTenant(tenantId?: string) {
const { isSystemAdmin } = useAuth();
if (!isSystemAdmin && tenantId) {
throw new Error('Only System Admins can filter by tenant_id');
}
const filters = {
tenant_id: isSystemAdmin ? tenantId : undefined,
status: 'active',
};
return await containerSessionService.listSessions(filters);
}
TypeScript Integration
Complete Integration Example
// src/lib/container-session.ts
import { containerSessionService } from '@/services/containerSession.service';
import type {
ContainerValidateRequest,
ContainerValidateResponse,
ContainerHeartbeatRequest,
} from '@/types/containerSession';
class ContainerSessionManager {
private sessionToken: string | null = null;
private heartbeatInterval: NodeJS.Timeout | null = null;
private heartbeatIntervalSeconds: number = 60;
/**
* Initialize container session
*/
async initialize(licenseKey: string, containerId: string): Promise<void> {
const request: ContainerValidateRequest = {
license_key: licenseKey,
container_id: containerId,
container_type: 'cloud_workstation',
max_users: 10,
};
const response: ContainerValidateResponse = await containerSessionService.validateContainer(request);
this.sessionToken = response.session_token;
this.heartbeatIntervalSeconds = response.heartbeat_interval_seconds;
// Store in localStorage
localStorage.setItem('container_session_token', this.sessionToken);
// Start heartbeat
this.startHeartbeat();
console.log('Container session initialized:', response.session_id);
}
/**
* Start heartbeat loop
*/
private startHeartbeat(): void {
if (!this.sessionToken) {
throw new Error('Cannot start heartbeat without session token');
}
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
}
this.heartbeatInterval = setInterval(() => {
this.sendHeartbeat();
}, this.heartbeatIntervalSeconds * 1000);
// Send initial heartbeat immediately
this.sendHeartbeat();
}
/**
* Send heartbeat
*/
private async sendHeartbeat(): Promise<void> {
if (!this.sessionToken) return;
try {
const request: ContainerHeartbeatRequest = {
session_token: this.sessionToken,
active_user_count: this.getCurrentUserCount(),
};
const response = await containerSessionService.sendHeartbeat(request);
console.log(`Heartbeat sent. Expires at: ${response.expires_at}`);
} catch (error) {
console.error('Heartbeat failed:', error);
if (error.response?.status === 410) {
// Session expired - attempt re-validation
await this.handleSessionExpiry();
}
}
}
/**
* Handle session expiry
*/
private async handleSessionExpiry(): Promise<void> {
console.warn('Session expired. Attempting re-validation...');
try {
const licenseKey = process.env.CODITECT_LICENSE_KEY!;
const containerId = process.env.HOSTNAME!;
await this.initialize(licenseKey, containerId);
} catch (error) {
console.error('Re-validation failed:', error);
process.exit(1);
}
}
/**
* Get current user count
*/
private getCurrentUserCount(): number {
// Implement your logic to track active users
return 1;
}
/**
* Release session
*/
async release(): Promise<void> {
if (!this.sessionToken) return;
// Stop heartbeat
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
try {
await containerSessionService.releaseContainer({
session_token: this.sessionToken,
});
console.log('Container session released');
} catch (error) {
console.error('Failed to release session:', error);
}
// Cleanup
this.sessionToken = null;
localStorage.removeItem('container_session_token');
}
}
// Singleton instance
export const containerSession = new ContainerSessionManager();
// Initialize on container start
if (process.env.NODE_ENV === 'production') {
const licenseKey = process.env.CODITECT_LICENSE_KEY;
const containerId = process.env.HOSTNAME;
if (licenseKey && containerId) {
containerSession.initialize(licenseKey, containerId);
}
}
// Release on shutdown
process.on('SIGTERM', () => containerSession.release());
process.on('SIGINT', () => containerSession.release());
Testing
Unit Tests
// src/services/__tests__/containerSession.service.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { containerSessionService } from '@/services/containerSession.service';
import api from '@/lib/api';
vi.mock('@/lib/api');
describe('containerSessionService', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('validateContainer', () => {
it('should validate container successfully', async () => {
const mockResponse = {
session_id: 'cs_123',
session_token: 'tok_abc',
status: 'active',
license_key: 'CODITECT-XXXX',
max_users: 10,
current_user_count: 0,
heartbeat_interval_seconds: 60,
ttl_seconds: 360,
expires_at: '2026-01-05T12:00:00Z',
message: 'Success',
};
vi.mocked(api.post).mockResolvedValueOnce({ data: mockResponse });
const result = await containerSessionService.validateContainer({
license_key: 'CODITECT-XXXX',
container_id: 'container-123',
});
expect(result).toEqual(mockResponse);
expect(api.post).toHaveBeenCalledWith('/sessions/validate/', {
license_key: 'CODITECT-XXXX',
container_id: 'container-123',
});
});
it('should handle invalid license error', async () => {
vi.mocked(api.post).mockRejectedValueOnce({
response: {
status: 401,
data: {
error: 'Invalid license key',
code: 'INVALID_LICENSE',
},
},
});
await expect(
containerSessionService.validateContainer({
license_key: 'INVALID',
container_id: 'container-123',
})
).rejects.toThrow();
});
});
describe('sendHeartbeat', () => {
it('should send heartbeat successfully', async () => {
const mockResponse = {
status: 'active',
expires_at: '2026-01-05T12:01:00Z',
current_user_count: 3,
message: 'Heartbeat received',
};
vi.mocked(api.post).mockResolvedValueOnce({ data: mockResponse });
const result = await containerSessionService.sendHeartbeat({
session_token: 'tok_abc',
active_user_count: 3,
});
expect(result).toEqual(mockResponse);
});
});
});
Integration Tests
// src/services/__tests__/containerSession.integration.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { containerSessionService } from '@/services/containerSession.service';
describe('Container Session Integration', () => {
let sessionToken: string;
beforeAll(async () => {
// Validate container
const response = await containerSessionService.validateContainer({
license_key: process.env.TEST_LICENSE_KEY!,
container_id: 'test-container-' + Date.now(),
container_type: 'docker',
});
sessionToken = response.session_token;
});
afterAll(async () => {
// Release session
if (sessionToken) {
await containerSessionService.releaseContainer({
session_token: sessionToken,
});
}
});
it('should complete full session lifecycle', async () => {
// Send heartbeat
const heartbeatResponse = await containerSessionService.sendHeartbeat({
session_token: sessionToken,
});
expect(heartbeatResponse.status).toBe('active');
// Release session
const releaseResponse = await containerSessionService.releaseContainer({
session_token: sessionToken,
});
expect(releaseResponse.released).toBe(true);
});
});
Appendix: API Endpoint Summary
| Endpoint | Method | Purpose | Auth Required |
|---|---|---|---|
/validate/ | POST | Validate license and create session | Yes |
/heartbeat/ | POST | Send heartbeat to keep session alive | Yes |
/release/ | POST | Release container session | Yes |
/user/join/ | POST | User joins multi-user container | Yes |
/user/leave/ | POST | User leaves multi-user container | Yes |
/ | GET | List container sessions (admin) | Yes |
/{id}/ | GET | Get single container session (admin) | Yes |
/{id}/ | DELETE | Terminate container session (admin) | Yes (admin) |
/{id}/users/{user_id}/ | DELETE | Kick user from session (admin) | Yes (admin) |
/stats/ | GET | Get session statistics | Yes |
Related Documentation
- ADR-055: Licensed Docker Container Schema Design
- ADR-056: Container Session UI Architecture
- Container Session Service (TypeScript)
- Container Session Types (TypeScript)
Compliance: CODITECT Documentation Standard v1.0.0 Owner: AZ1.AI INC Lead: Hal Casteel Version: 1.0.0 Last Updated: January 5, 2026