Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b783c34
feat: replace y-webrtc with PartyKit for real-time collaboration
cursoragent Aug 19, 2026
b86c999
feat: use user JWT for RLS-enforced auth instead of service role key
cursoragent Aug 21, 2026
c4aef68
fix: add npm scripts for partykit dev/deploy
cursoragent Aug 21, 2026
0072dfd
docs: update PARTYKIT.md to use npm scripts
cursoragent Aug 21, 2026
198fe32
refactor: simplify database schema for PartyKit
cursoragent Aug 21, 2026
85fb8fe
fix: create document in database before connecting to PartyKit
cursoragent Aug 21, 2026
35412c2
refactor: simplify document creation flow with isNew flag
cursoragent Aug 21, 2026
1b11a00
ux: skip loading skeleton for new documents
cursoragent Aug 21, 2026
9c6b3d4
feat: implement delayed loading skeleton (250ms) to avoid flicker
cursoragent Aug 21, 2026
45d2139
docs: add comprehensive PartyKit architecture documentation
cursoragent Aug 21, 2026
d18e519
fix: reset skeleton state on document change, increase delay to 500ms
cursoragent Aug 21, 2026
58668d1
feat: add migration script for old CRDT schema to PartyKit
cursoragent Aug 25, 2026
961809e
fix: load .env files automatically in migration script
cursoragent Aug 25, 2026
3aa945e
fix: use built-in env file parser instead of dotenv
cursoragent Aug 25, 2026
11b4a6c
fix: use dotenv with npm script for migration
cursoragent Aug 25, 2026
6cab4b2
fix: add tsx as dev dependency for migration script
cursoragent Aug 25, 2026
13e276d
fix: use y-partykit load option to properly load document state
cursoragent Aug 26, 2026
e0ae525
feat: implement robust connection recovery for token expiration
cursoragent Aug 27, 2026
7b69b50
feat: add save retry limits to prevent infinite retry loops
cursoragent Aug 27, 2026
0e52b58
feat: disable editor when offline with clear UI feedback
cursoragent Aug 27, 2026
ec2153d
fix: properly handle token expiration and disable editing during reco…
cursoragent Aug 31, 2026
6e9d93f
untrack partykit state / cache in git
Matia-R Aug 31, 2026
d0f2bfe
debug: add more logging to diagnose connection loop issue
cursoragent Aug 31, 2026
0c1e1d4
fix: stop PartyKit reconnect loop on expired JWT
cursoragent Aug 31, 2026
44fe92f
fix: unblock Vercel preview build
cursoragent Aug 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions .env.example
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,24 @@
# When adding additional environment variables, the schema in "/src/env.js"
# should be updated accordingly.

# Example:
# SERVERVAR="foo"
# NEXT_PUBLIC_CLIENTVAR="bar"
# Supabase
NEXT_PUBLIC_SUPABASE_URL="https://your-project.supabase.co"
NEXT_PUBLIC_SUPABASE_ANON_KEY="your-anon-key"

# Service role key (for migrations only - DO NOT use in client-side code)
# Get from: Supabase Dashboard → Settings → API → service_role key
# SUPABASE_SERVICE_ROLE_KEY="your-service-role-key"

# PartyKit (Real-time collaboration)
# For local development: localhost:1999
# For production: your-project.partykit.dev
NEXT_PUBLIC_PARTYKIT_HOST="localhost:1999"

# Shared secret for PartyKit server to authenticate with the app
# Generate with: openssl rand -base64 32
PARTYKIT_SECRET="your-partykit-secret"

# App URL (for PartyKit server to call back to the app)
# For local development: http://localhost:3000
# For production: https://your-app.vercel.app
APP_URL="http://localhost:3000"
3 changes: 3 additions & 0 deletions .gitignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -39,6 +39,9 @@ yarn-error.log*
# vercel
.vercel

# partykit
.partykit

# typescript
*.tsbuildinfo

Expand Down
198 changes: 198 additions & 0 deletions PARTYKIT.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
# PartyKit Integration

This document describes the PartyKit integration for real-time collaborative editing.

## Overview

PartyKit replaces the previous y-webrtc peer-to-peer sync with a server-mediated WebSocket architecture. This provides:

- **Reliable sync**: No more WebRTC connection failures through firewalls
- **Single persistence point**: Only the PartyKit server writes to the database (no more duplicate saves from multiple clients)
- **Better scalability**: Server handles coordination instead of mesh connections between clients
- **RLS respected**: User authentication is verified on every connection

## Architecture

```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Client A │ │ Client B │ │ Client C │
│ (w/ JWT) │ │ (w/ JWT) │ │ (w/ JWT) │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
└───────────────────┼───────────────────┘
│ WebSocket + JWT
┌────────────────────────┐
│ PartyKit Server │
│ - Verifies JWT │
│ - Manages Y.Doc │
└───────────┬────────────┘
│ HTTP + JWT
┌────────────────────────┐
│ Next.js API Routes │
│ /api/partykit/* │
└───────────┬────────────┘
│ RLS enforced
┌────────────────────────┐
│ Supabase │
└────────────────────────┘
```

## Security Model

1. **Client authenticates with Supabase** and receives a JWT
2. **Client connects to PartyKit** with JWT in query params
3. **PartyKit verifies** the JWT is not expired
4. **PartyKit calls API routes** with the user's JWT
5. **API routes create Supabase client** using that JWT
6. **RLS automatically enforced** - users can only access documents they have permission to

No service role key is used. The user's own credentials flow through the entire system.

## Setup

### 1. Install PartyKit CLI

```bash
npm install -g partykit
```

### 2. Login to PartyKit

```bash
npx partykit login
```

### 3. Configure Environment Variables

Add to your `.env` file:

```env
# PartyKit host (for client-side)
NEXT_PUBLIC_PARTYKIT_HOST=localhost:1999 # dev
# NEXT_PUBLIC_PARTYKIT_HOST=chptr-collab.partykit.dev # prod

# Shared secret for server-to-server auth
PARTYKIT_SECRET=your-secret-here # Generate with: openssl rand -base64 32

# App URL for PartyKit server callbacks
APP_URL=http://localhost:3000 # dev
# APP_URL=https://your-app.vercel.app # prod
```

### 4. Local Development

Run both the Next.js dev server and PartyKit dev server:

```bash
# Terminal 1: Next.js
npm run dev

# Terminal 2: PartyKit (use npm script, not npx!)
npm run dev:partykit
```

**Important:** Use `npm run dev:partykit` instead of `npx partykit dev`. The npx version downloads a fresh PartyKit that can't see your project's dependencies.

PartyKit dev server runs on `localhost:1999` by default.

### 5. Deploy PartyKit

```bash
npm run deploy:partykit
```

This deploys to PartyKit's free tier at `chptr-collab.partykit.dev`.

### 6. Configure PartyKit Environment Variables

After deploying, set the environment variables for the PartyKit server:

```bash
npx partykit env add APP_URL
# Enter: https://your-app.vercel.app

npx partykit env add PARTYKIT_SECRET
# Enter: your-secret-here (same as in your Next.js .env)
```

Note: For `env` commands, `npx partykit` is fine since it doesn't need to bundle code.

## Database Schema

The PartyKit integration uses a simplified single-table schema:

```sql
CREATE TABLE document_state (
document_id UUID PRIMARY KEY REFERENCES documents(id),
state_data BYTEA NOT NULL, -- Full Y.Doc state
updated_at TIMESTAMPTZ DEFAULT NOW()
);
```

**To set up:** Run the migration in `migrations/partykit_document_state.sql`

This replaces the old `document_changes` + `document_snapshots` tables with a single table. No more compaction needed since we always store the full state.

## Files

| File | Purpose |
|------|---------|
| `partykit.json` | PartyKit configuration |
| `party/document.ts` | PartyKit server (Yjs room handler, JWT verification) |
| `src/hooks/use-collaborative-doc-partykit.ts` | Client-side hook (gets session, passes JWT) |
| `src/app/api/partykit/load/route.ts` | API to load document state |
| `src/app/api/partykit/save/route.ts` | API to save document state |
| `src/utils/supabase/from-token.ts` | Creates Supabase client from JWT |
| `migrations/partykit_document_state.sql` | Database migration |

## How It Works

### Client Connection

1. Client gets Supabase session (includes access_token)
2. `useCollaborativeDocPartykit` hook creates a Y.Doc and YPartyKitProvider
3. Provider connects to PartyKit server with JWT in query params
4. Provider syncs document state and awareness (cursors)

### Server Lifecycle

1. First client connects with JWT → PartyKit verifies JWT not expired
2. Room calls `/api/partykit/load` with user's JWT
3. API route creates Supabase client with that JWT → RLS enforced
4. If user has access, document loads; otherwise, connection rejected
5. As clients make edits, Y.Doc updates are broadcast to all connected clients
6. Room debounces saves (1 second) and calls `/api/partykit/save` with JWT
7. Last client disconnects → room shuts down (but save completes first)

### Permission Enforcement

- **Load**: If user can't read the document, the Supabase query returns nothing/error
- **Save**: If user can't write to the document, the Supabase upsert fails
- **Connect**: If load fails due to permissions, the connection is closed with code 4003

## Costs

PartyKit runs on Cloudflare Workers. Estimated costs:

| Users | Monthly Cost |
|-------|--------------|
| 0-50 | $0 (free tier) |
| 50-500 | ~$5 |
| 500-2000 | ~$10-25 |
| 2000+ | ~$25-100 |

## Rollback

To revert to y-webrtc:

1. In `src/app/documents/[documentId]/page.tsx`:
- Change import back to `use-collaborative-doc-crdt`
- Change hook call back to `useCollaborativeDocCrdt`

2. In `src/app/_components/editor/editor.tsx`:
- Change provider type back to `WebrtcProvider`

**Note:** The PartyKit integration uses a new `document_state` table. The old `document_changes` and `document_snapshots` tables are still present but not used. If you have existing documents that were created with the old system, you may need to migrate the data or keep both systems available.
Loading