Skip to content

Instantly share code, notes, and snippets.

@decagondev
Created February 18, 2026 18:53
Show Gist options
  • Select an option

  • Save decagondev/efe68f7ea8805b19bdf111e514cdeea4 to your computer and use it in GitHub Desktop.

Select an option

Save decagondev/efe68f7ea8805b19bdf111e514cdeea4 to your computer and use it in GitHub Desktop.

Frontend Audit Report -- CollabBoard MVP

Date: 2026-02-18 Scope: frontend/ directory, build config, deployment configs Purpose: Evaluate frontend readiness against the CollabBoard MVP requirements (G4 Week 1) with a view to deploy on Netlify, Railway, or Vercel


Architecture Summary

The frontend is a React 18 single-page application built with Vite 5, using Konva for 2D canvas rendering, Zustand for state management, Supabase (Auth + Realtime + Database), and Tailwind CSS 3 for styling. There is no client-side router; board selection is handled via URL query parameters.

frontend/
├── index.html                    ← Entry HTML
├── package.json                  ← Dependencies and scripts
├── vite.config.js                ← Vite build configuration
├── tailwind.config.js            ← Tailwind CSS content paths
├── postcss.config.js             ← PostCSS plugins (tailwindcss, autoprefixer)
├── .env.example                  ← Local env var template
├── .env.production.example       ← Production env var template
└── src/
    ├── main.jsx                  ← React 18 createRoot entry
    ├── App.jsx                   ← Root component, board init, layout
    ├── index.css                 ← Tailwind directives, global reset
    ├── components/
    │   ├── WhiteboardCanvas.jsx  ← Konva Stage/Layer, drawing tools, cursors
    │   ├── Toolbar.jsx           ← Tool selection, delete, auth, status
    │   ├── AuthModal.jsx         ← Sign in / sign up modal with Google OAuth
    │   └── AIPanel.jsx           ← Floating AI assistant chat panel
    ├── contexts/
    │   └── AuthContext.jsx       ← Supabase Auth provider + hooks
    ├── services/
    │   └── supabase.js           ← Supabase client initialisation
    └── store/
        └── whiteboardStore.js    ← Zustand store: objects, users, cursors, AI

Data flow: Object CRUD and cursor positions sync via Supabase Realtime (Postgres Changes + Broadcast + Presence). AI commands go through a REST POST /api/ai call to the backend. Authentication uses Supabase Auth with JWT tokens passed in the Authorization header.


MVP Requirements Checklist

MVP Requirement Frontend Support Status
Infinite board with pan/zoom Fixed 3000x2000 canvas, no pan/zoom FAIL (see Issue 2)
Sticky notes with editable text No sticky note type in renderer FAIL (see Issue 3)
At least one shape type Rectangle, circle, arrow, text tools PASS
Create, move, edit objects Draw tools + drag-to-move + optimistic DB sync PASS
Real-time sync between 2+ users Supabase Realtime postgres_changes for objects PASS
Multiplayer cursors with name labels Cursor rendering exists but payload is broken PARTIAL (see Bug 2)
Presence awareness Supabase Presence with user count in toolbar PASS
User authentication Supabase Auth: email/password + Google OAuth PASS
Deployed and publicly accessible No frontend deployment config; minimal vercel.json at root FAIL (see Deployment)

What Is Working Well

1. Authentication Context

File: src/contexts/AuthContext.jsx

Clean Supabase Auth integration with:

  • Session listener via onAuthStateChange that keeps the user state in sync
  • Google OAuth with redirectTo: window.location.origin for correct post-login routing
  • getIdToken() helper for extracting the access token for API calls
  • Loading gate ({!loading && children}) that prevents child components from rendering before the initial session check completes

2. Zustand State Management

File: src/store/whiteboardStore.js

Well-structured store with clear separation of concerns:

  • Optimistic updates: addObject, updateObject, and deleteObject update the UI immediately, then persist to Supabase. The Realtime INSERT handler deduplicates to prevent double-rendering.
  • Realtime subscriptions: Postgres Changes for object CRUD, Broadcast for cursor positions, Presence for active user tracking -- all wired up in joinBoard() with proper cleanup in leaveBoard().
  • Connection status tracking: The connected flag updates on SUBSCRIBED, CLOSED, and CHANNEL_ERROR channel states, surfaced in the toolbar.

3. Drawing Tools

File: src/components/WhiteboardCanvas.jsx

Functional implementation of four drawing tools:

  • Rectangle and circle use click-and-drag with live preview via newObject state
  • Arrow draws point-to-point with Konva Arrow component
  • Text uses a prompt dialog (has issues; see Issue 4)
  • Minimum-size validation prevents accidental micro-objects (5px threshold)
  • useCallback on all mouse handlers prevents unnecessary re-renders

4. UI and Styling

Polished Tailwind-based design:

  • Consistent purple-to-blue gradient branding across toolbar, AI panel, and auth modal
  • Connection status indicator (green/red) and active user count in the toolbar
  • Disabled states on buttons (delete when nothing selected, submit when AI processing)
  • Smooth transitions and shadow effects on hover/focus
  • Lucide React icons used consistently throughout

5. AI Panel

File: src/components/AIPanel.jsx

Good user experience:

  • Floating toggle button with spinner during AI processing
  • Slide-out panel with message history, timestamps, and action summaries
  • Example command suggestions for discoverability
  • Auto-scroll to latest message via useEffect + scrollIntoView
  • Input disabled during processing to prevent double-submission

Critical Bugs

BUG 1: API endpoint hardcoded as relative /api/ai -- will 404 in production

File: src/store/whiteboardStore.js, line 275 Severity: BROKEN -- AI features will not work when frontend and backend are deployed to separate domains

The AI command handler uses a relative URL:

const res = await fetch('/api/ai', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${token}`,
  },
  body: JSON.stringify({ boardId, command }),
});

.env.example defines VITE_BACKEND_URL=http://localhost:8080 and .env.production.example defines VITE_BACKEND_URL=https://ai-whiteboard-backend.onrender.com, but neither is ever read in the code. The relative /api/ai path only works if a dev proxy or same-origin deployment is configured, and neither is set up in vite.config.js or any deployment config.

What to do: Import the backend URL from the environment and use it in the fetch call:

const backendUrl = import.meta.env.VITE_BACKEND_URL || '';
const res = await fetch(`${backendUrl}/api/ai`, { ... });

BUG 2: Cursor broadcast payload mismatch -- remote cursors will not render

File: src/store/whiteboardStore.js, lines 246-253 (sender) vs lines 118-124 (receiver) Severity: BROKEN -- multiplayer cursors are a core MVP feature and they silently fail

The updateCursor function sends the raw pointer position as the payload:

updateCursor: (position) => {
  const { _channel } = get();
  if (!_channel) return;
  _channel.send({
    type: 'broadcast',
    event: 'cursor_move',
    payload: position,   // sends { x, y } only
  });
},

But the broadcast listener on line 118 expects payload.userId, payload.x, payload.y, and payload.userName:

.on('broadcast', { event: 'cursor_move' }, ({ payload }) => {
  set((state) => ({
    cursors: {
      ...state.cursors,
      [payload.userId]: { x: payload.x, y: payload.y, userName: payload.userName }
    }
  }));
})

Since payload.userId is undefined, every cursor update writes to cursors[undefined], overwriting previous entries. No remote cursor will display correctly.

What to do: Build a complete payload in updateCursor that includes the current user's ID and name:

updateCursor: async (position) => {
  const { _channel } = get();
  if (!_channel) return;
  const { data: { session } } = await supabase.auth.getSession();
  const userId = session?.user?.id || 'anon';
  const userName = session?.user?.user_metadata?.display_name || 'Guest';
  _channel.send({
    type: 'broadcast',
    event: 'cursor_move',
    payload: { userId, userName, x: position.x, y: position.y },
  });
},

Cache the user info to avoid calling getSession() on every mouse move, or retrieve it once in joinBoard and store it in the Zustand state.

BUG 3: AuthModal close button not positioned correctly

File: src/components/AuthModal.jsx, lines 200-204 Severity: UI DEFECT -- close button floats to the wrong position

The close button uses absolute top-4 right-4:

<button
  onClick={onClose}
  className="absolute top-4 right-4 text-gray-400 hover:text-gray-600"
></button>

Its parent div (<div className="bg-white rounded-2xl shadow-2xl w-full max-w-md p-8">) is not position: relative. The button positions relative to the nearest positioned ancestor, which is the outer fixed-position overlay, placing it in the top-right corner of the viewport rather than the top-right corner of the modal card.

What to do: Add relative to the modal content div's class list:

<div className="relative bg-white rounded-2xl shadow-2xl w-full max-w-md p-8">

BUG 4: Auth error codes use Firebase format, not Supabase

File: src/components/AuthModal.jsx, lines 31-41 Severity: UX DEFECT -- users always see the generic "Authentication failed" message

The error handler matches Firebase-style error codes:

err.code === 'auth/email-already-in-use'
  ? 'Email already in use'
  : err.code === 'auth/invalid-email'
  ? 'Invalid email address'
  : ...
  : 'Authentication failed. Please try again.'

Supabase Auth errors do not use err.code with auth/ prefixes. They return error objects with a message string (e.g., "User already registered", "Invalid login credentials"). Every Supabase error will fall through to the generic fallback.

What to do: Match on err.message strings that Supabase actually returns, or display err.message directly since Supabase error messages are already user-readable:

setError(err.message || 'Authentication failed. Please try again.');

BUG 5: Board re-initialises on every auth state change

File: src/App.jsx, lines 14-48 Severity: UX DEFECT -- signing in wipes the current board and creates a new one

The useEffect in AppContent depends on [user, joinBoard, leaveBoard]. When a user signs in:

  1. user changes from null to the user object
  2. The effect's cleanup runs leaveBoard(), wiping all objects, users, and cursors
  3. The effect body runs again, calling initBoard() which creates a new board for the authenticated user

Any objects drawn while anonymous are lost, and all other users on the same board see the signed-in user disconnect.

What to do: Separate board initialisation from auth state. Track the board ID in a ref or state variable and skip re-init if a board is already active:

const boardIdRef = useRef(null);

useEffect(() => {
  const initBoard = async () => {
    if (boardIdRef.current) return;
    const params = new URLSearchParams(window.location.search);
    let boardId = params.get('board');
    // ... rest of init logic ...
    boardIdRef.current = boardId;
    await joinBoard(boardId);
  };
  initBoard();
  return () => { leaveBoard(); boardIdRef.current = null; };
}, [joinBoard, leaveBoard]);

Issues That Need Attention

ISSUE 1: @anthropic-ai/sdk in frontend dependencies

File: package.json, line 11 Impact: Unnecessary bundle bloat and potential secret exposure concern

The Anthropic SDK (@anthropic-ai/sdk) is listed as a production dependency. This is a server-side SDK that expects an ANTHROPIC_API_KEY environment variable. It is never imported in any frontend file. Including it increases the production bundle size and may trigger security scanner warnings.

What to do: Remove it from dependencies:

npm uninstall @anthropic-ai/sdk

ISSUE 2: No pan/zoom on canvas (MVP requirement)

File: src/components/WhiteboardCanvas.jsx Impact: The spec requires "Infinite board with pan/zoom". The current canvas is a fixed 3000x2000px Konva Stage inside a scrollable div. There is no infinite canvas, no drag-to-pan, and no scroll-to-zoom.

What to do: Add panning and zooming to the Konva Stage:

  1. Make the Stage draggable for click-and-drag panning (when the select tool is active and clicking on empty space)
  2. Add an onWheel handler that adjusts scaleX/scaleY on the Stage for scroll-to-zoom
  3. Update pointer position calculations to account for the Stage's scale and position offset

ISSUE 3: No sticky note object type in renderer

File: src/components/WhiteboardCanvas.jsx, renderObject function Impact: MVP spec requires "Sticky notes with editable text". The renderObject switch handles rectangle, circle, text, and arrow but has no sticky_note case. Even if the backend adds a create_sticky_note AI tool (per BACKEND-AUDIT Issue 1), the frontend cannot render it.

What to do: Add a sticky_note case in renderObject using a Konva Group containing a Rect (colored background with rounded corners) and a Text (content overlay). Also add a sticky note drawing tool to the toolbar.

ISSUE 4: Text tool uses window.prompt() -- poor UX

File: src/components/WhiteboardCanvas.jsx, line 73 Impact: prompt('Enter text:') is a blocking native browser dialog. It provides a jarring UX on desktop and is suppressed entirely on some mobile browsers and embedded WebViews, making the text tool unusable.

What to do: Replace with an inline text editing component. Options include a positioned <input> overlay at the click location, or a small modal dialog consistent with the existing UI style.

ISSUE 5: No error boundary

Impact: Any unhandled JavaScript error in a React component (e.g., a Konva rendering error from malformed object data, or a null reference from a race condition) will crash the entire app with a blank white screen and no recovery path.

What to do: Add a React ErrorBoundary class component at the App level that catches render errors and displays a fallback UI with a "Reload" button. Consider a separate boundary around the WhiteboardCanvas so toolbar and auth remain functional if only the canvas crashes.

ISSUE 6: No SPA redirect rules for static hosting

Impact: The app uses URL query parameters (?board=<id>) for board identification. While query-parameter routing does not strictly need SPA redirects (unlike path-based routing), adding a catch-all redirect is still best practice for robustness and ensures any future path-based routes work. Currently there is no netlify.toml, no _redirects file, and no redirect config for Railway.

What to do: Add a netlify.toml at the repo root (or in frontend/):

[build]
  base = "frontend"
  command = "npm run build"
  publish = "dist"

[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200

ISSUE 7: Toolbar height mismatch between App and Canvas

Files: src/App.jsx line 53, src/components/WhiteboardCanvas.jsx line 12 Impact: Layout inconsistency causing canvas overflow or gap

App.jsx applies pt-[72px] padding to the canvas container, but WhiteboardCanvas.jsx subtracts 120 pixels from the viewport height for the toolbar. The actual toolbar height (based on px-6 py-3 and content) is approximately 72px. The 120px subtraction leaves a 48px dead zone at the bottom of the viewport.

What to do: Use the same value in both files. Either define a shared constant (e.g., TOOLBAR_HEIGHT = 72) or use a CSS custom property (--toolbar-height) referenced in both Tailwind classes and JavaScript.

ISSUE 8: No accessibility

Impact: The app is inaccessible to keyboard-only and screen reader users.

Specific gaps:

  • All icon-only buttons in the toolbar lack aria-label attributes (they only have title)
  • The AuthModal has no role="dialog", no aria-modal="true", no focus trap, and no Escape key handler
  • No keyboard shortcuts for tool selection
  • The AI panel toggle button has no accessible name beyond title="AI Assistant"
  • Canvas objects are not reachable via keyboard

What to do: At minimum, add aria-label to all icon-only buttons, add role="dialog" and aria-modal="true" to the AuthModal, add a focus trap and Escape-to-close behaviour, and add keyboard shortcut labels (e.g., R for rectangle, C for circle).

ISSUE 9: Supabase client silently fails on missing env vars

File: src/services/supabase.js, lines 6-8 Impact: Missing VITE_SUPABASE_URL or VITE_SUPABASE_ANON_KEY logs a console error but proceeds to call createClient(undefined, undefined). This creates a broken client that will crash with an unhelpful error on the first API call.

What to do: Throw an error immediately if the required environment variables are missing, or render a visible configuration error screen instead of the app. This makes deployment misconfiguration obvious rather than producing obscure runtime errors.

ISSUE 10: No tests

Impact: No test files exist, no test framework is configured, and package.json has no test script. There is no automated verification of any functionality.

What to do: Add Vitest (the Vite-native test runner) and @testing-library/react. At minimum, write tests for:

  • Zustand store actions (addObject, updateObject, deleteObject)
  • Auth context behaviour (sign in, sign out, loading state)
  • Component rendering (toolbar tool selection, AI panel open/close)

ISSUE 11: Source maps enabled in production build

File: vite.config.js, line 11 Impact: sourcemap: true includes full source maps in the production build output. Anyone with browser DevTools can view the original source code, including comments, variable names, and application logic.

What to do: Set sourcemap: false for production, or use 'hidden' to generate source maps for error tracking services (like Sentry) without serving them publicly:

build: {
  outDir: 'dist',
  sourcemap: false
}

Deployment Readiness

Netlify (Recommended -- simplest path for a static SPA)

Netlify is ideal because the frontend is a static Vite build with no server-side rendering.

To deploy:

  1. Fix BUG 1 (API URL) -- otherwise AI commands will 404
  2. Add a netlify.toml (see Issue 6) with build config and SPA redirects
  3. Set these env vars in the Netlify dashboard:
    • VITE_SUPABASE_URL
    • VITE_SUPABASE_ANON_KEY
    • VITE_BACKEND_URL (set to deployed backend URL, e.g., https://ai-whiteboard-backend.onrender.com)
  4. Connect GitHub repo, set base directory to frontend/
  5. Build command: npm run build
  6. Publish directory: frontend/dist

CORS note: The backend's ALLOWED_ORIGINS env var must include the Netlify URL (e.g., https://your-app.netlify.app). This is set in the backend's Render/Railway dashboard.

Railway (Alternative)

Railway can serve static sites but requires either a Dockerfile or Nixpacks detection:

  1. Fix BUG 1 (API URL)
  2. Option A: Add a minimal Dockerfile to frontend/ that builds with Vite and serves with nginx or serve
  3. Option B: Railway's Nixpacks can auto-detect Vite projects; set the root directory to frontend/ and ensure a start script exists (e.g., "start": "npx serve dist")
  4. Set env vars in Railway dashboard (same as Netlify list above)

Vercel (Existing config)

A vercel.json already exists at the repo root:

{
  "buildCommand": "cd frontend && npm run build",
  "outputDirectory": "frontend/dist",
  "installCommand": "cd frontend && npm install"
}

This is functional. Vercel auto-handles SPA rewrites for client-side apps, so no explicit redirect config is needed. To deploy:

  1. Fix BUG 1 (API URL)
  2. Set env vars in Vercel dashboard (VITE_SUPABASE_URL, VITE_SUPABASE_ANON_KEY, VITE_BACKEND_URL)
  3. Connect GitHub repo and deploy

Note: Vercel is the lowest-friction option since config already exists, but Netlify offers a more generous free tier for bandwidth.


Priority Summary

Must Fix Before Deployment (Blockers)

  1. BUG 1: Use VITE_BACKEND_URL env var for the /api/ai endpoint
  2. BUG 2: Fix cursor broadcast payload to include userId, userName, x, y
  3. ISSUE 6: Add SPA redirect rules / deployment config (netlify.toml or equivalent)
  4. ISSUE 11: Disable source maps in production build

Should Fix for MVP Compliance

  1. ISSUE 2: Add pan/zoom to canvas (spec requirement)
  2. ISSUE 3: Add sticky note renderer (spec requirement)
  3. BUG 3: Fix AuthModal close button positioning (relative on parent)
  4. BUG 4: Fix Supabase error code handling in AuthModal
  5. BUG 5: Prevent board re-initialisation on auth state change

Nice to Have

  1. ISSUE 1: Remove @anthropic-ai/sdk from frontend dependencies
  2. ISSUE 4: Replace window.prompt() with inline text input
  3. ISSUE 5: Add React error boundary
  4. ISSUE 7: Fix toolbar height mismatch (72px vs 120px)
  5. ISSUE 8: Add accessibility basics (aria-labels, focus trap, keyboard nav)
  6. ISSUE 9: Fail fast on missing Supabase env vars
  7. ISSUE 10: Add test coverage with Vitest

Code Quality Notes

Strengths:

  • Clean Zustand store with well-separated concerns (board, collaboration, AI)
  • Optimistic updates with server-side deduplication prevent UI flicker
  • Consistent Tailwind-based design system with gradient branding
  • Supabase Realtime subscription setup is thorough (Postgres Changes + Broadcast + Presence)
  • Auth loading gate prevents premature renders
  • Good use of useCallback for performance-sensitive mouse handlers

Areas for improvement:

  • No JSDoc or structured documentation on any exported function, component, or hook
  • No TypeScript -- all files are .jsx/.js with no type safety
  • No error boundary or global error handling strategy (errors are only logged to console)
  • Single-line comments used in several files instead of structured documentation
  • No test coverage of any kind
  • useMemo imported but never used in WhiteboardCanvas.jsx
  • CANVAS_WIDTH and CANVAS_HEIGHT constants defined but never referenced (viewport size is calculated from window.innerWidth/innerHeight)
  • Flat component structure will not scale; consider grouping by feature (e.g., components/canvas/, components/auth/, components/ai/)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment