Master PartyKit realtime collaboration patterns for Google Antigravity IDE multiplayer applications
# PartyKit Realtime Collaboration for Google Antigravity IDE
Build multiplayer experiences with PartyKit using Google Antigravity IDE. This comprehensive guide covers party servers, real-time sync, presence awareness, and collaborative editing patterns.
## Party Server Setup
```typescript
// party/main.ts
import type { Party, Connection, ConnectionContext } from "partykit/server";
interface UserInfo {
id: string;
name: string;
color: string;
cursor?: { x: number; y: number };
}
interface RoomState {
users: Map<string, UserInfo>;
document: {
content: string;
version: number;
};
cursors: Map<string, { x: number; y: number }>;
}
type Message =
| { type: "join"; user: UserInfo }
| { type: "leave"; userId: string }
| { type: "cursor"; userId: string; position: { x: number; y: number } }
| { type: "edit"; operation: Operation; version: number }
| { type: "sync"; state: Partial<RoomState> };
interface Operation {
type: "insert" | "delete";
position: number;
text?: string;
length?: number;
}
export default class CollaborationRoom implements Party {
private state: RoomState = {
users: new Map(),
document: { content: "", version: 0 },
cursors: new Map(),
};
constructor(public party: Party) {}
async onStart() {
const stored = await this.party.storage.get<RoomState>("state");
if (stored) {
this.state = {
...stored,
users: new Map(Object.entries(stored.users || {})),
cursors: new Map(Object.entries(stored.cursors || {})),
};
}
}
async onConnect(connection: Connection, ctx: ConnectionContext) {
const url = new URL(ctx.request.url);
const userInfo: UserInfo = {
id: connection.id,
name: url.searchParams.get("name") || "Anonymous",
color: this.generateColor(),
};
this.state.users.set(connection.id, userInfo);
connection.send(
JSON.stringify({
type: "sync",
state: {
users: Object.fromEntries(this.state.users),
document: this.state.document,
cursors: Object.fromEntries(this.state.cursors),
},
})
);
this.broadcast(
JSON.stringify({ type: "join", user: userInfo }),
[connection.id]
);
}
async onMessage(message: string, connection: Connection) {
const data = JSON.parse(message) as Message;
switch (data.type) {
case "cursor":
this.state.cursors.set(data.userId, data.position);
this.broadcast(message, [connection.id]);
break;
case "edit":
if (data.version === this.state.document.version) {
this.applyOperation(data.operation);
this.state.document.version++;
this.broadcast(
JSON.stringify({
type: "edit",
operation: data.operation,
version: this.state.document.version,
})
);
await this.party.storage.put("state", this.state);
} else {
connection.send(
JSON.stringify({
type: "sync",
state: { document: this.state.document },
})
);
}
break;
}
}
async onClose(connection: Connection) {
const user = this.state.users.get(connection.id);
this.state.users.delete(connection.id);
this.state.cursors.delete(connection.id);
if (user) {
this.broadcast(
JSON.stringify({ type: "leave", userId: connection.id })
);
}
}
private applyOperation(operation: Operation) {
const doc = this.state.document;
if (operation.type === "insert" && operation.text) {
doc.content =
doc.content.slice(0, operation.position) +
operation.text +
doc.content.slice(operation.position);
} else if (operation.type === "delete" && operation.length) {
doc.content =
doc.content.slice(0, operation.position) +
doc.content.slice(operation.position + operation.length);
}
}
private broadcast(message: string, exclude: string[] = []) {
for (const connection of this.party.getConnections()) {
if (!exclude.includes(connection.id)) {
connection.send(message);
}
}
}
private generateColor(): string {
const colors = ["#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4", "#FFEAA7"];
return colors[Math.floor(Math.random() * colors.length)];
}
}
```
## React Client Hook
```typescript
// src/hooks/usePartyKit.ts
import usePartySocket from "partysocket/react";
import { useState, useCallback } from "react";
interface UseCollaborationOptions {
roomId: string;
userName: string;
}
export function useCollaboration({ roomId, userName }: UseCollaborationOptions) {
const [users, setUsers] = useState<Map<string, UserInfo>>(new Map());
const [document, setDocument] = useState({ content: "", version: 0 });
const [cursors, setCursors] = useState<Map<string, { x: number; y: number }>>(new Map());
const [isConnected, setIsConnected] = useState(false);
const socket = usePartySocket({
host: process.env.NEXT_PUBLIC_PARTYKIT_HOST!,
room: roomId,
query: { name: userName },
onOpen() {
setIsConnected(true);
},
onClose() {
setIsConnected(false);
},
onMessage(event) {
const message = JSON.parse(event.data);
switch (message.type) {
case "sync":
if (message.state.users) {
setUsers(new Map(Object.entries(message.state.users)));
}
if (message.state.document) {
setDocument(message.state.document);
}
break;
case "join":
setUsers((prev) => new Map(prev).set(message.user.id, message.user));
break;
case "leave":
setUsers((prev) => {
const next = new Map(prev);
next.delete(message.userId);
return next;
});
break;
case "cursor":
setCursors((prev) => new Map(prev).set(message.userId, message.position));
break;
}
},
});
const sendCursor = useCallback(
(position: { x: number; y: number }) => {
socket.send(JSON.stringify({ type: "cursor", userId: socket.id, position }));
},
[socket]
);
const sendEdit = useCallback(
(operation: Operation) => {
socket.send(JSON.stringify({ type: "edit", operation, version: document.version }));
},
[socket, document.version]
);
return { users: Array.from(users.values()), document, cursors, isConnected, sendCursor, sendEdit, myId: socket.id };
}
```
## Collaborative Editor Component
```typescript
// src/components/CollaborativeEditor.tsx
"use client";
import { useRef, useEffect } from "react";
import { useCollaboration } from "@/hooks/usePartyKit";
export function CollaborativeEditor({ roomId }: { roomId: string }) {
const editorRef = useRef<HTMLTextAreaElement>(null);
const { users, document, cursors, isConnected, sendCursor, sendEdit, myId } = useCollaboration({ roomId, userName: "User" });
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
const rect = editorRef.current?.getBoundingClientRect();
if (rect) {
sendCursor({ x: e.clientX - rect.left, y: e.clientY - rect.top });
}
};
window.addEventListener("mousemove", handleMouseMove);
return () => window.removeEventListener("mousemove", handleMouseMove);
}, [sendCursor]);
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const newValue = e.target.value;
const oldValue = document.content;
if (newValue.length > oldValue.length) {
const position = e.target.selectionStart - 1;
const text = newValue.slice(position, position + (newValue.length - oldValue.length));
sendEdit({ type: "insert", position, text });
} else {
const position = e.target.selectionStart;
const length = oldValue.length - newValue.length;
sendEdit({ type: "delete", position, length });
}
};
return (
<div className="collaborative-editor">
<div className="presence-bar">
{users.map((user) => (
<div key={user.id} className="user-avatar" style={{ backgroundColor: user.color }} title={user.name}>
{user.name[0]}
</div>
))}
<span className={isConnected ? "connected" : "disconnected"}>
{isConnected ? "Connected" : "Reconnecting..."}
</span>
</div>
<div className="editor-container">
<textarea ref={editorRef} value={document.content} onChange={handleChange} className="editor" />
{Array.from(cursors.entries()).map(([userId, pos]) => {
if (userId === myId) return null;
const user = users.find((u) => u.id === userId);
return (
<div key={userId} className="cursor" style={{ left: pos.x, top: pos.y, borderColor: user?.color }}>
<span className="cursor-label" style={{ backgroundColor: user?.color }}>{user?.name}</span>
</div>
);
})}
</div>
</div>
);
}
```
## Best Practices for Google Antigravity IDE
When using PartyKit with Google Antigravity, design party servers for specific collaboration patterns. Implement conflict resolution for concurrent edits. Use presence for user awareness. Persist important state to storage. Handle reconnection gracefully. Let Gemini 3 generate party server logic from collaboration requirements.
Google Antigravity excels at building real-time multiplayer features with PartyKit.This PartyKit prompt is ideal for developers working on:
By using this prompt, you can save hours of manual coding and ensure best practices are followed from the start. It's particularly valuable for teams looking to maintain consistency across their partykit implementations.
Yes! All prompts on Antigravity AI Directory are free to use for both personal and commercial projects. No attribution required, though it's always appreciated.
This prompt works excellently with Claude, ChatGPT, Cursor, GitHub Copilot, and other modern AI coding assistants. For best results, use models with large context windows.
You can modify the prompt by adding specific requirements, constraints, or preferences. For PartyKit projects, consider mentioning your framework version, coding style, and any specific libraries you're using.