Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ builders.
- [ ] Leave comments
- [ ] Advanced AI capabilities
- [x] Queue multiple messages at once
- [x] Cynthia Design Auditor - AI-powered UDEC scoring across 13 design axes
- [ ] Use Images as references and as assets in a project
- [ ] Setup and use MCPs in projects
- [ ] Allow Onlook to use itself as a toolcall for branch creation and iteration
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
-- PHASE 3: Add Audit Fields to Build Sessions
-- Created: 2025-12-19
-- Purpose: Wire build sessions to real Cynthia audits

-- ============================================================
-- ENUMS
-- ============================================================

-- Build session audit status enum
CREATE TYPE build_session_audit_status AS ENUM (
'pending',
'running',
'completed',
'failed'
);

-- ============================================================
-- ALTER TABLES
-- ============================================================

-- Add audit fields to build_sessions
ALTER TABLE build_sessions
ADD COLUMN audit_id UUID REFERENCES cynthia_audits(id) ON DELETE SET NULL ON UPDATE CASCADE,
ADD COLUMN audit_status build_session_audit_status DEFAULT 'pending';
Comment on lines +21 to +24

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Migration references missing cynthia_audits table

This migration adds audit_id with a foreign key to cynthia_audits, but no migration creates that table (rg over apps/backend/supabase/migrations finds only this reference), so applying this script will fail with a missing relation error and block the migration stack. The table or its creation migration needs to be added before introducing the FK.

Useful? React with 👍 / 👎.


-- ============================================================
-- INDEXES
-- ============================================================

-- Index on build_sessions.audit_id for audit lookups
CREATE INDEX idx_build_sessions_audit_id ON build_sessions(audit_id);

-- Index on build_sessions.audit_status for filtering by status
CREATE INDEX idx_build_sessions_audit_status ON build_sessions(audit_status);

-- ============================================================
-- COMMENTS
-- ============================================================

COMMENT ON COLUMN build_sessions.audit_id IS 'Phase 3: Links to real Cynthia audit (nullable until audit completes)';
COMMENT ON COLUMN build_sessions.audit_status IS 'Phase 3: Tracks audit processing state (pending → running → completed/failed)';
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
-- PHASE 2: Build Sessions & Preview Links
-- Created: 2025-12-19
-- Purpose: Add tables for "Build My Site" viral wedge functionality

-- ============================================================
-- ENUMS
-- ============================================================

-- Build session status enum
CREATE TYPE build_session_status AS ENUM (
'created',
'previewed',
'locked',
'converted'
);

-- Build session input type enum
CREATE TYPE build_session_input_type AS ENUM (
'idea',
'url'
);

-- ============================================================
-- TABLES
-- ============================================================

-- Build sessions table
-- Stores each "Build My Site" session (anonymous or authenticated)
CREATE TABLE IF NOT EXISTS build_sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

-- Session data
language TEXT NOT NULL DEFAULT 'en', -- 'en' | 'es'
input_type build_session_input_type NOT NULL,
input_value TEXT NOT NULL,

-- Audit results (static for Phase 2, real in Phase 3)
teaser_score INTEGER,
teaser_summary JSONB,

-- Status tracking
status build_session_status NOT NULL DEFAULT 'created',

-- User relationship (nullable - anonymous sessions allowed)
user_id UUID REFERENCES auth.users(id) ON DELETE SET NULL ON UPDATE CASCADE
);

-- Preview links table
-- Public shareable links for build sessions
CREATE TABLE IF NOT EXISTS preview_links (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

-- Build session relationship
build_session_id UUID NOT NULL REFERENCES build_sessions(id) ON DELETE CASCADE ON UPDATE CASCADE,

-- Public slug for sharing (unguessable)
slug TEXT NOT NULL UNIQUE,

-- Optional expiration
expires_at TIMESTAMPTZ
);

-- ============================================================
-- INDEXES
-- ============================================================

-- Index on preview_links.slug for fast lookup
CREATE INDEX idx_preview_links_slug ON preview_links(slug);

-- Index on build_sessions.user_id for user session queries
CREATE INDEX idx_build_sessions_user_id ON build_sessions(user_id);

-- Index on build_sessions.status for status filtering
CREATE INDEX idx_build_sessions_status ON build_sessions(status);

-- ============================================================
-- ROW LEVEL SECURITY (RLS) POLICIES
-- ============================================================

-- Enable RLS on both tables
ALTER TABLE build_sessions ENABLE ROW LEVEL SECURITY;
ALTER TABLE preview_links ENABLE ROW LEVEL SECURITY;

-- ============================================================
-- BUILD_SESSIONS POLICIES
-- ============================================================

-- RLS Policy: Public insert allowed (anonymous users can create sessions)
-- Rationale: "No signup to start" - anyone can create a build session
DROP POLICY IF EXISTS "build_sessions_insert_policy" ON build_sessions;
CREATE POLICY "build_sessions_insert_policy" ON build_sessions
FOR INSERT
TO anon, authenticated
WITH CHECK (true);

-- RLS Policy: Public select DENIED (no public listing of sessions)
-- Rationale: Sessions are private by default, only accessible via preview link
DROP POLICY IF EXISTS "build_sessions_select_anon_policy" ON build_sessions;
CREATE POLICY "build_sessions_select_anon_policy" ON build_sessions
FOR SELECT
TO anon
USING (false);

-- RLS Policy: Owner select allowed (users can see their own sessions)
-- Rationale: Authenticated users can view sessions they created
DROP POLICY IF EXISTS "build_sessions_select_owner_policy" ON build_sessions;
CREATE POLICY "build_sessions_select_owner_policy" ON build_sessions
FOR SELECT
TO authenticated
USING (user_id = auth.uid());

-- RLS Policy: Owner update allowed (users can update their own sessions)
-- Rationale: Users can change status or claim anonymous sessions
DROP POLICY IF EXISTS "build_sessions_update_owner_policy" ON build_sessions;
CREATE POLICY "build_sessions_update_owner_policy" ON build_sessions
FOR UPDATE
TO authenticated
USING (user_id = auth.uid() OR user_id IS NULL)
Comment on lines +116 to +121

Copilot AI Dec 27, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The build_sessions_update_owner_policy allows any authenticated user to update rows where user_id is NULL, which means a malicious user can "claim" any anonymous build session by setting its user_id to their own and then read or manipulate that session’s data. With the Supabase anon key exposed in the browser, an attacker can sign in, enumerate target build_session_ids via related APIs (e.g., from preview links), and issue update calls that pass RLS (user_id IS NULL), effectively hijacking other users’ build sessions. Tighten this policy so only the creator or a server-side service role can associate a session with a user (for example by tying updates to a per-session secret or only allowing user assignment in trusted backend code), and avoid permitting arbitrary updates on user_id IS NULL rows from client-side roles.

Suggested change
-- Rationale: Users can change status or claim anonymous sessions
DROP POLICY IF EXISTS "build_sessions_update_owner_policy" ON build_sessions;
CREATE POLICY "build_sessions_update_owner_policy" ON build_sessions
FOR UPDATE
TO authenticated
USING (user_id = auth.uid() OR user_id IS NULL)
-- Rationale: Authenticated users can only update sessions they own; associating
-- anonymous sessions with a user must be done via trusted backend code
DROP POLICY IF EXISTS "build_sessions_update_owner_policy" ON build_sessions;
CREATE POLICY "build_sessions_update_owner_policy" ON build_sessions
FOR UPDATE
TO authenticated
USING (user_id = auth.uid())

Copilot uses AI. Check for mistakes.
WITH CHECK (user_id = auth.uid());

-- ============================================================
-- PREVIEW_LINKS POLICIES
-- ============================================================

-- RLS Policy: Public select by slug allowed (anyone can view via preview link)
-- Rationale: Preview links are publicly shareable - this is the viral mechanic
DROP POLICY IF EXISTS "preview_links_select_by_slug_policy" ON preview_links;
CREATE POLICY "preview_links_select_by_slug_policy" ON preview_links
FOR SELECT
TO anon, authenticated
USING (true);
Comment on lines +130 to +134

Copilot AI Dec 27, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The preview_links_select_by_slug_policy grants SELECT on all preview_links rows to both anon and authenticated roles with USING (true), which lets anyone holding the public Supabase anon key enumerate every preview slug and associated build_session_id. This breaks the intended "unguessable slug, no enumeration" security model and allows an attacker to list all share links and then fetch each preview (and, in combination with the build session update policy, potentially take over those sessions). Restrict SELECT so that public roles can only read a single row by a provided slug through a tightly scoped backend endpoint or RPC (and not via unrestricted table SELECT), and ensure expired links and any sensitive fields are filtered server-side.

Copilot uses AI. Check for mistakes.

-- RLS Policy: No public insert (only server/authenticated can create)
-- Rationale: Prevent spam, only app can generate preview links
DROP POLICY IF EXISTS "preview_links_insert_policy" ON preview_links;
CREATE POLICY "preview_links_insert_policy" ON preview_links
FOR INSERT
TO authenticated
WITH CHECK (true);
Comment on lines +136 to +142

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

preview_links_insert_policy blocks anonymous session creation flow.

The policy only allows authenticated users to insert preview links, but the application flow (per build-session.ts) creates preview links for anonymous users. This will cause inserts to fail for anonymous sessions unless the insert bypasses RLS (e.g., using a service role).

🔎 Potential fixes

Option 1: Allow anon to insert (if acceptable):

 CREATE POLICY "preview_links_insert_policy" ON preview_links
 FOR INSERT
-TO authenticated
+TO anon, authenticated
 WITH CHECK (true);

Option 2: Ensure the tRPC router uses a service role connection that bypasses RLS for anonymous session creation. Verify this is the intended design.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
-- RLS Policy: No public insert (only server/authenticated can create)
-- Rationale: Prevent spam, only app can generate preview links
DROP POLICY IF EXISTS "preview_links_insert_policy" ON preview_links;
CREATE POLICY "preview_links_insert_policy" ON preview_links
FOR INSERT
TO authenticated
WITH CHECK (true);
-- RLS Policy: No public insert (only server/authenticated can create)
-- Rationale: Prevent spam, only app can generate preview links
DROP POLICY IF EXISTS "preview_links_insert_policy" ON preview_links;
CREATE POLICY "preview_links_insert_policy" ON preview_links
FOR INSERT
TO anon, authenticated
WITH CHECK (true);


-- RLS Policy: No public update (immutable once created)
-- Rationale: Preview links should not change after creation
DROP POLICY IF EXISTS "preview_links_update_policy" ON preview_links;
CREATE POLICY "preview_links_update_policy" ON preview_links
FOR UPDATE
TO authenticated
USING (false);

-- RLS Policy: No public delete (only via cascade from build_session)
-- Rationale: Cleanup happens automatically when session is deleted
DROP POLICY IF EXISTS "preview_links_delete_policy" ON preview_links;
CREATE POLICY "preview_links_delete_policy" ON preview_links
FOR DELETE
TO authenticated
USING (
EXISTS (
SELECT 1 FROM build_sessions
WHERE build_sessions.id = preview_links.build_session_id
AND build_sessions.user_id = auth.uid()
)
);

-- ============================================================
-- SECURITY NOTES
-- ============================================================
--
-- build_sessions:
-- - Public INSERT: Anyone can create (viral onboarding)
-- - Public SELECT: DENIED (no data leaks)
-- - Owner SELECT/UPDATE: User can see and update their own sessions
-- - Anonymous sessions can be "claimed" by authenticated users
--
-- preview_links:
-- - Public SELECT: Anyone with slug can view (shareable)
-- - Public INSERT: DENIED (only app creates)
-- - Public UPDATE/DELETE: DENIED (immutable)
-- - No listing endpoint (must know exact slug)
--
-- Privacy guarantees:
-- - Slug must be unguessable (min 8 chars, random)
-- - No enumeration attack possible (no public list)
-- - User data not exposed via preview (sanitized in query)
-- - Expired links can be checked client-side before rendering
--
-- ============================================================

-- Add comment metadata
COMMENT ON TABLE build_sessions IS 'Phase 2: Build My Site sessions (anonymous or authenticated)';
COMMENT ON TABLE preview_links IS 'Phase 2: Public shareable preview links for build sessions';
COMMENT ON COLUMN build_sessions.user_id IS 'Nullable - anonymous sessions allowed, can be claimed later';
COMMENT ON COLUMN preview_links.slug IS 'Unguessable random slug for public sharing (min 8 chars)';
105 changes: 104 additions & 1 deletion apps/web/client/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,51 @@
"blankStart": "Start from a blank page"
}
},
"landing": {
"hero": {
"headline": "Stop shipping ugly.",
"subhead": "Cynthia audits your UI like a credit bureau—and rebuilds it into a lovable, conversion-ready front end.",
"inputPlaceholder": "Describe your idea… (or paste your URL)",
"primaryCta": "Get my Cynthia Score",
"secondaryCta": "See a sample report",
"trustLine": "No signup to start. Preview first. Upgrade when you're ready."
},
"sections": {
"creditBureau": {
"title": "A design credit report for your product",
"body": "We don't give vibes. We give receipts: scores, findings, exact fixes.",
"bullet1": "Hierarchy failures that confuse users",
"bullet2": "Typography that kills trust",
"bullet3": "Spacing debt that makes everything feel cheap",
"bullet4": "Accessibility issues that block real customers"
},
"fixPacks": {
"title": "One-click Fix Packs",
"description": "Apply grouped fixes instantly"
},
"designSystem": {
"title": "Prevent design debt from coming back",
"description": "Generate governance rules from your fixes"
},
"deploy": {
"title": "Ship to GitHub. Deploy fast.",
"description": "Commit fixes directly to your repo"
},
"whiteLabel": {
"title": "White-label ready for agencies",
"description": "Rebrand Cynthia for your clients"
}
},
"demo": {
"title": "See it in action",
"overallScore": "Overall Score",
"issuesFound": "{count} issues found",
"topIssues": "Top Issues Preview",
"unlockPrompt": "Unlock full report to see all {count} issues and fix packs"
}
},
"welcome": {
"title": "Welcome to Onlook",
"title": "Welcome to Synthia",
"titleReturn": "Welcome back to Onlook",
"description": "A next-generation visual code editor that lets designers and product managers craft web experiences with AI.",
"alpha": "Alpha",
Expand Down Expand Up @@ -362,6 +405,66 @@
},
"reportIssue": "Report Issue",
"shortcuts": "Shortcuts"
},
"cynthia": {
"title": "Cynthia Design Audit",
"subtitle": "AI-powered design audit with actionable fixes",
"startNew": {
"title": "Start New Audit",
"description": "Enter a URL or select a component to audit",
"urlPlaceholder": "https://example.com",
"buttonRun": "Run Audit",
"buttonRunning": "Starting..."
},
"status": {
"title": "Audit Status",
"pending": "Pending",
"running": "Running",
"completed": "Completed",
"failed": "Failed",
"analyzing": "Analyzing your UI...",
"error": "Audit failed: {message}"
},
"score": {
"title": "Cynthia Score",
"issuesFound": "Found {count} {count, plural, one {issue} other {issues}}"
},
"issues": {
"title": "Top Issues (Teaser)",
"description": "Unlock full report for complete details and fix packs",
"whyMatters": "Why it matters:",
"impact": "Impact:",
"fix": "Fix:"
},
"unlock": {
"title": "Unlock Full Report",
"description": "Get the complete fix plan + one-click refactors",
"feature1": "Full UDEC breakdown with exact measurements",
"feature2": "One-click Fix Packs (layout, type, color, spacing, accessibility)",
"feature3": "Exportable patch + changelog for your repo",
"button": "Upgrade to Pro"
},
"severity": {
"critical": "Critical",
"major": "Major",
"minor": "Minor",
"info": "Info"
},
"axes": {
"CTX": "Context & Clarity",
"DYN": "Dynamic",
"LFT": "Layout & Flow",
"TYP": "Typography",
"CLR": "Color",
"GRD": "Grid",
"SPC": "Spacing",
"IMG": "Imagery",
"MOT": "Motion",
"ACC": "Accessibility",
"RSP": "Responsive",
"TRD": "Trends",
"EMO": "Emotional Impact"
}
}
}
}
Loading