Enforcing Per-User Room Limits Without a Database
TermOnMac enforces per-user limits on how many concurrent Mac rooms a user can register. Free tier: 4 rooms. Pro/Premium: 32 rooms. The implementation uses Cloudflare KV prefix scans — no separate database, no counters to keep synchronized.
The Room Registration Key Pattern
When a Mac registers a room, a KV entry is written:
// relay_server/src/room.ts
await this.env.AUTH_KV.put(
`room_reg:${userId}:${roomId}`,
JSON.stringify({ do_hex_id: doHexId, created_at: Date.now(), last_seen: Date.now() }),
{ expirationTtl: ROOM_REG_TTL }, // 15 minutes
);
Key pattern: room_reg:{userId}:{roomId} → { do_hex_id, created_at, last_seen }
The TTL is 15 minutes (ROOM_REG_TTL = 900 seconds), refreshed by the Room DO alarm while the Mac is connected:
// Refresh room_reg TTL while DO is alive
if (regUserId && regRoomId) {
await this.env.AUTH_KV.put(
`room_reg:${regUserId}:${regRoomId}`,
JSON.stringify({ do_hex_id: doHexId, created_at: Date.now(), last_seen: Date.now() }),
{ expirationTtl: ROOM_REG_TTL },
);
}
When a Mac disconnects and doesn’t reconnect, the KV entry expires after 15 minutes and the room slot is released automatically. No cleanup job needed.
Counting Rooms with a Prefix Scan
The room limit check counts existing registrations by listing all keys with the user’s prefix:
// relay_server/src/tier.ts
export async function checkRoomLimit(
kv: KVNamespace,
userId: string,
roomId: string,
tier: UserTier = "free",
): Promise<RoomLimitResult> {
const config = await getTierConfig(kv);
const limits = getLimitsForTier(config, tier);
// List active room registrations for this user
const result = await kv.list({ prefix: `room_reg:${userId}:` });
const currentRooms = result.keys.length;
// Reconnection: if this room already exists, always allow
const existingKey = `room_reg:${userId}:${roomId}`;
const isReconnection = result.keys.some((k) => k.name === existingKey);
if (isReconnection) {
return { allowed: true, current: currentRooms, limit: limits.max_rooms, tier };
}
return {
allowed: currentRooms < limits.max_rooms,
current: currentRooms,
limit: limits.max_rooms,
tier,
};
}
The prefix scan room_reg:{userId}: returns all rooms registered to that user. If the count is below the tier limit, the new registration is allowed.
Reconnection is Always Allowed
If room_reg:{userId}:{roomId} already exists in KV, the connection is a reconnection — the Mac is coming back online after a disconnect. Reconnections bypass the room count check:
const isReconnection = result.keys.some((k) => k.name === existingKey);
if (isReconnection) {
return { allowed: true, current: currentRooms, limit: limits.max_rooms, tier };
}
Without this, a Mac that briefly lost network connectivity would be blocked from reconnecting if the user was at the room limit.
Tier Configuration is Runtime-Configurable
The tier limits are not hardcoded — they’re stored in KV and can be updated without a deployment:
// relay_server/src/tier.ts
const DEFAULT_TIER_CONFIG: TierConfig = {
tiers: {
free: { max_rooms: 4, max_sessions: -1, credits_per_window: 1_000 },
pro: { max_rooms: 32, max_sessions: -1, credits_per_window: 10_000 },
premium: { max_rooms: 32, max_sessions: -1, credits_per_window: 50_000 },
},
updated_at: 0,
};
export async function getTierConfig(kv: KVNamespace): Promise<TierConfig> {
const json = await kv.get(TIER_CONFIG_KEY);
if (!json) return DEFAULT_TIER_CONFIG;
return JSON.parse(json);
}
max_sessions: -1 means unlimited sessions for all tiers at the relay level. Session limits are enforced by the Mac agent, based on the max_sessions value returned in the room_registered message.