Files
calctext/supabase/migrations/001_create_schema.sql
C. Cassel ef302ebda9 feat: add auth, real-time collaboration, sharing, font control, and UI fixes
Phase 1 - Bug fixes:
- Fix color labels not showing on active line in format preview
- Replace eye emoji with SVG icon showing clear preview/raw state
- Replace // button with comment icon + better tooltip
- Fix ThemePicker accent colors when using system theme

Phase 2 - Font:
- Load JetBrains Mono via Google Fonts with offline fallback
- Add font size control (A-/A+) with keyboard shortcuts
- Persist font size preference in localStorage

Phase 3 - Auth:
- Supabase-based email/password authentication
- Device session management with configurable password renewal TTL
- AuthModal, UserMenu, SecuritySettings components

Phase 4 - Cloud sync:
- Document metadata sync to Supabase PostgreSQL
- Legacy localStorage migration on first login
- IndexedDB persistence via y-indexeddb

Phase 5 - Real-time collaboration:
- Y.js CRDT integration with CodeMirror 6
- Hocuspocus WebSocket server with JWT auth
- Collaborative cursor awareness
- CollabIndicator component

Phase 6 - Sharing:
- Share links with view/edit permissions
- ShareDialog component with copy-to-clipboard
- Minimal client-side router for /s/{token} URLs

Infrastructure:
- Docker Compose with PostgreSQL, GoTrue, PostgREST, Hocuspocus
- Nginx reverse proxy for all backend services
- SQL migrations with RLS policies
- Production-ready Dockerfile with build args

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 16:21:04 -04:00

166 lines
5.8 KiB
SQL

-- CalcText database schema
-- Run via Docker init or manually
-- Enable UUID generation
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- Roles for RLS
DO $$ BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'anon') THEN
CREATE ROLE anon NOLOGIN;
END IF;
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'authenticated') THEN
CREATE ROLE authenticated NOLOGIN;
END IF;
END $$;
GRANT USAGE ON SCHEMA public TO anon, authenticated;
-- ============================================================
-- User profiles (extends GoTrue auth.users)
-- ============================================================
CREATE TABLE IF NOT EXISTS profiles (
id UUID PRIMARY KEY,
display_name TEXT,
avatar_url TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
CREATE POLICY profiles_own ON profiles
FOR ALL USING (id = current_setting('request.jwt.claims', true)::json->>'sub'::text::uuid);
GRANT SELECT, INSERT, UPDATE ON profiles TO authenticated;
-- ============================================================
-- Folders
-- ============================================================
CREATE TABLE IF NOT EXISTS folders (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
owner_id UUID NOT NULL,
name TEXT NOT NULL DEFAULT 'New Folder',
parent_id UUID REFERENCES folders(id) ON DELETE CASCADE,
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE folders ENABLE ROW LEVEL SECURITY;
CREATE POLICY folders_own ON folders
FOR ALL USING (owner_id = current_setting('request.jwt.claims', true)::json->>'sub'::text::uuid);
GRANT SELECT, INSERT, UPDATE, DELETE ON folders TO authenticated;
-- ============================================================
-- Documents
-- ============================================================
CREATE TABLE IF NOT EXISTS documents (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
owner_id UUID NOT NULL,
title TEXT NOT NULL DEFAULT 'Untitled',
folder_id UUID REFERENCES folders(id) ON DELETE SET NULL,
is_favorite BOOLEAN NOT NULL DEFAULT false,
share_token TEXT UNIQUE,
share_permission TEXT CHECK (share_permission IN ('view', 'edit')),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_documents_owner ON documents(owner_id);
CREATE INDEX idx_documents_share_token ON documents(share_token) WHERE share_token IS NOT NULL;
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
-- Owner can do anything
CREATE POLICY documents_owner ON documents
FOR ALL USING (owner_id = current_setting('request.jwt.claims', true)::json->>'sub'::text::uuid);
-- Shared documents can be read via share token (anon access)
CREATE POLICY documents_shared_read ON documents
FOR SELECT USING (share_token IS NOT NULL AND share_permission IS NOT NULL);
GRANT SELECT, INSERT, UPDATE, DELETE ON documents TO authenticated;
GRANT SELECT ON documents TO anon;
-- ============================================================
-- Document collaborators (explicit user-to-user sharing)
-- ============================================================
CREATE TABLE IF NOT EXISTS document_collaborators (
document_id UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
user_id UUID NOT NULL,
permission TEXT NOT NULL CHECK (permission IN ('view', 'edit')),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (document_id, user_id)
);
ALTER TABLE document_collaborators ENABLE ROW LEVEL SECURITY;
-- Users can see their own collaborations
CREATE POLICY collab_own ON document_collaborators
FOR SELECT USING (user_id = current_setting('request.jwt.claims', true)::json->>'sub'::text::uuid);
-- Document owners can manage collaborators
CREATE POLICY collab_owner ON document_collaborators
FOR ALL USING (
document_id IN (
SELECT id FROM documents
WHERE owner_id = current_setting('request.jwt.claims', true)::json->>'sub'::text::uuid
)
);
GRANT SELECT, INSERT, UPDATE, DELETE ON document_collaborators TO authenticated;
-- ============================================================
-- Device sessions (password renewal per device)
-- ============================================================
CREATE TABLE IF NOT EXISTS device_sessions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL,
device_fingerprint TEXT NOT NULL,
device_name TEXT,
session_ttl_seconds INTEGER NOT NULL DEFAULT 86400,
last_password_auth_at TIMESTAMPTZ NOT NULL DEFAULT now(),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (user_id, device_fingerprint)
);
ALTER TABLE device_sessions ENABLE ROW LEVEL SECURITY;
CREATE POLICY device_sessions_own ON device_sessions
FOR ALL USING (user_id = current_setting('request.jwt.claims', true)::json->>'sub'::text::uuid);
GRANT SELECT, INSERT, UPDATE, DELETE ON device_sessions TO authenticated;
-- ============================================================
-- Y.js document snapshots storage
-- ============================================================
CREATE TABLE IF NOT EXISTS ydoc_snapshots (
document_id UUID PRIMARY KEY REFERENCES documents(id) ON DELETE CASCADE,
state BYTEA NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE ydoc_snapshots ENABLE ROW LEVEL SECURITY;
-- Owner can read/write snapshots
CREATE POLICY ydoc_owner ON ydoc_snapshots
FOR ALL USING (
document_id IN (
SELECT id FROM documents
WHERE owner_id = current_setting('request.jwt.claims', true)::json->>'sub'::text::uuid
)
);
-- Collaborators can read/write snapshots
CREATE POLICY ydoc_collab ON ydoc_snapshots
FOR ALL USING (
document_id IN (
SELECT document_id FROM document_collaborators
WHERE user_id = current_setting('request.jwt.claims', true)::json->>'sub'::text::uuid
AND permission = 'edit'
)
);
GRANT SELECT, INSERT, UPDATE ON ydoc_snapshots TO authenticated;