# How to Add Video Annotation Tools to Your App — Copy-Paste Prompts Inside

- Tool: App Features
- Last updated: July 2026

## TL;DR

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.

## 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** (ui): video.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.
- **Annotation canvas overlay** (ui): Fabric.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.
- **Timestamped comment thread** (data): annotation_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.
- **Real-time sync** (backend): Supabase 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.
- **Timeline marker layer** (ui): A 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).
- **Export service** (backend): A 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.

## 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:

```sql
create table public.videos (
  id uuid primary key default gen_random_uuid(),
  owner_id uuid references auth.users(id) on delete cascade not null,
  title text not null,
  asset_url text not null,
  duration_ms int,
  created_at timestamptz not null default now()
);

alter table public.videos enable row level security;

create policy "Video owners and permitted users can view videos"
  on public.videos for select
  using (
    owner_id = auth.uid() or
    exists (
      select 1 from public.annotation_permissions
      where video_id = id and user_id = auth.uid()
    )
  );

create policy "Video owners can manage their videos"
  on public.videos for all
  using (owner_id = auth.uid())
  with check (owner_id = auth.uid());

create table public.annotation_comments (
  id uuid primary key default gen_random_uuid(),
  video_id uuid references public.videos(id) on delete cascade not null,
  user_id uuid references auth.users(id) on delete cascade not null,
  timestamp_ms int not null,
  content text,
  annotation_type text not null default 'comment'
    check (annotation_type in ('comment', 'drawing', 'arrow')),
  drawing_data jsonb,
  color text not null default '#3b82f6',
  created_at timestamptz not null default now()
);

alter table public.annotation_comments enable row level security;

create policy "Users with any role can view annotations"
  on public.annotation_comments for select
  using (
    video_id in (
      select video_id from public.annotation_permissions
      where user_id = auth.uid()
    ) or
    video_id in (
      select id from public.videos where owner_id = auth.uid()
    )
  );

create policy "Annotators and owners can insert annotations"
  on public.annotation_comments for insert
  with check (
    user_id = auth.uid() and (
      video_id in (
        select video_id from public.annotation_permissions
        where user_id = auth.uid() and role in ('annotator', 'owner')
      ) or
      video_id in (
        select id from public.videos where owner_id = auth.uid()
      )
    )
  );

create policy "Users can delete own annotations"
  on public.annotation_comments for delete
  using (user_id = auth.uid());

create table public.annotation_permissions (
  video_id uuid references public.videos(id) on delete cascade not null,
  user_id uuid references auth.users(id) on delete cascade not null,
  role text not null check (role in ('viewer', 'annotator', 'owner')),
  primary key (video_id, user_id)
);

alter table public.annotation_permissions enable row level security;

create policy "Video owners manage permissions"
  on public.annotation_permissions for all
  using (
    video_id in (
      select id from public.videos where owner_id = auth.uid()
    )
  )
  with check (
    video_id in (
      select id from public.videos where owner_id = auth.uid()
    )
  );

create index annotation_comments_video_time_idx
  on public.annotation_comments (video_id, timestamp_ms);
```

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 paths

### Lovable — fit 4/10, 1–2 days

Lovable handles the Supabase Realtime subscription, comment thread UI, and timeline marker rendering reliably. The Fabric.js canvas overlay needs an explicit prompt specifying the pointer-events: none passthrough and z-index layering. Expect one follow-up prompt to fix canvas-video interaction.

1. Create a new Lovable project with Lovable Cloud enabled and run the annotation data model SQL in the Supabase SQL editor
2. Paste the prompt below — Lovable will scaffold the video.js player, Supabase Realtime subscription, annotation sidebar, and timeline markers in Agent Mode
3. After generation, verify the canvas pointer-events behavior: switch between playback mode and draw mode and confirm that clicking the video play/pause control works in playback mode
4. If video controls are blocked by the canvas, add a follow-up prompt: 'Set canvas pointer-events to none during playback mode and pointer-events all during draw/comment mode'
5. Publish and test with two browser windows signed in as different users to verify real-time collaboration

Starter prompt:

```
Build a video annotation feature. Embed a video player using video.js with a custom <canvas> overlay using Fabric.js for drawing tools. The canvas must have pointer-events: none during playback mode and pointer-events: all during annotation mode. Implement three annotation modes toggled by a toolbar: 'Comment' mode (click to place a pin at player.currentTime() in seconds, converted to milliseconds), 'Draw' mode (freehand pen on Fabric.js canvas at the current timestamp), 'Arrow' mode (click-drag to place an arrow overlay). Show colored dots on the video progress bar at each annotation's (timestamp_ms / duration_ms) * 100% position; clicking a dot seeks to that timestamp. Annotation sidebar: fetch annotation_comments from Supabase filtered by video_id, sorted by timestamp_ms ascending; show avatar, content text, and a 'Jump to' button per entry that calls player.currentTime(ms/1000). Store each annotation in Supabase annotation_comments with user_id, video_id, timestamp_ms (Math.round(currentTime * 1000)), content, annotation_type, drawing_data (Fabric.js JSON), and color. Subscribe to Supabase Realtime on annotation_comments filtered by video_id and append new annotations on INSERT events; deduplicate by tracking IDs in a Set. During playback, highlight the annotation in the sidebar when playback passes within 500 ms of its timestamp_ms. Handle states: idle / comment-mode / draw-mode / saving / loading-annotations / error.
```

Limitations:

- Fabric.js canvas pointer-events conflict with the video.js native controls — explicitly prompt for the pointer-events toggle or video controls become unclickable
- Lovable may generate stale Supabase Realtime subscription syntax — verify the subscription uses the current supabase.channel().on() API
- Complex drawing annotations (thick brushes, many paths) can generate large drawing_data JSONB values — monitor database row sizes after testing

### V0 — fit 3/10, 1–2 days

V0 produces clean annotation sidebar and timeline marker UI in Next.js. Fabric.js canvas integration and Supabase Realtime subscription code are hit-or-miss and frequently need manual corrections — plan for a few follow-up prompts.

1. Prompt V0 for the annotation sidebar component and timeline marker overlay separately before prompting for the full integration
2. Add Supabase environment variables in the Vars panel (NEXT_PUBLIC_SUPABASE_URL and anon key)
3. Run the data model SQL in the Supabase SQL editor
4. Manually add Fabric.js and wire the canvas over the video — V0 often skips this or generates incorrect import syntax for Fabric.js v6
5. Verify the Realtime subscription syntax by checking the Supabase JavaScript client docs — V0 may generate deprecated on('*') syntax

Starter prompt:

```
Create a Next.js video annotation system. Component 1: VideoAnnotator — client component wrapping a video.js player with a Fabric.js canvas overlay; canvas pointer-events: none in playback mode, pointer-events: all in annotation mode; toolbar with Comment / Draw / Arrow mode buttons. Component 2: AnnotationSidebar — fetches annotation_comments from Supabase for the given videoId; displays entries sorted by timestamp_ms with a 'Jump to' button each; highlights the nearest annotation as video plays. Component 3: TimelineMarkers — renders colored dots absolutely positioned over the video progress bar at (timestamp_ms / duration_ms * 100)%; clicking a dot calls seekTo. Supabase Realtime: subscribe to annotation_comments channel filtered by videoId and append INSERT events to the sidebar list, deduplicating by ID using a Set ref. Store annotation to Supabase on submit with video_id, user_id, timestamp_ms, content, annotation_type, drawing_data (Fabric.js JSON), color. Handle states: idle / comment-mode / draw-mode / saving / error.
```

Limitations:

- V0 does not auto-provision Supabase — run the data model SQL manually and set env vars in the Vars panel
- Supabase Realtime subscription code from V0 is often syntactically stale (deprecated .subscribe() callback pattern) — verify against current Supabase docs
- Fabric.js v6 import syntax changed significantly from v5 — V0 may generate v5-style imports that throw module errors

### Flutterflow — fit 3/10, 2–4 days

FlutterFlow wires video_player, Supabase annotation queries, and the comment list well in the visual builder. The flutter_drawing_board drawing overlay requires a Custom Widget with manual Dart registration — this is the blocker that separates FlutterFlow from custom dev for this feature.

1. Add video_player, flutter_drawing_board, supabase_flutter packages in FlutterFlow Settings → Pub Dependencies
2. Build the video player screen: add video_player widget, bind to a video URL from Supabase; add a horizontal gesture detector to capture taps for timestamped comments
3. Register flutter_drawing_board as a Custom Widget in FlutterFlow: paste the Custom Widget Dart code that wraps DrawingBoard and exposes onDrawingComplete callback with the serialized drawing data
4. Build the annotation list using a Supabase Backend Query on annotation_comments filtered by video_id; display in a ListView with timestamp, content, and a seek button
5. Add Realtime subscription via a Custom Action that subscribes to the annotation_comments channel and calls setState to update the list on INSERT events

Limitations:

- flutter_drawing_board Custom Widget registration requires hand-written Dart code — no visual builder support for drawing tool configuration
- Supabase Realtime in FlutterFlow requires a Custom Action; the visual Action editor does not expose channel subscription natively
- Z-index management between video_player and flutter_drawing_board requires an AbsorbPointer wrapper with manual state toggling

### Custom — fit 5/10, 2–4 weeks

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.

1. Next.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
2. Supabase Realtime with Postgres-level conflict detection — last-write-wins for text edits, append-only for drawing data
3. PDF export via pdf-lib generating a multi-page report: cover page, screenshot thumbnail per annotation, drawing overlay burned in, timestamp list
4. Drawing data optimization: serialize only Fabric.js path data (not internal cache properties) to keep JSONB rows under 10 KB even for complex drawings
5. Integration with Frame.io Review API or Vimeo Review API for teams already using those platforms

Limitations:

- 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

## Gotchas

- **Canvas overlay blocks video controls** — 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** — 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** — 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** — 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** — 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

## 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.

---

Source: https://www.rapidevelopers.com/app-features/video-annotation-tools
© RapidDev — https://www.rapidevelopers.com/app-features/video-annotation-tools
