TypeSafe Primitives for a Realtime Web
Open-source, multiplayer APIs powered-by TypeScript inference end-to-end
// backend
const sendMessage = io.procedure
.input(z.object({ message: z.ZodString
message: z.string() }))
.broadcast(({ message: string
message }) => ({
receiveMessage: { message: string
message },
}));
const router = io.router({ sendMessage });
const ioServer = io.server({ router });
// frontend
event.receiveMessage.useEvent(({ data }) => {
console.log(data.message: string
message);
});
broadcast.sendMessage: (input: {
message: string;
}) => void
sendMessage({ message: string
message: "Hello!" });
Automatic type-safety
Leverage the full-power of TypeScript interfence to catch errors quickly and get IDE auto-completions as you're developing!
// pnpm install @pluv/platform-node
createIO(platformNode());
// pnpm install @pluv/platform-cloudflare
createIO(platformCloudflare());
const GET = async (req: Request) => {
const const room: string
room = getRoomFromUrl(req.url);
const { const user: {
id: string;
}
user } = await getSession(req);
const token = await ioServer
.createToken({ room: string
room, user: {
id: string;
}
user });
return new Response(token, { status: 200 });
};
const {
useBroadcast,
useOthers,
// ...
} = createBundle(client);
const broadcast = useBroadcast();
const names = useOthers((others) => {
return others.map(({ user }) => user.name);
});
// pnpm install @pluv/platform-pluv
const io = createIO(
platformPluv({
authorize: {
user: z.object({ id: z.string() }),
},
basePath: "/api/pluv",
publicKey: "pk_...",
secretKey: "sk_...",
webhookSecret: "whsec_...",
}),
);
Usage monitoring
When hosted on the pluv.io network, track active rooms and connections, and custom events to gain insights on how your app is being used.
const client = createClient({
types,
initialStorage: yjs.doc(() => ({
messages: YjsType<YArray<string>, string[]>
messages: yjs.array<string>([]),
})),
});
const [const messages: string[] | null
messages, const type: YjsType<YArray<string>, string[]> | null
type] = useStorage("messages");
const addMessage = (message: string
message: string) => {
const type: YjsType<YArray<string>, string[]> | null
type?.push([message: string
message]);
};
const messages: string[] | null
messages?.map((message: string
message, i) => (
<div key={i}>{message: string
message}</div>
));
const client = createClient({
types,
presence?: InputZodLike<{
cursor: {
x: number;
y: number;
} | null;
}> | undefined
presence: z.object({
cursor: z.ZodNullable<z.ZodObject<{
x: z.ZodNumber;
y: z.ZodNumber;
}, "strip", z.ZodTypeAny, {
x: number;
y: number;
}, {
x: number;
y: number;
}>>
cursor: z.object({
x: z.number(),
y: z.number(),
}).nullable(),
}),
});
const [const myCursor: {
x: number;
y: number;
} | null
myCursor] = useMyPresence(
({ cursor: {
x: number;
y: number;
} | null
cursor }) => cursor: {
x: number;
y: number;
} | null
cursor,
);
const const cursors: ({
x: number;
y: number;
} | null)[]
cursors = useOthers((others) => (
others.map(({ presence: {
cursor: {
x: number;
y: number;
} | null;
}
presence }) => presence: {
cursor: {
x: number;
y: number;
} | null;
}
presence.cursor: {
x: number;
y: number;
} | null
cursor)
));
// backend
const sendMessage = io.procedure
.input(z.object({ message: z.ZodString
message: z.string() }))
.broadcast(({ message: string
message }) => ({
receiveMessage: { message: string
message },
}));
const router = io.router({ sendMessage });
const ioServer = io.server({ router });
// frontend
event.receiveMessage.useEvent(({ data }) => {
console.log(data.message: string
message);
});
broadcast.sendMessage: (input: {
message: string;
}) => void
sendMessage({ message: string
message: "Hello!" });
Automatic type-safety
Leverage the full-power of TypeScript interfence to catch errors quickly and get IDE auto-completions as you're developing!
const client = createClient({
types,
initialStorage: yjs.doc(() => ({
messages: YjsType<YArray<string>, string[]>
messages: yjs.array<string>([]),
})),
});
const [const messages: string[] | null
messages, const type: YjsType<YArray<string>, string[]> | null
type] = useStorage("messages");
const addMessage = (message: string
message: string) => {
const type: YjsType<YArray<string>, string[]> | null
type?.push([message: string
message]);
};
const messages: string[] | null
messages?.map((message: string
message, i) => (
<div key={i}>{message: string
message}</div>
));
// pnpm install @pluv/platform-pluv
const io = createIO(
platformPluv({
authorize: {
user: z.object({ id: z.string() }),
},
basePath: "/api/pluv",
publicKey: "pk_...",
secretKey: "sk_...",
webhookSecret: "whsec_...",
}),
);
const GET = async (req: Request) => {
const const room: string
room = getRoomFromUrl(req.url);
const { const user: {
id: string;
}
user } = await getSession(req);
const token = await ioServer
.createToken({ room: string
room, user: {
id: string;
}
user });
return new Response(token, { status: 200 });
};
const {
useBroadcast,
useOthers,
// ...
} = createBundle(client);
const broadcast = useBroadcast();
const names = useOthers((others) => {
return others.map(({ user }) => user.name);
});
// pnpm install @pluv/platform-node
createIO(platformNode());
// pnpm install @pluv/platform-cloudflare
createIO(platformCloudflare());
const client = createClient({
types,
presence?: InputZodLike<{
cursor: {
x: number;
y: number;
} | null;
}> | undefined
presence: z.object({
cursor: z.ZodNullable<z.ZodObject<{
x: z.ZodNumber;
y: z.ZodNumber;
}, "strip", z.ZodTypeAny, {
x: number;
y: number;
}, {
x: number;
y: number;
}>>
cursor: z.object({
x: z.number(),
y: z.number(),
}).nullable(),
}),
});
const [const myCursor: {
x: number;
y: number;
} | null
myCursor] = useMyPresence(
({ cursor: {
x: number;
y: number;
} | null
cursor }) => cursor: {
x: number;
y: number;
} | null
cursor,
);
const const cursors: ({
x: number;
y: number;
} | null)[]
cursors = useOthers((others) => (
others.map(({ presence: {
cursor: {
x: number;
y: number;
} | null;
}
presence }) => presence: {
cursor: {
x: number;
y: number;
} | null;
}
presence.cursor: {
x: number;
y: number;
} | null
cursor)
));
Usage monitoring
When hosted on the pluv.io network, track active rooms and connections, and custom events to gain insights on how your app is being used.
Automatic type-safety
Leverage the full-power of TypeScript interfence to catch errors quickly and get IDE auto-completions as you're developing!
Usage monitoring
When hosted on the pluv.io network, track active rooms and connections, and custom events to gain insights on how your app is being used.
Developer-Focused APIs
Unlock powerful utilities to build complex multiplayer experiences in minutes, not days.
Create your PluvIO server
To get started with pluv.io, you will first need to create a PluvIO
server that can start registering new realtime connections.
Pick which platform (i.e. adapter) you wish to connect to. Connect to pluv.io as a fully-managed service for your Next.js app. Or self-host on Node.js or on Cloudflare Workers.
Define a Zod schema validator to ensure the structure and type-safety of authorized users in your rooms.
Set-up HTTP endpoints
Next, set-up custom authorization for your rooms. And mount the `PluvServer` to enable realtime connections to your pluv.io instance.
Setup will vary depending on the platform (e.g. Node.js or Cloudflare Workers) used.
Prepare your frontend bundle
ioServer
. This frontend bundle contains all of pluv.io's APIs for realtime collaboration.You can optionally unlock more realtime capabilities for your app by defining:
- A presence state for each user with Zod.
- CRDT storage with Yjs or Loro.
- Custom typesafe events on your backend[1] or your frontend.
Wrap with PluvRoomProvider
The room bundle provides a PluvRoomProvider
to wrap your page with. Once you do, pluv.io APIs can now be used in nested components!
Build with typesafe realtime primitives!
With our frontend bundle ready to use, you can start using pluv.io realtime primitives with TypeScript autocompletion and intellisense for your custom events, presence and storage.
Type definitions will be as narrow as you've configured, with minimal manual type definitions and without code-generation!
Native-like Realtime Data
Treat your realtime data like ordinary state.Storage data is shared and modifiable by all participants.
Simple, Transparent Pricing
Avoid surprise bills by tracking usage in your dashboard.Self-host for free, or connect to our network on a single, usage-based plan.
- ∞ messages
- Self-hosted WebSockets
- Realtime APIs
- Typesafe client events
- Typesafe server events
- Per-message server event listener
- Email support
- Community Support
- Usage monitoring
- 10M messages included
- WebSockets on the Edge
- Realtime APIs
- Typesafe client events
- Typesafe server events
- Per-message server event listener
- Email support
- Community Support
- Usage monitoring