Feature spec
IntermediateCategory
media-content
Build with AI
1–3 days with Lovable or FlutterFlow
Custom build
2–4 weeks custom dev
Running cost
$0–$25/mo (Supabase free tier covers most use cases)
Works on
Everything it takes to ship Video Annotation Tools — parts, prompts, and real costs.
Video annotation needs four pieces: a video player that exposes current timestamp, a canvas overlay for drawings (Fabric.js on web), a Supabase table for annotation storage, and Supabase Realtime for live collaboration. With Lovable you can ship a working annotator in 1–3 days for $0–$25/month. The canvas-over-video pointer events conflict is the bug that kills every first build.
What Video Annotation Tools Actually Are
Video annotation lets reviewers drop timestamped comments, draw freehand, or place arrows on specific video frames — all stored as structured data linked to the precise playback moment. The viewer can jump back to any annotation timestamp and see exactly what was being referenced. Design review teams, educators, sports coaches, and QA teams use this to give contextual feedback that would take paragraphs to describe in plain text. The technical core is a <canvas> element layered over a video player, with all drawing data serialized to JSON and stored in Supabase so collaborators see changes in real time. The product decisions that determine complexity are: real-time vs. async collaboration, the permission model (who can draw vs. who can only comment), and whether annotations need to be exported as PDF or JSON reports.
What users consider table stakes in 2026
- Click anywhere on the video timeline to drop a timestamped comment pin, visible as a colored dot on the scrubber
- Drawing overlay (pen, arrow, rectangle) synced to a specific timestamp — drawing appears when playback reaches that moment
- Sidebar list of all annotations sorted by timestamp with a 'jump to' button per entry
- Real-time updates when another collaborator adds an annotation — no manual refresh
- Permission model distinguishing viewers (read-only) from annotators (can add comments and drawings) from owners (can delete any annotation)
- Export annotations as structured JSON or a PDF report with video title, timestamp list, and drawing thumbnails
Anatomy of the Feature
Six components. Lovable handles the Supabase Realtime subscription and comment thread UI reliably. The Fabric.js canvas overlay and pointer-events passthrough are the parts that require manual attention.
Video player with timestamp sync
UIvideo.js (^8.x) on web for its rich currentTime API, seekTo() method, and cross-browser compatibility including HLS/DASH streams. On mobile, video_player Flutter package. The player exposes a currentTime property for reading the playhead position when a user clicks to annotate, and a seekTo() method for jumping to annotation timestamps from the sidebar.
Note: Plyr.js is a lightweight alternative to video.js with a cleaner API surface, but fewer format options. For simple MP4 annotation use cases, Plyr.js reduces bundle size by ~40 KB.
Annotation canvas overlay
UIFabric.js (web) rendered on a <canvas> element layered directly over the video with position: absolute and matching dimensions. Provides pen, arrow, and rectangle drawing tools with color and stroke width controls. Each completed drawing is serialized to Fabric.js JSON for storage. On mobile, flutter_drawing_board package provides equivalent functionality as a Flutter Custom Widget.
Note: The canvas must use CSS pointer-events: none during video playback mode and pointer-events: all during active annotation mode. This toggle is the most common missing piece in AI-generated implementations.
Timestamped comment thread
Dataannotation_comments Supabase table stores each annotation: video_id, user_id, timestamp_ms (milliseconds for precision), content (text), annotation_type ('comment' | 'drawing' | 'arrow'), drawing_data JSONB (Fabric.js serialized JSON), and color. The sidebar renders annotations sorted by timestamp_ms ascending, with avatar, text content, and a 'jump to' button per row.
Note: Store timestamps in milliseconds (not seconds) — video.js currentTime returns seconds as a float; multiply by 1000 and Math.round() when writing to the database.
Real-time sync
BackendSupabase Realtime subscription on the annotation_comments table filtered by video_id. New annotations from other collaborators arrive via the INSERT event callback and are appended to the local list without polling. UPDATE and DELETE events handle edit and removal operations.
Note: After a network interruption, Supabase Realtime replays recent events on reconnect — deduplicate incoming annotations by tracking seen IDs in a Set ref to prevent duplicate entries in the sidebar.
Timeline marker layer
UIA custom DOM overlay positioned absolutely over the video progress bar. Each annotation generates a colored dot at position (timestamp_ms / duration_ms) * 100%. Clicking a dot calls player.currentTime(timestamp_ms / 1000) to seek the video. Dots are color-coded by annotation type (blue = comment, red = drawing, orange = arrow).
Note: Use a separate div overlay rather than modifying video.js's internal progress bar — the internal structure varies between video.js versions and custom modifications break on upgrade.
Export service
BackendA Supabase Edge Function or Next.js Server Action that queries all annotation_comments for a video, aggregates them with video metadata, and returns structured JSON or triggers a PDF render via pdf-lib. The JSON export is a flat array of annotations suitable for downstream processing or archiving.
Note: pdf-lib runs in both Node.js and browser environments — client-side PDF generation avoids the need for a server action if the annotation count is under ~200 rows. For larger exports, generate server-side to avoid browser memory pressure.
The data model
Three tables: videos, annotation_comments with JSONB drawing data, and annotation_permissions for the role model. Run this in the Supabase SQL editor:
1create table public.videos (2 id uuid primary key default gen_random_uuid(),3 owner_id uuid references auth.users(id) on delete cascade not null,4 title text not null,5 asset_url text not null,6 duration_ms int,7 created_at timestamptz not null default now()8);910alter table public.videos enable row level security;1112create policy "Video owners and permitted users can view videos"13 on public.videos for select14 using (15 owner_id = auth.uid() or16 exists (17 select 1 from public.annotation_permissions18 where video_id = id and user_id = auth.uid()19 )20 );2122create policy "Video owners can manage their videos"23 on public.videos for all24 using (owner_id = auth.uid())25 with check (owner_id = auth.uid());2627create table public.annotation_comments (28 id uuid primary key default gen_random_uuid(),29 video_id uuid references public.videos(id) on delete cascade not null,30 user_id uuid references auth.users(id) on delete cascade not null,31 timestamp_ms int not null,32 content text,33 annotation_type text not null default 'comment'34 check (annotation_type in ('comment', 'drawing', 'arrow')),35 drawing_data jsonb,36 color text not null default '#3b82f6',37 created_at timestamptz not null default now()38);3940alter table public.annotation_comments enable row level security;4142create policy "Users with any role can view annotations"43 on public.annotation_comments for select44 using (45 video_id in (46 select video_id from public.annotation_permissions47 where user_id = auth.uid()48 ) or49 video_id in (50 select id from public.videos where owner_id = auth.uid()51 )52 );5354create policy "Annotators and owners can insert annotations"55 on public.annotation_comments for insert56 with check (57 user_id = auth.uid() and (58 video_id in (59 select video_id from public.annotation_permissions60 where user_id = auth.uid() and role in ('annotator', 'owner')61 ) or62 video_id in (63 select id from public.videos where owner_id = auth.uid()64 )65 )66 );6768create policy "Users can delete own annotations"69 on public.annotation_comments for delete70 using (user_id = auth.uid());7172create table public.annotation_permissions (73 video_id uuid references public.videos(id) on delete cascade not null,74 user_id uuid references auth.users(id) on delete cascade not null,75 role text not null check (role in ('viewer', 'annotator', 'owner')),76 primary key (video_id, user_id)77);7879alter table public.annotation_permissions enable row level security;8081create policy "Video owners manage permissions"82 on public.annotation_permissions for all83 using (84 video_id in (85 select id from public.videos where owner_id = auth.uid()86 )87 )88 with check (89 video_id in (90 select id from public.videos where owner_id = auth.uid()91 )92 );9394create index annotation_comments_video_time_idx95 on public.annotation_comments (video_id, timestamp_ms);Heads up: The composite index on (video_id, timestamp_ms) makes sidebar loading fast even at thousands of annotations per video. The annotation_permissions table drives the RLS policies — without it, all authenticated users could read all annotations.
Build it — pick your path
Each tool fits this feature differently. Switch tabs to compare fit, time, the exact steps, and a copy-paste prompt.
Full control over drawing tool precision, Fabric.js canvas performance, real-time conflict resolution, PDF export pipeline, and integration with professional video review APIs. Necessary for legal review, film production, or education platforms where annotation fidelity and export quality matter.
Step by step
- 1Next.js with Fabric.js v6, carefully coordinated z-index and pointer-events between video.js and the canvas layer, and requestAnimationFrame synchronization for drawing-to-playhead accuracy
- 2Supabase Realtime with Postgres-level conflict detection — last-write-wins for text edits, append-only for drawing data
- 3PDF export via pdf-lib generating a multi-page report: cover page, screenshot thumbnail per annotation, drawing overlay burned in, timestamp list
- 4Drawing data optimization: serialize only Fabric.js path data (not internal cache properties) to keep JSONB rows under 10 KB even for complex drawings
- 5Integration with Frame.io Review API or Vimeo Review API for teams already using those platforms
Where this path bites
- 2–4 week timeline; Fabric.js + video synchronization requires careful engineering to avoid requestAnimationFrame timing bugs
- PDF generation with burned-in drawings requires canvas-to-image conversion which has CORS constraints on video frame capture
Third-party services you'll need
Video annotation is primarily a Supabase feature — third-party services are optional for video hosting and export:
| Service | What it does | Free tier | Paid from |
|---|---|---|---|
| Supabase | Database for annotation storage, Realtime for live collaboration, Storage for video assets | Free tier: 2 projects, 500 MB DB, Realtime included | Pro $25/mo (approx) |
| video.js | Web video player with currentTime API and cross-browser format support | Open-source, free | Free |
| Fabric.js | Canvas drawing library for freehand, arrow, and rectangle annotation tools | Open-source, free | Free |
| pdf-lib | Client-side or server-side PDF generation for annotation export reports | Open-source, free | Free |
Swipe the table sideways to see pricing.
What it costs to run
Drag through the tiers to see how your monthly bill scales with users — no surprises later.
Estimated monthly running cost
Supabase free tier covers the DB storage and Realtime connection count at 100 concurrent users. Video hosting bandwidth depends on whether videos are in Supabase Storage or an external CDN.
Estimates use verified 2026 pricing and assume typical usage per user. Your real bill depends on activity, storage, and third-party plans.
What breaks when AI tools build this
The failures people actually hit on their first build — the symptom, why it happens, and the exact fix.
Canvas overlay blocks video controls
Symptom: Placing a full-size <canvas> element over the video with pointer-events: all intercepts every mouse click and prevents the video player's native controls (play button, seek bar, volume) from receiving events. The controls appear visually but clicking them does nothing — a disorienting experience that looks like a broken video player.
Fix: Set the canvas to CSS pointer-events: none during playback mode and switch to pointer-events: all only when an annotation mode is active. Wire this to a mode state variable toggled by the toolbar buttons. If using React, update the canvas element's className or inline style whenever mode changes. This single fix resolves the most common reported bug in canvas-over-video implementations.
Supabase Realtime duplicate annotations on reconnect
Symptom: After a brief network interruption (mobile switching from WiFi to cellular, for example), Supabase Realtime reconnects and replays recent INSERT events from the channel. Annotation comments that were already displayed in the sidebar appear a second time, causing duplicates that persist until the page is refreshed.
Fix: Track all received annotation IDs in a JavaScript Set stored in a ref: const seenIds = useRef(new Set()). In the Realtime INSERT callback, check if seenIds.current.has(payload.new.id) before adding to state. If already seen, discard. This prevents duplicates during reconnection while still displaying new genuine annotations from other users.
Drawing data inflates database rows
Symptom: Fabric.js serializes the full canvas object including internal _cacheCanvas properties, texture buffers, and rendering hints. A single freehand drawing stroke can serialize to 200–500 KB of JSON. At even moderate usage (100 annotations per video), this bloats the annotation_comments table and makes sidebar load times visibly slow.
Fix: Before storing, serialize only the essential Fabric.js properties: const stripped = canvas.toJSON(['type', 'path', 'stroke', 'strokeWidth', 'left', 'top', 'angle', 'scaleX', 'scaleY']). Remove any properties containing 'cache' or starting with '_'. This reduces a typical freehand stroke from ~300 KB to under 5 KB without losing any information needed to replay the drawing.
Missing RLS leaves annotations world-readable
Symptom: AI tools commonly scaffold the annotation_comments table without enabling Row Level Security (RLS). All annotations for all videos become readable by any authenticated user — a privacy failure that is invisible during single-user testing and only discovered when a user accidentally sees another company's review notes.
Fix: Always enable RLS on annotation_comments immediately after creating the table: ALTER TABLE public.annotation_comments ENABLE ROW LEVEL SECURITY. Then create policies that gate SELECT on membership in annotation_permissions for the video, plus video ownership. Do not ship without verifying RLS is active — run SELECT * FROM annotation_comments as a non-owner user in the Supabase SQL editor to confirm it returns zero rows.
flutter_drawing_board z-index conflict on mobile
Symptom: On mobile in the FlutterFlow implementation, the flutter_drawing_board Custom Widget captures all gesture events globally — including tap-to-play on the video_player widget beneath it. The user cannot toggle video playback while the drawing board is mounted, making the combined player+annotator feel frozen in playback mode.
Fix: Wrap the video_player in an AbsorbPointer widget with the absorbing property bound to a drawingModeActive App State variable. When drawingModeActive is true, AbsorbPointer lets drawing board receive all gestures. When false (playback mode), AbsorbPointer with absorbing: false passes gestures through to the video player. Toggle drawingModeActive from the annotation mode toolbar buttons.
Best practices
Set canvas pointer-events: none in playback mode from the very first build — retrofitting this after users report broken video controls wastes a full debugging session
Deduplicate Supabase Realtime events with a Set ref from day one — reconnection duplicates are guaranteed to occur in production and look like a data integrity bug
Store timestamp as milliseconds (not seconds) in the database — floating-point second values cause off-by-one errors in seek operations at long video durations
Serialize only essential Fabric.js properties before storing drawing_data — the difference between 300 KB and 5 KB per annotation row compounds dramatically at scale
Always verify RLS on annotation_comments with a cross-user test before launch — RLS omissions are invisible to the developer but expose all customer review data to every other user
Color-code annotation markers by type on the timeline scrubber — comment (blue), drawing (red), arrow (orange) — so reviewers can scan the annotation density at a glance
Highlight the active annotation in the sidebar within 500 ms of its timestamp during playback — the lack of this feels like the annotation feature isn't responding
When You Need Custom Development
Lovable covers async review workflows reliably. Certain annotation requirements need custom engineering:
- Frame-accurate drawing that locks to a specific video frame rather than a timestamp range — this requires frame extraction from the video stream and annotation storage keyed to exact frame numbers
- Integration with professional video review platforms (Frame.io API, Vimeo Review API) for teams that already use those tools for asset management
- AI-powered auto-annotation that identifies objects, speakers, or actions in the video and creates pre-populated annotation suggestions
- Multi-track annotation where audio, video, and subtitle tracks each have independent annotation layers with their own permissions and export formats
RapidDev builds these features for production
Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.
Frequently asked questions
What's the difference between timestamped comments and drawing annotations on video?
Timestamped comments are text notes linked to a playback position — they appear in a sidebar and let reviewers jump to the referenced moment. Drawing annotations are visual overlays (lines, shapes, freehand) rendered directly on the video frame at a specific timestamp. Both are stored in the same annotation_comments table using different annotation_type values ('comment' vs 'drawing'). Most review workflows use both: text comments for context and drawings to point at the exact element being discussed.
Can multiple people annotate the same video at the same time?
Yes — Supabase Realtime subscriptions deliver INSERT events from other users within 100–300 ms. Each collaborator's annotations appear in every other viewer's sidebar in near real-time. Conflict resolution is simple: annotations are append-only (no two users edit the same row), so concurrent annotation is inherently safe. The only edge case is the Realtime reconnection duplicate bug — deduplicate by tracking annotation IDs in a Set ref.
How do I show annotation markers on the video timeline scrubber?
Position a div absolutely over the video progress bar element. For each annotation, compute the left position as (timestamp_ms / duration_ms) * 100%. Render a small colored dot at that position with position: absolute and left: {percent}%. The dot needs pointer-events: all and a click handler that calls player.currentTime(timestamp_ms / 1000) to seek. Don't modify video.js internal DOM — overlay a separate div that matches the progress bar's dimensions.
Can I export video annotations as a PDF or structured file?
Yes. For JSON: query all annotation_comments for the video and serialize to JSON — a Next.js Server Action or Supabase Edge Function works well for this. For PDF: use pdf-lib to create a document with one page per annotation, including the timestamp, content text, and a thumbnail of the drawing (convert Fabric.js canvas to a PNG data URL and embed it in the PDF). For simple annotation lists (no drawings), pdf-lib can run client-side in the browser without a server.
How do I prevent annotations from one user being visible to others?
The annotation_permissions table controls who can see which video's annotations. The RLS policy on annotation_comments checks that the querying user's auth.uid() appears in annotation_permissions with any role for that video_id. A user who is not in annotation_permissions for a video gets zero rows from any query on annotation_comments for that video. Add users to annotation_permissions with their role when you share a video with them.
Does Supabase Realtime work for live video annotation collaboration?
Yes, Supabase Realtime is well-suited for annotation collaboration — annotations are low-frequency events (a few per minute per user), well within Realtime's capacity. The main limitation is concurrent connection count: the free tier supports ~200 concurrent Realtime connections, Pro supports ~500. For a public video review platform with thousands of simultaneous viewers, you'd need Supabase Enterprise or a dedicated WebSocket server.
How do I make the drawing canvas not block the video player controls?
Set the canvas element's CSS pointer-events property to 'none' when in playback mode and 'all' when in an annotation mode. This is the single most important implementation detail for this feature. In React: style={{ pointerEvents: mode === 'playback' ? 'none' : 'all' }} on the canvas element. Test by clicking the video play button and the volume control while the canvas is mounted — both should work in playback mode.
What's the best library for drawing overlays on a web video player?
Fabric.js is the best choice for video annotation — it provides a complete Object Model for pen, shape, and text tools with JSON serialization built in, making it straightforward to store and replay annotations. Konva.js is a capable alternative with better TypeScript types in version 9+. For simpler use cases (just freehand drawing, no shapes), the browser's native Canvas 2D API with Path2D is sufficient and has zero dependencies — but you lose the object-level manipulation that Fabric.js provides for editing and deleting individual annotations.
Need this feature production-ready?
RapidDev builds video annotation tools into real apps — auth, database, payments — at $13K–$25K.
Book a free consultation30-min call. No commitment.