Skip to content

WebSockets

VentureKit supports real-time WebSocket APIs alongside REST APIs in the same project, with built-in connection management via DynamoDB.

Add WebSocket configuration to your environment config:

config/dev.ts
import type { EnvConfigInput } from '@venturekit/core';
export const dev: EnvConfigInput = {
preset: 'nano',
websocket: {
enabled: true,
routeSelectionExpression: '$request.body.action',
idleTimeoutSec: 600, // 10 minutes (max: 7200)
throttleRateLimit: 100,
throttleBurstLimit: 200,
},
};

VentureKit uses two-phase authentication for WebSockets. JWTs are never sent in query parameters (which appear in logs), but instead over the already-encrypted WebSocket channel:

1. Client connects: wss://api.example.com (no credentials in URL)
2. Server saves connection as unauthenticated (30s TTL)
3. Client sends: { "action": "auth", "token": "<jwt>" }
4. Server verifies JWT, upgrades connection (TTL extended)
5. Server responds: { "action": "auth", "success": true }

If the client doesn’t authenticate within 30 seconds, the connection record expires automatically.

Create three handler files in src/ws/:

RouteFilePurpose
$connectsrc/ws/connect.tsSave unauthenticated connection
$disconnectsrc/ws/disconnect.tsRemove connection record
$defaultsrc/ws/default.tsHandle auth, messages, broadcast
src/ws/connect.ts
import { connectionStore } from '@venturekit/runtime';
export const handler = async (event: any) => {
const connectionId = event.requestContext.connectionId;
await connectionStore.save(connectionId);
return { statusCode: 200 };
};
src/ws/disconnect.ts
import { connectionStore } from '@venturekit/runtime';
export const handler = async (event: any) => {
const connectionId = event.requestContext.connectionId;
await connectionStore.remove(connectionId);
return { statusCode: 200 };
};
src/ws/default.ts
import { connectionStore } from '@venturekit/runtime';
export const handler = async (event: any) => {
const connectionId = event.requestContext.connectionId;
const body = JSON.parse(event.body);
const { domainName, stage } = event.requestContext;
switch (body.action) {
case 'auth':
const user = await verifyJwt(body.token);
await connectionStore.authenticate(connectionId, {
userId: user.sub,
email: user.email,
tenantId: body.tenantId,
});
await connectionStore.postToConnection(domainName, stage, connectionId, {
action: 'auth', success: true, userId: user.sub,
});
break;
case 'ping':
await connectionStore.postToConnection(domainName, stage, connectionId, {
action: 'pong', timestamp: new Date().toISOString(),
});
break;
case 'broadcast':
await connectionStore.broadcast(domainName, stage, body.data);
break;
case 'send':
await connectionStore.sendToUser(domainName, stage, body.to, body.data);
break;
}
return { statusCode: 200 };
};

All connection management is in @venturekit/runtime:

import { connectionStore } from '@venturekit/runtime';
// Lifecycle
await connectionStore.save(connectionId);
await connectionStore.authenticate(connectionId, { userId, email, tenantId });
await connectionStore.remove(connectionId);
// Messaging
await connectionStore.postToConnection(domain, stage, connectionId, data);
await connectionStore.sendToUser(domain, stage, userId, data);
await connectionStore.sendToTenant(domain, stage, tenantId, data);
await connectionStore.broadcast(domain, stage, data);
// Queries
const conn = await connectionStore.get(connectionId);
const userConns = await connectionStore.getByUser(userId);
const tenantConns = await connectionStore.getByTenant(tenantId);

Connection state is stored in DynamoDB:

FieldTypeDescription
connectionIdString (PK)API Gateway connection ID
authenticatedBooleanWhether authenticated
userIdStringUser ID from JWT
tenantIdStringTenant ID (optional)
emailStringUser email
connectedAtNumberUnix timestamp (ms)
ttlNumberDynamoDB TTL (epoch seconds)

Required GSIs:

GSIPartition KeyPurpose
userId-indexuserIdgetByUser(), sendToUser()
tenantId-indextenantIdgetByTenant(), sendToTenant()
{ "action": "auth", "token": "<jwt>" }
{ "action": "auth", "token": "<jwt>", "tenantId": "acme" }
{ "action": "ping" }
{ "action": "broadcast", "data": { "text": "Hello!" } }
{ "action": "send", "to": "<userId>", "data": { "text": "Hello!" } }