PlayMesh/Docs
HomeGitHubnpm

Examples

Full working examples showing common PlayMesh patterns.

Chat server

The canonical PlayMesh example — a multiplayer chat lobby. Included inexamples/chat in the repository.

server.ts

examples/chat/server.ts
import { PlayMesh } from '@playmesh/server';

const port = Number(process.env.PORT ?? 4000);
const mesh = new PlayMesh({ port });

mesh.onAuthenticate(async request => {
  const username = String(request.auth.username ?? '').trim();
  if (!username) throw new Error('A username is required');
  return { userId: username };
});

mesh.bootstrap(async ({ mesh }) => {
  mesh.createDomain('social').createInstance('lobby');
});

mesh.onAdmission(async () => ({ instances: ['social/lobby'] }));

mesh.onStarted(() => {
  const lobby = mesh.domain('social').instance('lobby');

  lobby.onJoin(session => {
    lobby.broadcast('system', { text: `${session.userId} joined` });
  });
  lobby.onLeave(session => {
    lobby.broadcast('system', { text: `${session.userId} left` });
  });
  lobby.on('chat', (session, payload) => {
    lobby.broadcast('chat', {
      sender: session.userId,
      message: (payload as { message: string }).message,
    });
  });
});

await mesh.start();

client.ts

examples/chat/client.ts
import { createInterface } from 'node:readline';
import { PlayMeshClient } from '@playmesh/client';

const username = process.argv[2] ?? `guest-${Math.floor(Math.random() * 1000)}`;

const client = new PlayMeshClient({
  url: `http://localhost:${process.env.PORT ?? 4000}`,
  auth: { username },
});

client.on('chat', payload => {
  const { sender, message } = payload as { sender: string; message: string };
  console.log(`${sender}: ${message}`);
});
client.on('system', payload => {
  console.log(`* ${(payload as { text: string }).text}`);
});

const session = await client.connect();
console.log(`Connected as ${session.userId}`);

const rl = createInterface({ input: process.stdin });
rl.on('line', line => {
  if (line.trim()) client.emit('chat', { message: line.trim() });
});

Run it

cd examples/chat && npm install

# Terminal 1
npx tsx server.ts

# Terminal 2
npx tsx client.ts alice

# Terminal 3
npx tsx client.ts bob

Multi-zone world

Players move between zones. Each zone is an instance. A player is always in exactly one zone.
const ZONES = ['forest', 'village', 'dungeon', 'market'];

mesh.bootstrap(async ({ mesh }) => {
  const world = mesh.createDomain('world');

  for (const zone of ZONES) {
    const instance = world.createInstance(zone);

    instance.onJoin(session => {
      instance.broadcast('player-entered', { userId: session.userId, zone });
    });
    instance.onLeave(session => {
      instance.broadcast('player-left', { userId: session.userId, zone });
    });
    instance.on('chat', (session, payload) => {
      instance.broadcast('chat', { zone, sender: session.userId, ...payload });
    });
  }
});

mesh.onAdmission(async request => {
  const player = await db.players.findById(request.userId);
  return { instances: [`world/${player.currentZone ?? 'village'}`] };
});

mesh.onStarted(() => {
  const world = mesh.domain('world');

  for (const instance of world.instances) {
    instance.on('travel-to', async (session, payload) => {
      const { zone } = payload as { zone: string };
      if (!ZONES.includes(zone)) return;

      for (const current of session.instances) {
        if (current.domain === world) await session.leave(current);
      }

      await session.join(world.instance(zone));
      await db.players.setZone(session.userId, zone);
    });
  }
});

Guild chat (multi-instance membership)

Players are in the world zone and their guild chat simultaneously.
mesh.bootstrap(async ({ mesh }) => {
  const world = mesh.createDomain('world');
  const guilds = mesh.createDomain('guild');

  world.createInstance('city');

  const allGuilds = await db.guilds.findAll();
  for (const guild of allGuilds) {
    const instance = guilds.createInstance(guild.id);
    instance.on('guild-chat', (session, payload) => {
      instance.broadcast('guild-chat', {
        sender: session.userId,
        guildId: guild.id,
        message: (payload as { message: string }).message,
      });
    });
  }
});

mesh.onAdmission(async request => {
  const player = await db.players.findById(request.userId);
  return {
    instances: [
      'world/city',
      ...(player.guildId ? [`guild/${player.guildId}`] : []),
    ],
  };
});

Matchmaking with queues

Uses BullMQ to form 2-player matches. Requires Redis.
const mesh = new PlayMesh({
  port: 4000,
  redis: { host: 'localhost', port: 6379 },
});

mesh.bootstrap(async ({ mesh }) => {
  const ranked = mesh.createDomain('ranked');
  const lobby = ranked.createInstance('lobby');

  lobby.on('join-queue', async (session, payload) => {
    await mesh.queues.queue('matchmaking').add('find-match', {
      userId: session.userId,
      rank: (payload as { rank: number }).rank,
    });
    session.send('queued', {});
  });
});

mesh.onStarted(() => {
  mesh.queues.worker('matchmaking', async job => {
    const { userId, rank } = job.data;
    const opponent = await matchmakingService.findOpponent(userId, rank);
    if (!opponent) return;

    const ranked = mesh.domain('ranked');
    const match = ranked.createInstance(`match-${Date.now()}`);

    for (const uid of [userId, opponent.userId]) {
      const s = mesh.sessions.find(s => s.userId === uid);
      if (s) {
        await s.leave(ranked.instance('lobby'));
        await s.join(match);
      }
    }

    match.broadcast('match-started', { players: [userId, opponent.userId] });
  });
});

await mesh.start();
For production matchmaking, use a skill-based algorithm and a dedicated service. This example illustrates the queue integration pattern only.