npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@watchtower-sdk/sdk

v0.2.0

Published

Simple game backend SDK - saves, multiplayer rooms, and more

Readme

@watchtower/sdk

Simple game backend SDK - cloud saves, multiplayer rooms, automatic state sync.

Installation

npm install @watchtower/sdk

Quick Start

import { Watchtower } from '@watchtower/sdk'

const wt = new Watchtower({
  gameId: 'my-game',
  apiKey: 'wt_live_...' // Get from dashboard
})

// Cloud saves
await wt.save('progress', { level: 5, coins: 100 })
const data = await wt.load('progress')

// Multiplayer
const room = await wt.createRoom()
console.log('Share this code:', room.code) // e.g., "ABCD"

Cloud Saves

Simple key-value storage per player. Works across devices.

// Save anything JSON-serializable
await wt.save('progress', { level: 5, coins: 100 })
await wt.save('settings', { music: true, sfx: true })
await wt.save('inventory', ['sword', 'shield', 'potion'])

// Load it back
const progress = await wt.load('progress')
const settings = await wt.load('settings')

// List all save keys
const keys = await wt.listSaves() // ['progress', 'settings', 'inventory']

// Delete a save
await wt.deleteSave('inventory')

Multiplayer Rooms

Create rooms with 4-letter codes. Share with friends to play together.

// Create a room (you become the host)
const room = await wt.createRoom()
console.log('Room code:', room.code)

// Join an existing room
const room = await wt.joinRoom('ABCD')

// Check room properties
room.isHost      // true if you're the host
room.hostId      // current host's player ID
room.playerId    // your player ID
room.playerCount // number of players
room.players     // all players' states

Player State Sync

Automatically sync your player's position/state to all other players.

// Set your player state (automatically synced at 20Hz)
room.player.set({
  x: 100,
  y: 200,
  sprite: 'running',
  health: 100
})

// State is merged, so you can update individual fields
room.player.set({ x: 150 }) // keeps y, sprite, health

// Force immediate sync
room.player.sync()

// See all players' states
room.on('players', (players) => {
  for (const [playerId, state] of Object.entries(players)) {
    if (playerId !== room.playerId) {
      // Update other player's sprite
      updateOtherPlayer(playerId, state.x, state.y, state.sprite)
    }
  }
})

Game State (Host-Controlled)

Shared state for things like game phase, scores, round number. Only the host can modify it.

// Host sets game state
if (room.isHost) {
  room.state.set({
    phase: 'lobby',
    round: 0,
    scores: {}
  })
  
  // Start the game
  room.state.set({ phase: 'playing', round: 1 })
}

// Everyone receives state updates
room.on('state', (state) => {
  if (state.phase === 'playing') {
    startGame()
  }
  if (state.phase === 'gameover') {
    showWinner(state.winner)
  }
})

// Read current state anytime
const currentState = room.state.get()

Broadcast Messages

For one-off events that don't need persistent state.

// Broadcast to all players
room.broadcast({ type: 'explosion', x: 50, y: 50 })
room.broadcast({ type: 'chat', message: 'gg!' })

// Send to specific player
room.sendTo(playerId, { type: 'private_message', text: 'hey' })

// Receive messages
room.on('message', (from, data) => {
  if (data.type === 'explosion') {
    createExplosion(data.x, data.y)
  }
  if (data.type === 'chat') {
    showChat(from, data.message)
  }
})

Room Events

// Connection established
room.on('connected', ({ playerId, room }) => {
  console.log('Connected as', playerId)
  console.log('Host is', room.hostId)
})

// Player joined
room.on('playerJoined', (playerId, playerCount) => {
  console.log(`${playerId} joined (${playerCount} players)`)
  spawnPlayer(playerId)
})

// Player left
room.on('playerLeft', (playerId, playerCount) => {
  console.log(`${playerId} left (${playerCount} players)`)
  removePlayer(playerId)
})

// Host changed (automatic migration when host leaves)
room.on('hostChanged', (newHostId) => {
  console.log('New host:', newHostId)
  if (newHostId === room.playerId) {
    console.log("I'm the host now!")
  }
})

// Disconnected
room.on('disconnected', () => {
  console.log('Lost connection')
})

// Error
room.on('error', (error) => {
  console.error('Room error:', error)
})

Host Transfer

// Host can transfer to another player
if (room.isHost) {
  room.transferHost(otherPlayerId)
}

Full Example: Simple Multiplayer Game

import { Watchtower } from '@watchtower/sdk'

const wt = new Watchtower({ gameId: 'my-game', apiKey: 'wt_...' })

// Join or create room
async function joinGame(code?: string) {
  const room = code 
    ? await wt.joinRoom(code)
    : await wt.createRoom()
  
  console.log('Room:', room.code)
  
  // Game loop - update player position
  function gameLoop() {
    room.player.set({
      x: myPlayer.x,
      y: myPlayer.y,
      animation: myPlayer.currentAnim
    })
    requestAnimationFrame(gameLoop)
  }
  gameLoop()
  
  // Render other players
  const otherPlayers: Record<string, Sprite> = {}
  
  room.on('players', (players) => {
    for (const [id, state] of Object.entries(players)) {
      if (id === room.playerId) continue
      
      // Create sprite if new player
      if (!otherPlayers[id]) {
        otherPlayers[id] = createSprite()
      }
      
      // Update position
      otherPlayers[id].x = state.x as number
      otherPlayers[id].y = state.y as number
      otherPlayers[id].play(state.animation as string)
    }
  })
  
  // Clean up when players leave
  room.on('playerLeft', (id) => {
    otherPlayers[id]?.destroy()
    delete otherPlayers[id]
  })
  
  // Handle game events
  room.on('message', (from, data: any) => {
    if (data.type === 'shoot') {
      createBullet(data.x, data.y, data.dir)
    }
  })
  
  // Game state (host manages rounds, scores)
  room.on('state', (state: any) => {
    if (state.phase === 'playing') {
      showRound(state.round)
    }
    if (state.phase === 'gameover') {
      showWinner(state.winner)
    }
  })
  
  // If we're host, start game when 2 players join
  room.on('playerJoined', (_, count) => {
    if (room.isHost && count >= 2) {
      room.state.set({ phase: 'playing', round: 1 })
    }
  })
  
  return room
}

API Reference

Watchtower

const wt = new Watchtower(config)

| Config | Type | Description | |--------|------|-------------| | gameId | string | Your game's unique identifier | | apiKey | string | API key from dashboard | | playerId | string? | Custom player ID (auto-generated if not provided) | | apiUrl | string? | Custom API URL (default: Watchtower API) |

Room

| Property | Type | Description | |----------|------|-------------| | code | string | 4-letter room code | | isHost | boolean | True if you're the host | | hostId | string | Current host's player ID | | playerId | string | Your player ID | | playerCount | number | Number of players | | players | PlayersState | All players' states | | connected | boolean | Connection status |

| Method | Description | |--------|-------------| | player.set(state) | Set your player state (auto-synced) | | player.get() | Get your current player state | | player.sync() | Force immediate sync | | state.set(state) | Set game state (host only) | | state.get() | Get current game state | | broadcast(data) | Send to all players | | sendTo(id, data) | Send to specific player | | transferHost(id) | Transfer host (host only) | | disconnect() | Leave the room |

| Event | Callback | Description | |-------|----------|-------------| | connected | ({playerId, room}) => void | Connected to room | | players | (players) => void | Player states updated | | state | (state) => void | Game state updated | | playerJoined | (id, count) => void | Player joined | | playerLeft | (id, count) => void | Player left | | hostChanged | (newHostId) => void | Host changed | | message | (from, data) => void | Received broadcast | | disconnected | () => void | Lost connection | | error | (error) => void | Error occurred |

License

MIT