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
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 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) |
File: src/contexts/AuthContext.jsx
Clean Supabase Auth integration with:
- Session listener via
onAuthStateChangethat keeps the user state in sync - Google OAuth with
redirectTo: window.location.originfor 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
File: src/store/whiteboardStore.js
Well-structured store with clear separation of concerns:
- Optimistic updates:
addObject,updateObject, anddeleteObjectupdate 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 inleaveBoard(). - Connection status tracking: The
connectedflag updates onSUBSCRIBED,CLOSED, andCHANNEL_ERRORchannel states, surfaced in the toolbar.
File: src/components/WhiteboardCanvas.jsx
Functional implementation of four drawing tools:
- Rectangle and circle use click-and-drag with live preview via
newObjectstate - Arrow draws point-to-point with Konva
Arrowcomponent - Text uses a prompt dialog (has issues; see Issue 4)
- Minimum-size validation prevents accidental micro-objects (5px threshold)
useCallbackon all mouse handlers prevents unnecessary re-renders
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
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
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`, { ... });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.
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">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.');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:
userchanges fromnullto the user object- The effect's cleanup runs
leaveBoard(), wiping all objects, users, and cursors - 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]);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/sdkFile: 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:
- Make the Stage
draggablefor click-and-drag panning (when the select tool is active and clicking on empty space) - Add an
onWheelhandler that adjustsscaleX/scaleYon the Stage for scroll-to-zoom - Update pointer position calculations to account for the Stage's scale and position offset
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.
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.
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.
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 = 200Files: 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.
Impact: The app is inaccessible to keyboard-only and screen reader users.
Specific gaps:
- All icon-only buttons in the toolbar lack
aria-labelattributes (they only havetitle) - The AuthModal has no
role="dialog", noaria-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).
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.
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)
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
}Netlify is ideal because the frontend is a static Vite build with no server-side rendering.
To deploy:
- Fix BUG 1 (API URL) -- otherwise AI commands will 404
- Add a
netlify.toml(see Issue 6) with build config and SPA redirects - Set these env vars in the Netlify dashboard:
VITE_SUPABASE_URLVITE_SUPABASE_ANON_KEYVITE_BACKEND_URL(set to deployed backend URL, e.g.,https://ai-whiteboard-backend.onrender.com)
- Connect GitHub repo, set base directory to
frontend/ - Build command:
npm run build - 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 can serve static sites but requires either a Dockerfile or Nixpacks detection:
- Fix BUG 1 (API URL)
- Option A: Add a minimal
Dockerfiletofrontend/that builds with Vite and serves withnginxorserve - Option B: Railway's Nixpacks can auto-detect Vite projects; set the root directory to
frontend/and ensure astartscript exists (e.g.,"start": "npx serve dist") - Set env vars in Railway dashboard (same as Netlify list above)
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:
- Fix BUG 1 (API URL)
- Set env vars in Vercel dashboard (
VITE_SUPABASE_URL,VITE_SUPABASE_ANON_KEY,VITE_BACKEND_URL) - 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.
- BUG 1: Use
VITE_BACKEND_URLenv var for the/api/aiendpoint - BUG 2: Fix cursor broadcast payload to include
userId,userName,x,y - ISSUE 6: Add SPA redirect rules / deployment config (netlify.toml or equivalent)
- ISSUE 11: Disable source maps in production build
- ISSUE 2: Add pan/zoom to canvas (spec requirement)
- ISSUE 3: Add sticky note renderer (spec requirement)
- BUG 3: Fix AuthModal close button positioning (
relativeon parent) - BUG 4: Fix Supabase error code handling in AuthModal
- BUG 5: Prevent board re-initialisation on auth state change
- ISSUE 1: Remove
@anthropic-ai/sdkfrom frontend dependencies - ISSUE 4: Replace
window.prompt()with inline text input - ISSUE 5: Add React error boundary
- ISSUE 7: Fix toolbar height mismatch (72px vs 120px)
- ISSUE 8: Add accessibility basics (aria-labels, focus trap, keyboard nav)
- ISSUE 9: Fail fast on missing Supabase env vars
- ISSUE 10: Add test coverage with Vitest
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
useCallbackfor 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/.jswith 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
useMemoimported but never used inWhiteboardCanvas.jsxCANVAS_WIDTHandCANVAS_HEIGHTconstants defined but never referenced (viewport size is calculated fromwindow.innerWidth/innerHeight)- Flat component structure will not scale; consider grouping by feature (e.g.,
components/canvas/,components/auth/,components/ai/)