Socket
Book a DemoInstallSign in
Socket

@hpkv/zustand-multiplayer

Package Overview
Dependencies
Maintainers
2
Versions
22
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@hpkv/zustand-multiplayer

A multiplayer middleware for Zustand using HPKV

latest
Source
npmnpm
Version
1.0.0
Version published
Weekly downloads
33
1550%
Maintainers
2
Weekly downloads
 
Created
Source

Zustand Multiplayer Middleware

npm version npm downloads TypeScript License: MIT

What is Multiplayer?

Multiplayer is a Zustand middleware that adds real-time synchronization capabilities to your stores. When you wrap your store with the multiplayer middleware, every state change is automatically:

  • Synchronized across all connected clients in real-time via WebSockets
  • Persisted to a distributed database with atomic operations
  • Shared with all connected users instantly

No WebSocket server needed! Multiplayer is built on top of HPKV's WebSocket API, so you don't need to set up or maintain any server infrastructure. Just create a free HPKV API key in a few clicks, configure your store options, and you're ready to go.

Think of it as adding a "sync engine" to your existing Zustand store - turning any local state into shared, collaborative state that multiple users can interact with simultaneously.

Transform any Zustand store into a real-time synchronized multiplayer experience with just one line of code.

// Before: Local Zustand store
const useStore = create((set) => ({
  todos: {},
  addTodo: (text) => set(state => ...)
}));

// After: Real-time multiplayer store
const useStore = create(
  multiplayer((set) => ({
    todos: {},
    addTodo: (text) => set(state => ...)
  }), { namespace: 'my-app' })
);

That's it! Your store now syncs in real-time across all connected clients. 🎉

Why Zustand Multiplayer?

Building real-time collaborative features is complex. You need WebSockets, conflict resolution, state persistence, and synchronization logic. Zustand Multiplayer handles all of this for you:

  • 🔄 Instant Synchronization - State changes propagate to all clients in milliseconds
  • 💾 Automatic Persistence - State survives page refreshes and reconnections
  • 🎯 Selective Sync - Choose exactly what to share vs keep local
  • ⚡ Optimized Performance - Granular updates, minimal network traffic
  • 🔌 Works Everywhere - React, Node.js, vanilla JavaScript, Client, Server - anywhere Zustand works

Installation

npm install @hpkv/zustand-multiplayer zustand

5-Minute Quick Start

1. Get Your API Credentials

Sign up at hpkv.io and get your API credentials from the dashboard.

2. Create a Multiplayer Store

// store.ts
import { create } from 'zustand';
import { multiplayer, WithMultiplayer } from '@hpkv/zustand-multiplayer';

interface AppState {
  count: number;
  increment: () => void;
}

export const useStore = create<WithMultiplayer<AppState>>()(
  multiplayer(
    (set) => ({
      count: 0,
      increment: () => set((state) => ({ count: state.count + 1 })),
    }),
    {
      namespace: 'counter-app',  // Unique identifier for your app
      apiBaseUrl: process.env.NEXT_PUBLIC_HPKV_API_BASE_URL!,
      tokenGenerationUrl: '/api/generate-token',  // Your auth endpoint
    }
  )
);

3. Set Up Token Generation (Security)

Create an endpoint to generate tokens for client authentication:

// pages/api/generate-token.ts (Next.js) or server.js (Express)
import { TokenHelper } from '@hpkv/zustand-multiplayer';

const tokenHelper = new TokenHelper(
  process.env.HPKV_API_KEY!,
  process.env.HPKV_API_BASE_URL!
);

export default async function handler(req, res) {
  // Add your authentication logic here
  // const user = await authenticate(req);
  // if (!user) return res.status(401).json({ error: 'Unauthorized' });
  
  const response = await tokenHelper.processTokenRequest(req.body);
  res.status(200).json(response);
}

4. Use in Your App

// App.tsx
import { useStore } from './store';

function App() {
  const { count, increment, multiplayer } = useStore();
  
  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={increment}>+1</button>
      <p>Open this page in multiple tabs to see real-time sync!</p>
      <p>Status: {multiplayer.connectionState}</p>
    </div>
  );
}

That's it! Your app now syncs in real-time. Open it in multiple browser tabs to see the magic. ✨

Core Concepts

🏷️ Namespaces - Your Sync Scope

A namespace is a unique identifier that determines which stores sync together. Think of it as a "room" where all stores with the same namespace share state.

// All stores with namespace 'team-dashboard' will sync together
{ namespace: 'team-dashboard' }

// Different namespaces = isolated data
{ namespace: 'team-dashboard' }  // These sync together
{ namespace: 'user-settings' }   // This is completely separate

Best Practices:

  • Use descriptive, unique namespaces: todo-app-v1, game-room-${roomId}
  • Version your namespaces when making breaking changes: app-v1app-v2
  • Use dynamic namespaces for isolated sessions: meeting-${meetingId}

🔐 Authentication - Client vs Server

When creating a store using multiplayer, you either need to provide HPKV API key or a token generation url. As API key should never be exposed on client-side, for client-side usage always setup a token generation endpoint, but for server-side usage, you can use the API key directly.

See the documentation on how to set up the token generation endpoint in the Token Generation Guideline

Client-side (Web Apps):

// Never expose API keys in client code!
{
  namespace: 'my-app',
  apiBaseUrl: process.env.NEXT_PUBLIC_HPKV_API_BASE_URL,
  tokenGenerationUrl: '/api/generate-token',  // Secure backend endpoint
}

Server-side (Node.js):

// Safe to use API key directly on server
{
  namespace: 'my-app',
  apiBaseUrl: process.env.HPKV_API_BASE_URL,
  apiKey: process.env.HPKV_API_KEY,  // Direct API key usage
}

🎯 Selective Synchronization

By default, multiplayer syncs all the state with other clients, but it also allows you to control exactly what syncs and what stays local through sync option:

const useStore = create(
  multiplayer(
    (set) => ({
      // Shared data
      sharedTodos: {},
      teamSettings: {},
      
      // Local data
      draftText: '',
      userPreferences: {},
      
      // Actions...
    }),
    {
      namespace: 'my-app',
      // Only sync these fields
      sync: ['sharedTodos', 'teamSettings'],
      // Everything else stays local
    }
  )
);

🔧 zFactor - Fine-tune for best performance and less conflicts

The zFactor controls storage granularity. Choose based on what gets updated, how often, and together:

// Example state structure
{
  users: {
    user1: { name: 'Alice', score: 10 },
    user2: { name: 'Bob', score: 20 }
  }
}
zFactor: 0 (Atomic)          zFactor: 1 (Default)         zFactor: 2 (Granular)
┌─────────────────┐          ┌──────────────┐             ┌─────────────────┐
│ Store entire    │          │ Each user    │             │ Each property   │
│ 'users' object  │          │ stored       │             │ stored          │
│ as one unit     │          │ separately   │             │ separately      │
└─────────────────┘          └──────────────┘             └─────────────────┘
        ↓                            ↓                             ↓
  users → {...}               users:user1 → {...}          users:user1:name → 'Alice'
                             users:user2 → {...}          users:user1:score → 10
                                                          users:user2:name → 'Bob'
                                                          users:user2:score → 20

Analyze your specific state structure and update frequency. There's no universal "right" zFactor for application types.

If you don't set zFactor option, the default zFactor is 2 (two levels of storage granularity from root)

Exapmle Recipes

🗳️ Live Voting/Polling

const usePollStore = create(
  multiplayer(
    (set) => ({
      votes: {} as Record<string, number>,
      vote: (option: string) => set((state) => {
        state.votes[option] = (state.votes[option] || 0) + 1;
      }),
    }),
    { namespace: `poll-${pollId}` }
  )
);

👥 Presence & Live Cursors

const usePresenceStore = create(
  multiplayer(
    (set) => ({
      users: {} as Record<string, { name: string; cursor: { x: number; y: number } }>,
      updateCursor: (userId: string, x: number, y: number) => set((state) => {
        state.users[userId] = { ...state.users[userId], cursor: { x, y } };
      }),
    }),
    { 
      namespace: 'collaborative-canvas',
    }
  )
);

🎮 Game State

const useGameStore = create(
  multiplayer(
    (set) => ({
      players: {} as Record<string, Player>,
      gameState: 'waiting' as 'waiting' | 'playing' | 'finished',
      scores: {} as Record<string, number>,
      
      joinGame: (playerId: string, name: string) => set((state) => {
        state.players[playerId] = { id: playerId, name, ready: false };
      }),
      
      updateScore: (playerId: string, points: number) => set((state) => {
        state.scores[playerId] = (state.scores[playerId] || 0) + points;
      }),
    }),
    { 
      namespace: `game-room-${roomId}`,
    }
  )
);

📝 Collaborative Forms

const useFormStore = create(
  multiplayer(
    (set) => ({
      formData: {},
      fieldLocks: {} as Record<string, string>,  // Track who's editing what
      
      updateField: (field: string, value: any, userId: string) => set((state) => {
        if (!state.fieldLocks[field] || state.fieldLocks[field] === userId) {
          state.formData[field] = value;
          state.fieldLocks[field] = userId;
        }
      }),
      
      releaseField: (field: string) => set((state) => {
        delete state.fieldLocks[field];
      }),
    }),
    {
      namespace: `form-${formId}`,
      sync: ['formData', 'fieldLocks'],  // Don't sync local validation errors
    }
  )
);

🔔 Server-to-Client Broadcasting

// Server-side (Node.js)
import { createStore } from 'zustand/vanilla';

const broadcastStore = createStore(
  multiplayer(
    (set) => ({
      notifications: [] as Notification[],
      broadcast: (message: string) => set((state) => ({
        notifications: [...state.notifications, {
          id: Date.now(),
          message,
          timestamp: new Date(),
        }],
      })),
    }),
    {
      namespace: 'system-notifications',
      apiKey: process.env.HPKV_API_KEY,  // Server uses API key directly
    }
  )
);

// Broadcast to all clients
broadcastStore.getState().broadcast('System maintenance at 5 PM');

Advanced Features

⚡ Performance Optimization

For applications with high-frequency updates, consider these optimization strategies:

// Example: Optimizing a collaborative drawing app
const useCanvasStore = create(
  multiplayer(
    (set) => ({
      strokes: {},
      currentStroke: null,
      
      // Batch updates for better performance
      updateStroke: debounce((strokeId, points) => set((state) => {
        state.strokes[strokeId] = points;
      }), 100), // Debounce to max 10 updates/second. This should not exceed the rate limit for better performance
    }),
    {
      namespace: 'canvas',
      rateLimit: 10,    // Match your HPKV tier (Free: 10/s, Pro: 100/s)
      zFactor: 1,       // Store each stroke separately
    }
  )
);

Performance Tips:

  • Set rateLimit to match your HPKV tier to enable automatic throttling
  • Use debouncing for high-frequency events (mouse moves, typing)
  • Batch updates when possible to reduce network calls
  • Choose appropriate zFactor - higher values mean more granular updates but more keys

🔄 Middleware Composition

Zustand Multiplayer works seamlessly with other middlewares:

import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
import { subscribeWithSelector } from 'zustand/middleware';
import { multiplayer } from '@hpkv/zustand-multiplayer';

const useStore = create(
  multiplayer(
    subscribeWithSelector(
      immer((set) => ({
        // Immer allows direct mutations
        todos: {},
        addTodo: (text: string) => set((state) => {
          const id = Date.now().toString();
          state.todos[id] = { id, text, completed: false };  // Direct mutation!
        }),
        toggleTodo: (id: string) => set((state) => {
          state.todos[id].completed = !state.todos[id].completed;
        }),
      }))
    ),
    { namespace: 'todos-with-immer' }
  )
);

// Subscribe to specific changes
useStore.subscribe(
  (state) => state.todos,
  (todos) => console.log('Todos changed:', todos)
);

📊 Monitoring & Debugging

function ConnectionMonitor() {
  const { multiplayer } = useStore();
   
  return (
    <div>
      <p>Status: {multiplayer.connectionState}</p>
      <p>Round Trip Latency: {multiplayer.performanceMetrics.averageSyncTime}ms</p>
      <button onClick={() => multiplayer.reHydrate()}>Force Sync</button>
    </div>
  );
}

TypeScript Usage Guide

Zustand Multiplayer is built with TypeScript-first design and provides full type safety for your multiplayer stores.

Basic Type Setup

Always use the WithMultiplayer<T> wrapper type to ensure proper typing:

import { create } from 'zustand';
import { multiplayer, WithMultiplayer } from '@hpkv/zustand-multiplayer';

interface TodoState {
  todos: Record<string, Todo>;
  filter: 'all' | 'active' | 'completed';
  addTodo: (text: string) => void;
  toggleTodo: (id: string) => void;
  setFilter: (filter: TodoState['filter']) => void;
}

// Use WithMultiplayer wrapper
const useTodoStore = create<WithMultiplayer<TodoState>>()(
  multiplayer(
    (set) => ({
      todos: {},
      filter: 'all',
      addTodo: (text) => set((state) => ({
        todos: {
          ...state.todos,
          [Date.now().toString()]: { id: Date.now().toString(), text, completed: false }
        }
      })),
      toggleTodo: (id) => set((state) => ({
        todos: {
          ...state.todos,
          [id]: { ...state.todos[id], completed: !state.todos[id].completed }
        }
      })),
      setFilter: (filter) => set({ filter }),
    }),
    {
      namespace: 'todos-app',
      apiBaseUrl: process.env.NEXT_PUBLIC_HPKV_API_BASE_URL!,
      tokenGenerationUrl: '/api/generate-token',
    }
  )
);

Using Without React

Zustand Multiplayer works anywhere Zustand works - not just React!

Vanilla JavaScript

import { createStore } from 'zustand/vanilla';
import { multiplayer } from '@hpkv/zustand-multiplayer';

const store = createStore(
  multiplayer(
    (set) => ({
      count: 0,
      increment: () => set((state) => ({ count: state.count + 1 })),
    }),
    {
      namespace: 'vanilla-counter',
      apiKey: 'your-api-key',  // Server-side only!
    }
  )
);

// Use the store
store.getState().increment();
console.log(store.getState().count);

// Subscribe to changes
store.subscribe((state) => {
  document.getElementById('count').textContent = state.count;
});

Node.js Server

import { createStore } from 'zustand/vanilla';
import { multiplayer } from '@hpkv/zustand-multiplayer';

// Create a server-side store
const metricsStore = createStore(
  multiplayer(
    (set) => ({
      metrics: {},
      updateMetric: (key, value) => set((state) => {
        state.metrics[key] = value;
      }),
    }),
    {
      namespace: 'server-metrics',
      apiKey: process.env.HPKV_API_KEY,
    }
  )
);

// Update metrics from your server
setInterval(() => {
  metricsStore.getState().updateMetric('cpu', process.cpuUsage());
  metricsStore.getState().updateMetric('memory', process.memoryUsage());
}, 5000);

🔐 Security Best Practices

Token Generation Endpoint

Always implement proper authentication and authorization:

// api/generate-token.ts
export default async function handler(req, res) {
  // 1. Authenticate the user
  const user = await authenticateUser(req.headers.authorization);
  if (!user) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  
  // 2. Check permissions for the requested namespace
  const { namespace } = req.body;
  if (!user.canAccessNamespace(namespace)) {
    return res.status(403).json({ error: 'Access denied' });
  }
  
  // 3. Rate limiting
  if (await isRateLimited(user.id)) {
    return res.status(429).json({ error: 'Too many requests' });
  }
  
  // 4. Generate token
  const token = await tokenHelper.processTokenRequest({
    ...req.body
  });
  
  // 5. Log for audit
  await logTokenGeneration(user.id, namespace);
  
  return res.status(200).json(token);
}

Important Security Notes

  • Never expose API keys in client-side code
  • Tokens expire after 2 hours by default
  • Anyone with a token can read/write to that namespace
  • Implement authorization in your token endpoint
  • Consider rate limiting to prevent abuse

API Reference

Multiplayer Options

interface MultiplayerOptions<TState> {
  namespace: string;              // Required: Unique identifier
  apiBaseUrl: string;             // Required: HPKV API URL
  apiKey?: string;                // Server-side only
  tokenGenerationUrl?: string;    // Client-side only
  sync?: Array<keyof TState>;    // Fields to sync (default: all non-function keys)
  zFactor?: number;               // Storage depth (0-10, default: 1)
  logLevel?: LogLevel;            // Logging verbosity
  rateLimit?: number;             // Throttle to N req/s (match your HPKV tier)
}

Multiplayer State & Methods

// Access via store
const { multiplayer } = useStore();

// State (reactive)
multiplayer.connectionState     // 'CONNECTED' | 'DISCONNECTED' | 'CONNECTING' | 'RECONNECTING'
multiplayer.hasHydrated        // boolean - Has initial sync completed

multiplayer.performanceMetrics  // perfromance metrics

const store = useStore();
// Methods
await store.multiplayer.reHydrate();        // Force sync with server
await store.multiplayer.clearStorage();     // Clear all persisted data
await store.multiplayer.disconnect();       // Close connection
await store.multiplayer.connect();          // Establish connection
await store.multiplayer.destroy();          // Cleanup (call on unmount)

// Monitoring
store.store.multiplayer.getConnectionStatus();    // Detailed connection info
store.multiplayer.getMetrics();             // Performance metrics

Rate Limiting & Throttling

HPKV has rate limits based on your tier (Free tier: 10 requests/second). The rateLimit option enables automatic throttling to avoid hitting these limits:

{
  namespace: 'high-frequency-app',
  rateLimit: 10,  // Automatically throttle to 10 updates/second
}

For high-frequency updates (e.g., mouse movements, real-time drawing):

  • Consider debouncing or throttling at the application level
  • Batch multiple changes into single updates
  • Use higher zFactor for granular updates to reduce operation size

Examples & Resources

📦 Example Applications

We provide two complete example applications demonstrating real-world usage:

1. Next.js + React - Collaborative ToDo List Example (/examples/nextjs-collaborative-todo)

  • Full-stack setup with Next.js
  • Token generation endpoint implementation
  • React hooks integration
  • TypeScript

2. Vanilla JS - Collaborative ToDo List Example (/examples/javascript-collaborative-todo)

  • Vanilla JavaScript (no framework)
  • HTML5 with real-time updates
  • Token endpoint with Express.js

2. React - Realtime Chat Example (/examples/react-chat)

A traditional web application demonstrating:

  • React
  • Token endpoint with Express.js
  • Typescript

📚 Documentation

Contributing

We welcome contributions! See CONTRIBUTING.md for guidelines.

git clone https://github.com/hpkv-io/zustand-multiplayer.git
cd zustand-multiplayer
npm install
npm test

Support

License

MIT © HPKV Team

Keywords

zustand

FAQs

Package last updated on 11 Aug 2025

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts