
Research
Two Malicious Rust Crates Impersonate Popular Logger to Steal Wallet Keys
Socket uncovers malicious Rust crates impersonating fast_log to steal Solana and Ethereum wallet keys from source code.
@hpkv/zustand-multiplayer
Advanced tools
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:
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. 🎉
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:
npm install @hpkv/zustand-multiplayer zustand
Sign up at hpkv.io and get your API credentials from the dashboard.
// 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
}
)
);
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);
}
// 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. ✨
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:
todo-app-v1
, game-room-${roomId}
app-v1
→ app-v2
meeting-${meetingId}
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
}
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
}
)
);
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)
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}` }
)
);
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',
}
)
);
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}`,
}
)
);
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-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');
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:
rateLimit
to match your HPKV tier to enable automatic throttlingzFactor
- higher values mean more granular updates but more keysZustand 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)
);
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>
);
}
Zustand Multiplayer is built with TypeScript-first design and provides full type safety for your multiplayer stores.
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',
}
)
);
Zustand Multiplayer works anywhere Zustand works - not just React!
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;
});
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);
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);
}
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)
}
// 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
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):
We provide two complete example applications demonstrating real-world usage:
/examples/nextjs-collaborative-todo
)/examples/javascript-collaborative-todo
)/examples/react-chat
)A traditional web application demonstrating:
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
MIT © HPKV Team
FAQs
A multiplayer middleware for Zustand using HPKV
The npm package @hpkv/zustand-multiplayer receives a total of 29 weekly downloads. As such, @hpkv/zustand-multiplayer popularity was classified as not popular.
We found that @hpkv/zustand-multiplayer demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 2 open source maintainers collaborating on the project.
Did you know?
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.
Research
Socket uncovers malicious Rust crates impersonating fast_log to steal Solana and Ethereum wallet keys from source code.
Research
A malicious package uses a QR code as steganography in an innovative technique.
Research
/Security News
Socket identified 80 fake candidates targeting engineering roles, including suspected North Korean operators, exposing the new reality of hiring as a security function.