A card game with RPCs

Card games are the clean case for server authority: the deck is secret, the rules are strict, and no client should be trusted to enforce either. State is server-owned; every move is an RPC.

Schema

export const schema = defineSchema({
  match: entity({ turn: string(), phase: enumOf(['deal', 'play', 'over']) }),
  hands: map(list(string())),   // per-player, only sent to its owner
  pile: list(string()),
}, { serverOwned: true });

Hands use per-owner visibility — a client only receives its own hand, so the secret never reaches other browsers.

Moves as RPCs

export const rpc = defineRpc({
  play: { args: { card: string() }, returns: { ok: boolean() } },
  draw: { args: {} },
});
export default defineRoom(schema, {
  rpc: {
    play(state, { card }, ctx) {
      if (state.match.turn !== ctx.clientId) return { ok: false };
      const hand = state.hands[ctx.clientId];
      if (!hand.includes(card)) return { ok: false };
      hand.splice(hand.indexOf(card), 1);
      state.pile.push(card);
      state.match.turn = nextPlayer(state, ctx.clientId);
      return { ok: true };
    },
  },
});

Client

async function onPlay(card: string) {
  const { ok } = await room.rpc.play({ card });
  if (!ok) nudge();
}
room.subscribe((s) => renderTable(s.pile, s.hands[room.me], s.match.turn));

Turn order, legality, and secrecy are all enforced in one place the client can’t bypass.

Next steps

Placeholder guide.