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>
166 lines
5.8 KiB
SQL
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;
|