Skip to main content

Managing Client Connections

Detect and respond to client connections and disconnections in real time from the backend.

Why Use Client Connections

You're building a feature that depends on knowing whether a user is online: a chat app showing presence indicators, a collaborative editor tracking active participants, or a game lobby displaying connected players.

Without connection tracking, you'd need to implement heartbeat polling or custom WebSocket logic. With Squid's client connection handler, you decorate a single function and react to state changes as they happen:

// Backend: react to connection changes
@clientConnectionStateHandler()
async onConnectionChange(clientId: ClientId, state: ClientConnectionState): Promise<void> {
if (state === 'DISCONNECTED') {
await this.squid.collection('presence').doc(clientId).update({ status: 'offline' });
}
}

// Frontend: check connection status
const isConnected = squid.connectionDetails().connected;

No polling. No custom WebSocket management. Just a decorator.

Overview

Client connections let your backend detect when clients connect to, disconnect from, or are removed from the Squid server. Each client is assigned a unique clientId on connection, which you can use to track presence, clean up resources, or trigger workflows.

When to use client connections

Use CaseRecommendation
Track user online/offline statusClient connections
Clean up resources when a user leavesClient connections
React to database changesUse Triggers
Run code on a scheduleUse Schedulers

How it works

  1. A client connects to Squid and is assigned a unique clientId
  2. Your backend handler is called with state CONNECTED
  3. If the client disconnects (e.g., closes the browser tab), the handler is called with DISCONNECTED
  4. After a period, if the client does not reconnect, Squid removes the clientId and calls the handler with REMOVED
  5. If the client reconnects after removal, they receive a new clientId

Quick Start

Prerequisites

  • A Squid backend project initialized with squid init-backend
  • The @squidcloud/backend package installed

Step 1: Create a connection handler

Add a connection state handler to your service class:

Backend code
import { SquidService, clientConnectionStateHandler } from '@squidcloud/backend';
import { ClientConnectionState, ClientId } from '@squidcloud/client';

export class ExampleService extends SquidService {
@clientConnectionStateHandler()
async onConnectionChange(clientId: ClientId, state: ClientConnectionState): Promise<void> {
console.log(`Client ${clientId} is now ${state}`);
}
}

Step 2: Start or deploy the backend

For local development, run the backend locally using the Squid CLI:

squid start

To deploy to the cloud, see deploying your backend.

Step 3: Observe connection status on the client

Client code
import { filter } from 'rxjs';

// Check if currently connected
const isConnected = squid.connectionDetails().connected;

// Get the current client's ID
const clientId = squid.connectionDetails().clientId;

// React to connection changes
squid
.connectionDetails()
.observeConnected()
.pipe(filter(Boolean))
.subscribe(() => {
console.log('Connected with client ID:', squid.connectionDetails().clientId);
});

Core Concepts

Connection states

Each client transitions through these states:

StateMeaning
CONNECTEDThe client just connected to the server
DISCONNECTEDThe client disconnected, but Squid retains the clientId in case the client reconnects
REMOVEDThe client has been disconnected long enough that Squid discarded the clientId. The next connection will receive a new ID.

Client IDs

Every client receives a unique clientId on connection. This ID is:

  • Available on the frontend via squid.connectionDetails().clientId
  • Available on the backend via this.context.clientId (in any backend function, not just connection handlers)
  • Stable across brief disconnects (the same ID is retained until REMOVED)
  • Not the same as a userId from your auth provider. You must map between them if needed.

The @clientConnectionStateHandler decorator

The decorated function receives two arguments:

ParameterTypeDescription
clientIdClientId (string)The ID of the client whose state changed
connectionStateClientConnectionStateOne of 'CONNECTED', 'DISCONNECTED', or 'REMOVED'

The function can return void or Promise<void>.

Frontend connection API

Access connection information via squid.connectionDetails():

Property / MethodReturn TypeDescription
.connectedbooleanWhether the client is currently connected
.clientIdstringThe unique client ID assigned to this connection
.observeConnected()Observable<boolean>Emits true on connect and false on disconnect
note

The Squid client establishes its WebSocket connection lazily, meaning it only connects to the server when an operation requires it (such as subscribing to a query with .snapshots()). Calling .connected or .observeConnected() alone does not initiate the connection. If you need to ensure the client is connected before checking status, first subscribe to data or perform an operation that triggers the connection.

Error Handling

Handler errors

If your @clientConnectionStateHandler function throws an error, it is logged server-side but does not affect the client's connection. Make sure to handle errors within your handler to avoid silent failures:

Backend code
@clientConnectionStateHandler()
async onConnectionChange(clientId: ClientId, state: ClientConnectionState): Promise<void> {
try {
await this.updatePresence(clientId, state);
} catch (error) {
console.error(`Failed to update presence for ${clientId}:`, error);
}
}

Common issues

IssueCauseSolution
Handler not calledService not exported or backend not deployedEnsure service is exported in service/index.ts and run squid deploy
clientId changes unexpectedlyClient was removed after prolonged disconnectUse REMOVED state to clean up, and re-map on CONNECTED
observeConnected() throwsClient is in passive modeEnsure the Squid client is initialized in active (default) mode

Best Practices

  1. Map clientId to userId on connect. Since clientId is connection-scoped and userId is auth-scoped, insert a mapping when the client connects so you can look up users by connection.
  2. Clean up on REMOVED, not just DISCONNECTED. A DISCONNECTED client may reconnect with the same clientId. Only delete resources when the state is REMOVED.
  3. Keep handler logic fast. Connection state handlers run for every connect/disconnect across all clients. Avoid expensive operations or chain them asynchronously.
  4. Use authentication to identify users. The clientId alone does not tell you who the user is. Combine with auth to build presence features.

Code Examples

User presence tracking

This example tracks which users are online by mapping clientId to userId in a presence collection.

Frontend: register presence on connect

Client code
import { filter } from 'rxjs';

interface PresenceData {
userId: string;
status: 'online' | 'offline';
}

// When connected, insert a presence record linking clientId to userId
squid
.connectionDetails()
.observeConnected()
.pipe(filter(Boolean))
.subscribe(() => {
const clientId = squid.connectionDetails().clientId;
squid.collection<PresenceData>('presence').doc(clientId).insert({ userId: currentUserId, status: 'online' });
});

Backend: update presence on disconnect/removal

Backend code
import { SquidService, clientConnectionStateHandler } from '@squidcloud/backend';
import { ClientConnectionState, ClientId } from '@squidcloud/client';

export class PresenceService extends SquidService {
@clientConnectionStateHandler()
async handlePresenceChange(clientId: ClientId, state: ClientConnectionState): Promise<void> {
const presenceRef = this.squid.collection('presence').doc(clientId);

if (state === 'DISCONNECTED') {
await presenceRef.update({ status: 'offline' });
} else if (state === 'REMOVED') {
await presenceRef.delete();
}
}
}

Frontend: query online users

Client code
// Get all online users
const onlineUsers = await squid.collection<PresenceData>('presence').query().eq('status', 'online').dereference().snapshot();

console.log(
'Online users:',
onlineUsers.map((u) => u.userId)
);

For more on querying, see Queries.

See Also