Skip to content
fvtt-sockets logo

Foundry VTT Sockets & Multiplayer

fvtt-sockets

This skill should be used when implementing multiplayer synchronization, using game.socket.emit/on, creating executeAsGM patterns for privileged operations, broadcasting events between clients, or avoiding common pitfalls like race conditions and duplicate execution.

SKILL.md

Full skill instructions

Foundry VTT Sockets & Multiplayer

Domain: Foundry VTT Module/​System Development Status: Production-Ready Last Updated: 2026-01-04

Overview

Foundry VTT uses Socket.io for real-time communication between server and clients. Understanding socket patterns is essential for multiplayer-safe code.

When to Use This Skill

  • Broadcasting events to other connected clients
  • Implementing GM-delegated operations for players
  • Synchronizing non-document state across clients
  • Creating animations/​effects visible to all players
  • Avoiding duplicate execution in hooks

Socket Setup

Manifest Configuration

Request socket access in your manifest:

{
  "id": "my-module",
  "socket": true
}

Event Naming

Each package gets ONE event namespace:

  • Modules: module.{module-id}
  • Systems: system.{system-id}

Multiplex event types with structured data:

const SOCKET_NAME = "module.my-module";

game.socket.emit(SOCKET_NAME, {
  type: "playAnimation",
  payload: { tokenId: "abc123", effect: "fire" }
});

Registration Timing

Register listeners after game.socket is available:

Hooks.once("init", () => {
  game.socket.on("module.my-module", handleSocketMessage);
});

function handleSocketMessage(data) {
  switch (data.type) {
    case "playAnimation":
      playTokenAnimation(data.payload);
      break;
    case "syncState":
      updateLocalState(data.payload);
      break;
  }
}

Basic Socket Patterns

Emit to All Other Clients

function broadcastAnimation(tokenId, effect) {
  game.socket.emit("module.my-module", {
    type: "playAnimation",
    tokenId,
    effect
  });
}

Critical: Emitting client does NOT receive its own broadcast.

Self-Invoke Pattern

Always call handler locally when emitting:

function triggerEffect(tokenId, effect) {
  const data = { type: "effect", tokenId, effect };

  // Execute locally
  handleEffect(data);

  // Broadcast to others
  game.socket.emit("module.my-module", data);
}

function handleEffect(data) {
  const token = canvas.tokens.get(data.tokenId);
  token?.animate({ alpha: 0.5 }, { duration: 500 });
}

// Socket listener (for other clients)
Hooks.once("init", () => {
  game.socket.on("module.my-module", (data) => {
    if (data.type === "effect") handleEffect(data);
  });
});

ExecuteAsGM Pattern

Players often need GM-authorized operations (damage enemies, modify world data).

Native Socket Approach

const SOCKET_NAME = "module.my-module";

Hooks.once("init", () => {
  game.socket.on(SOCKET_NAME, async (data) => {
    // Only active GM handles this
    if (game.user !== game.users.activeGM) return;

    if (data.type === "damageActor") {
      const actor = game.actors.get(data.actorId);
      if (actor) {
        const newHp = actor.system.hp.value - data.damage;
        await actor.update({ "system.hp.value": Math.max(0, newHp) });
      }
    }
  });
});

// Player calls this
function requestDamage(actorId, damage) {
  game.socket.emit(SOCKET_NAME, {
    type: "damageActor",
    actorId,
    damage
  });
}

Limitations:

  • No return value
  • Manual GM check required
  • Fails silently if no GM connected

Socketlib Approach (Recommended)

Socketlib handles multiple GMs, return values, and error cases.

Dependency (module.json):

{
  "relationships": {
    "requires": [{
      "id": "socketlib",
      "type": "module"
    }]
  }
}

Registration:

let socket;

Hooks.once("socketlib.ready", () => {
  socket = socketlib.registerModule("my-module");

  // Register callable functions
  socket.register("damageActor", damageActor);
  socket.register("getActorData", getActorData);
});

async function damageActor(actorId, damage) {
  const actor = game.actors.get(actorId);
  if (!actor) return { success: false, error: "Actor not found" };

  const newHp = Math.max(0, actor.system.hp.value - damage);
  await actor.update({ "system.hp.value": newHp });
  return { success: true, newHp };
}

function getActorData(actorId) {
  return game.actors.get(actorId)?.toObject() ?? null;
}

Usage:

// Execute on GM client, get return value
async function applyDamage(actorId, damage) {
  try {
    const result = await socket.executeAsGM("damageActor", actorId, damage);
    if (result.success) {
      ui.notifications.info(`Damage applied. HP now: ${result.newHp}`);
    }
  } catch (error) {
    ui.notifications.error("No GM connected to process damage");
  }
}

Socketlib Methods

MethodTargetAwaitableUse Case
executeAsGM(fn, ...args)One GMYesPrivileged operations
executeAsUser(fn, userId, ...args)Specific userYesPlayer-specific actions
executeForEveryone(fn, ...args)All clientsNoBroadcast effects
executeForOthers(fn, ...args)All except selfNoSync without local call
executeForAllGMs(fn, ...args)All GMsNoGM notifications
executeForUsers(fn, ids[], ...args)Listed usersNoTargeted messages

ExecuteForEveryone Example

// Trigger animation on ALL clients
function playGlobalEffect(effectData) {
  socket.executeForEveryone("renderEffect", effectData);
}

// Registered function
function renderEffect(data) {
  canvas.effects.playEffect(data);
}

ExecuteAsUser Example

// Ask specific player for input
async function promptPlayer(userId, question) {
  try {
    return await socket.executeAsUser("showDialog", userId, question);
  } catch {
    return null; // Player disconnected
  }
}

// Registered function
async function showDialog(question) {
  return new Promise(resolve => {
    new Dialog({
      title: question,
      buttons: {
        yes: { label: "Yes", callback: () => resolve(true) },
        no: { label: "No", callback: () => resolve(false) }
      }
    }).render(true);
  });
}

Data Synchronization

Document Updates (Automatic)

Foundry syncs document updates automatically:

// Syncs to all clients
await actor.update({ "system.hp.value": 50 });

// Does NOT sync (in-memory only)
actor.system.hp.value = 50;

Non-Document State

Use sockets for custom state:

let combatState = {};

Hooks.once("socketlib.ready", () => {
  socket.register("syncCombatState", (state) => {
    combatState = state;
    Hooks.callAll("combatStateChanged", state);
  });
});

function updateCombatState(newState) {
  combatState = newState;
  socket.executeForEveryone("syncCombatState", newState);
}

Ownership Considerations

Only owners can update documents:

// Player cannot update enemy
await enemyActor.update({ ... }); // Permission denied!

// Must delegate to GM
await socket.executeAsGM("updateEnemy", enemyId, changes);

Common Pitfalls

1. Emitter Doesn't Receive Broadcast

// WRONG - emitter never sees this
game.socket.on("module.my-module", playSound);
game.socket.emit("module.my-module", { sound: "bell.wav" });
// Sound plays for others, NOT for emitter!

// CORRECT - call locally AND emit
playSound({ sound: "bell.wav" });
game.socket.emit("module.my-module", { sound: "bell.wav" });

2. Duplicate Execution in Hooks

// WRONG - runs on ALL clients
Hooks.on("deleteItem", (item) => {
  item.parent.update({ "system.count": item.parent.items.length });
});

// CORRECT - only owner executes
Hooks.on("deleteItem", (item) => {
  if (!item.parent?.isOwner) return;
  item.parent.update({ "system.count": item.parent.items.length });
});

3. Race Conditions with Multiple GMs

// RISKY - activeGM can change during async
game.socket.on(name, async (data) => {
  if (game.user !== game.users.activeGM) return;
  await actor.update({ ... }); // Another GM might be active now!
});

// SAFE - socketlib guarantees atomic execution
await socket.executeAsGM("updateActor", actorId, data);

4. No Permission Check on Handlers

// VULNERABLE - any player can trigger
game.socket.on(name, (data) => {
  game.actors.get(data.id).update({ "system.hp": 9999 });
});

// SAFE - validate permissions
game.socket.on(name, (data) => {
  const actor = game.actors.get(data.id);
  if (!actor?.isOwner && !game.user.isGM) return;
  actor.update({ "system.hp": data.hp });
});

5. No GM Connected

// WRONG - silent failure
socket.executeAsGM("doThing", data);

// CORRECT - handle error
try {
  await socket.executeAsGM("doThing", data);
} catch {
  ui.notifications.warn("A GM must be connected for this action");
}

6. Update Storms

// WRONG - N clients = N updates
Hooks.on("updateActor", (actor, changes) => {
  actor.update({ "system.modified": Date.now() });
});

// CORRECT - only owner updates
Hooks.on("updateActor", (actor, changes) => {
  if (!actor.isOwner) return;
  if (changes.system?.modified) return; // Prevent loop
  actor.update({ "system.modified": Date.now() });
});

Best Practices

1. Use Structured Events

// Good - clear, maintainable
game.socket.emit(SOCKET_NAME, {
  type: "applyEffect",
  targetId: token.id,
  effectType: "fire",
  duration: 3000
});

2. Batch Updates

// Bad - 3 updates
await actor.update({ "system.hp": 10 });
await actor.update({ "system.mp": 5 });
await actor.update({ "system.status": "hurt" });

// Good - 1 update
await actor.update({
  "system.hp": 10,
  "system.mp": 5,
  "system.status": "hurt"
});

3. Skip No-Op Updates

const newHp = calculateHp(actor);
if (actor.system.hp.value === newHp) return;
await actor.update({ "system.hp.value": newHp });

4. Document Socket Messages

/​**
 * Socket: module.my-module
 *
 * @event applyDamage
 * @param {string} actorId - Target actor
 * @param {number} damage - Damage amount
 * @param {string} type - Damage type (fire, cold, etc.)
 */

5. Prefer Socketlib for Complex Operations

Native sockets for simple broadcasts. Socketlib when you need:

  • Return values
  • Multiple GM handling
  • Permission-based execution
  • Error handling

Implementation Checklist

  • Add "socket": true to manifest
  • Use correct namespace (module.X or system.X)
  • Register listeners in init hook
  • Use structured event data with type field
  • Call handler locally when emitting (self-invoke pattern)
  • Check ownership in document operation hooks
  • Use socketlib for GM-delegated operations
  • Handle "no GM connected" errors
  • Batch related updates
  • Skip no-op updates
  • Test with multiple connected clients

References


Last Updated: 2026-01-04 Status: Production-Ready Maintainer: ImproperSubset

More skills from majiayu000

xiaohongshu logo
majiayu000/claude-arsenal

xiaohongshu

xiaohongshu

286 148
View
agent-task-conductor logo
majiayu000/claude-skill-registry

agent-task-conductor

Conduct multi-agent task orchestration and workflow coordination.

663 1
View
conductor-setup logo
majiayu000/claude-skill-registry

conductor-setup

Initialize project with Conductor artifacts (product definition,

663 1
View
animation-designer logo
majiayu000/claude-skill-registry

animation-designer

Expert in web animations, transitions, and motion design using Framer Motion and CSS

663 1
View
diagramming logo
majiayu000/claude-skill-registry

diagramming

Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...

663 1
View
h3-pg logo
majiayu000/claude-skill-registry-data

h3-pg

PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.

23 1
View
conductor-development logo
majiayu000/claude-skill-registry

conductor-development

Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...

663 1
View
conductor-status logo
majiayu000/claude-skill-registry

conductor-status

Display project status, active tracks, and next actions

663 1
View
dockerization logo
majiayu000/claude-skill-registry

dockerization

Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.

663 1
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View