Relay & Durable Objects

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.