Skip to content
qwelplPublic

About

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Quovine

A calm, collaborative document editor: live co-editing with multiplayer cursors, in-doc chat, version history, and sharing - all synced to the cloud, free to run. (Working name, rename freely - it's just the metadata.title in app/layout.tsx and the brand mark in the top bars.)

Stack

  • Next.js (App Router) - hosted on Vercel's free (Hobby) tier
  • Supabase free tier - Postgres, Auth, Storage, and Realtime
  • Yjs (CRDT) + Tiptap for the editor, synced over a custom provider built on Supabase Realtime Broadcast (see lib/yjs/supabase-provider.ts for why: Vercel functions can't hold persistent WebSocket connections, so the realtime transport has to live on Supabase's side)

No billing/pricing logic anywhere - every feature described below is unconditionally available.

Features

  • Email/password auth, folders + documents dashboard
  • Real-time collaborative editing with multiplayer cursors and presence avatars
  • Comments anchored to text via Yjs relative positions (survive concurrent edits elsewhere in the doc)
  • In-document chat
  • Version history: manual "Save version" snapshots, restore, pruned to the last 50 per document
  • Sharing: invite collaborators by email with a role (viewer/commenter/editor), or generate a role-scoped link that anyone signed in can use to join
  • Focus mode, dark/light mode, a handful of accent themes
  • Live word count, Markdown export
  • Works offline-ish: edits persist to IndexedDB locally and sync once reconnected

Local setup

  1. Create a Supabase project at supabase.com (free tier).
  2. Run the migrations in supabase/migrations/ against your project, in order - either via the Supabase CLI (supabase link, then supabase db push) or by pasting each file's contents into the SQL Editor in the Supabase dashboard, in filename order (0001, 0002, ...).
  3. Enable private Realtime channels: Project Settings > Realtime > turn off "Allow public access to all channels" (the migrations lock channel access down to collaborators via RLS on realtime.messages, but that only takes effect once public access is off).
  4. Copy env vars: cp .env.local.example .env.local, then fill in:
    • NEXT_PUBLIC_SUPABASE_URL / NEXT_PUBLIC_SUPABASE_ANON_KEY - Project Settings > API
    • SUPABASE_SERVICE_ROLE_KEY - same page (server-only, used by the maintenance cron)
    • CRON_SECRET - any random string
  5. Install deps and run:
    npm install
    npm run dev
    Open http://localhost:3000.

Deploying (Vercel free tier)

  1. Push this repo to GitHub, then import it in Vercel.
  2. Add the same env vars from .env.local to the Vercel project's Environment Variables.
  3. Deploy. vercel.json already declares the daily maintenance cron (/api/cron/maintenance) - Vercel's Hobby plan runs it once a day and automatically sends Authorization: Bearer $CRON_SECRET, which the route checks. That job does two things: prunes version history past 50 snapshots per document, and touches the database so Supabase's free project doesn't auto-pause after 7 days of inactivity.

Notable design choices (for whoever picks this up next)

  • Document sync isn't "read-modify-write a snapshot column." Every edit appends to an append-only document_updates log (concurrent inserts never conflict); documents.content_snapshot is just a periodic compaction of that log, written under an optimistic-concurrency check (snapshot_version). See lib/yjs/compact.ts for why a naive locked read-modify-write doesn't work well against a serverless/pooled Postgres connection.
  • Realtime Broadcast channels are private and RLS-gated (supabase/migrations/0003_rls_realtime.sql) - without this, anyone who learns a document's UUID could join its live channel regardless of the collaborators table.
  • profiles.email is locked down at the column-privilege level, not just row-level - the only reader is the find_user_by_email RPC used for invites. The client always gets "my own email" from the auth session, never from this table.
  • Types aren't generated from the live schema yet (supabase gen types typescript needs a linked project) - lib/types.ts is hand-written and the Supabase client isn't generic-typed. Worth doing once the project is linked; low urgency since RLS is the actual authorization boundary, not the TS types.

About

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages