# How to Add AR Stickers to Your App — Copy-Paste Prompts Inside

- Tool: App Features
- Last updated: July 2026

## TL;DR

AR stickers need three pieces: a face-landmark engine (MediaPipe FaceMesh — free, in-browser), a canvas overlay anchored to facial landmarks, and a sticker asset CDN. With Lovable you can scaffold the UI shell and Supabase Storage integration in 2–4 days, but WebXR/MediaPipe wiring requires heavy manual JavaScript. Running cost is $0–$25/month for most apps since all computation happens in the browser.

## What Augmented Reality Stickers Actually Are

AR stickers overlay animated graphics — hats, glasses, effects — directly onto a live camera feed, anchored to specific points on a user's face. The user sees themselves in real time with the sticker composited on top. Social apps use this for photo booths and filters; marketing campaigns use it for branded selfie experiences. The face-tracking itself (MediaPipe FaceMesh) is open-source and runs entirely in the browser — no cloud API, no per-use cost. The product decisions that make or break the feature are sticker quality, anchor precision, and capture output: a blurry sticker or a drifting overlay at head turn destroys the experience instantly.

## Anatomy of the Feature

Six components. The face landmark engine and capture pipeline are where builds fail — AI tools get the React shell right but consistently mis-wire the MediaPipe locateFile path and the canvas compositing order.

- **Camera access module** (ui): Calls navigator.mediaDevices.getUserMedia() with { video: { facingMode: 'user', width: 1280, height: 720 } } and streams the result into a <video> element. Handles permission denial, device enumeration for front/rear switching, and the iOS Safari portrait rotation bug.
- **Face landmark engine** (backend): MediaPipe FaceMesh via @mediapipe/face_mesh returns 468 3D facial landmarks at 30 fps. Initialized once per session; drives a requestAnimationFrame loop that feeds video frames to FaceMesh and receives landmark arrays back for each detected face.
- **Sticker placement engine** (ui): Maps MediaPipe landmark indices to sticker anchor points and applies affine transforms for rotation and scale. Uses three-point anchoring: landmark 10 (forehead center), landmark 234 (left ear), landmark 454 (right ear) to compute a transform matrix that follows head tilt and turn correctly.
- **AR rendering layer** (ui): A <canvas> element positioned absolutely over the <video> feed at identical dimensions. In the requestAnimationFrame loop: clearRect(), draw the transformed sticker image (SVG or WebP) at the computed anchor position using ctx.drawImage() with matrix transforms applied via ctx.setTransform().
- **Sticker asset manager** (data): SVG, WebP, or APNG sticker sprites stored in a Supabase Storage public bucket; sticker metadata (name, category, tags, is_animated, asset_url) in a Postgres stickers table. Stickers are preloaded as Image() objects at picker open time to avoid per-frame fetch latency.
- **Capture and export pipeline** (backend): On capture button press: draw the current video frame onto a temporary OffscreenCanvas, then draw the composited sticker canvas on top, and call toBlob() to produce a PNG Blob. The Blob is either offered as a file download via URL.createObjectURL() or uploaded to Supabase Storage and returned as a signed URL for sharing.

## Data model

Two tables: one for the sticker library and one for user captures. Run this in the Supabase SQL editor:

```sql
create table public.stickers (
  id uuid primary key default gen_random_uuid(),
  name text not null,
  category text not null,
  asset_url text not null,
  tags text[] not null default '{}',
  is_animated boolean not null default false,
  created_at timestamptz not null default now()
);

alter table public.stickers enable row level security;

create policy "Stickers are publicly readable"
  on public.stickers for select
  using (true);

create index stickers_category_idx
  on public.stickers (category);

create table public.user_captures (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users(id) on delete cascade not null,
  capture_url text not null,
  sticker_ids uuid[] not null default '{}',
  created_at timestamptz not null default now()
);

alter table public.user_captures enable row level security;

create policy "Users can view own captures"
  on public.user_captures for select
  using (auth.uid() = user_id);

create policy "Users can insert own captures"
  on public.user_captures for insert
  with check (auth.uid() = user_id);

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

create index user_captures_user_recent_idx
  on public.user_captures (user_id, created_at desc);
```

The stickers table uses public read so the picker loads without auth. user_captures restricts all access to the owning user. The category index keeps the sticker picker fast as the library grows.

## Build paths

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

Lovable scaffolds the React UI shell, Supabase Storage integration, and sticker picker reliably. The MediaPipe FaceMesh wiring and requestAnimationFrame loop require manual JavaScript work after the initial scaffold — treat Lovable as a fast starting point, not a complete solution.

1. Create a new Lovable project with Lovable Cloud enabled to auto-provision Supabase auth and Storage
2. Paste the prompt below to scaffold the sticker picker panel, camera permission flow, capture button, and Supabase Storage upload
3. After Lovable generates the scaffold, manually add the @mediapipe/face_mesh npm import and write the FaceMesh initialization block with the explicit locateFile CDN path
4. Wire the requestAnimationFrame loop in a useEffect, connecting landmark output to canvas drawImage() calls using three-point anchoring
5. Publish the app and test on your phone over HTTPS — camera and MediaPipe both require a published URL, not the preview iframe

Starter prompt:

```
Build an AR sticker web app. Camera module: request camera permission with getUserMedia({ video: { facingMode: 'user', width: 1280, height: 720 } }); show a permission-denied screen with a 'Try Again' button if denied; render the video feed in a <video> element. Overlay a <canvas> at the same dimensions absolutely positioned on top. Sticker picker: floating panel below the camera showing a thumbnail grid of stickers from the Supabase 'stickers' table (id, name, category, asset_url, tags, is_animated); group by category in tabs; selecting a sticker sets it as the active sticker. Capture button: composites the video frame and canvas overlay into a PNG using OffscreenCanvas, uploads to Supabase Storage under 'captures/{user_id}/{timestamp}.png', inserts a row into user_captures table (user_id, capture_url, sticker_ids), and offers a download link plus a share button using navigator.share(). Handle states: idle / requesting-permission / permission-denied / no-face-found / sticker-active / capturing. Mobile touch support for sticker selection. The MediaPipe FaceMesh wiring will be added manually — scaffold the canvas and video layout, state machine, and Supabase integration now.
```

Limitations:

- getUserMedia() and MediaPipe WASM both fail inside Lovable's preview iframe — testing requires the published URL only
- Lovable frequently mis-wires the MediaPipe locateFile path, causing 'Failed to fetch' WASM errors — manual fix required
- Animated sticker rendering (APNG/Lottie) requires additional manual implementation beyond what Lovable generates

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

V0 produces a clean React component shell for the sticker picker and capture button layout, but MediaPipe WASM assets (~8 MB) cause esm.sh sandbox timeouts in preview. Treat V0 output as a UI starter that needs manual WebXR wiring.

1. Prompt V0 to generate the sticker picker UI component, camera permission state machine, and Supabase query for the stickers table
2. Add your Supabase environment variables in the Vars panel (NEXT_PUBLIC_SUPABASE_URL and anon key)
3. Manually add @mediapipe/face_mesh to the project dependencies and write the FaceMesh initialization hook
4. Implement the requestAnimationFrame loop and canvas overlay rendering outside of V0
5. Run the data model SQL in the Supabase SQL editor, then publish and test on HTTPS

Starter prompt:

```
Build a Next.js AR sticker page. Component 1: CameraView — client component that calls getUserMedia({ video: { facingMode: 'user', width: 1280, height: 720 } }), renders a <video> element, and overlays a <canvas> at the same size. Shows a 'Camera Denied' screen with retry on NotAllowedError. Component 2: StickerPicker — fetches stickers from Supabase 'stickers' table grouped by category; renders category tabs and a thumbnail grid; calls onSelectSticker(sticker) on click. Component 3: CaptureButton — on click, draws video frame then canvas content onto a hidden OffscreenCanvas, calls toBlob(), and uploads to Supabase Storage returning the public URL. State machine: idle / requesting-permission / permission-denied / no-face-found / sticker-active / capturing. Sticker picker as a bottom drawer on mobile. The MediaPipe landmark wiring will be added manually after this scaffold.
```

Limitations:

- MediaPipe WASM assets cause esm.sh sandbox timeouts in V0 preview — do not attempt MediaPipe in the V0 sandbox
- V0 does not auto-provision Supabase — run the SQL schema manually and set env vars in the Vars panel
- next/image wrapping sticker thumbnails may interfere with fast thumbnail grids — swap to plain <img> if loading stalls

### Custom — fit 5/10, 3–6 weeks

Full control over MediaPipe FaceMesh configuration, WebCodecs for video export, GPU-accelerated canvas rendering, and CDN sticker delivery. Custom dev is the only reliable path to production-quality AR at scale with sub-30 ms latency and multi-face support.

1. Set up Next.js with @mediapipe/face_mesh, configure locateFile for WASM CDN delivery, and build the requestAnimationFrame inference loop with proper cleanup on unmount
2. Implement three-point affine transform anchoring using landmarks 10, 234, and 454 for accurate sticker placement across head orientations
3. Build the sticker asset pipeline: WebP sprites in Supabase Storage, metadata in Postgres, preloaded Image() objects keyed by sticker ID
4. Add WebCodecs-based video recording that captures the composited canvas stream as MP4 for video sticker output
5. Implement CDN caching strategy for sticker assets and signed URL generation for user captures

Limitations:

- 3–6 week timeline requires engineers with WebGL/WebXR expertise
- MediaPipe WASM initialization adds 1–2 seconds on first load — requires a loading screen

## Gotchas

- **Camera blocked in preview iframe** — getUserMedia() silently fails or throws NotAllowedError inside Lovable's embedded preview and V0's sandbox iframe. Browser security policies do not grant microphone or camera permissions to sandboxed iframes. The video element remains black and MediaPipe never initializes, making it impossible to validate the feature in the builder's preview panel. Fix: Always test AR sticker features on the published HTTPS URL opened on a real device. In Lovable, use the mobile preview QR code; in V0, publish to production first. Add a clear UI indicator ('Open on your phone to test AR') in the preview state.
- **MediaPipe WASM fails to load** — @mediapipe/face_mesh requires its WASM binary (~8 MB) to be fetched at runtime. ESM bundlers (Vite, esbuild, webpack) often fail to resolve the locateFile path correctly, resulting in a 'Failed to fetch' error that prevents FaceMesh from initializing. The error appears in the console but the UI may show a blank camera feed with no indication of what went wrong. Fix: Explicitly set the locateFile option when constructing FaceMesh: locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/${file}`. This bypasses the bundler and fetches WASM directly from the jsDelivr CDN. Test by opening DevTools Network tab and confirming the .wasm file loads with a 200 response.
- **Sticker drifts when head rotates** — AI-generated AR code commonly anchors the sticker to a single landmark (e.g. landmark 1 — nose tip). When the user turns their head, the sticker's reference point moves but its orientation doesn't update, causing the graphic to visibly drift away from the intended position. At large head angles the sticker can float completely off the face. Fix: Use three-point anchoring with landmarks 10 (forehead center), 234 (left ear), and 454 (right ear) to compute an affine transform matrix. Apply the matrix via ctx.setTransform() before drawing the sticker. This approach correctly accounts for head rotation and scale changes from distance. Specify this explicitly in your prompt — single-point anchoring is the default AI output.
- **iOS Safari captures appear rotated** — iOS Safari delivers the front camera video stream in landscape orientation regardless of the physical device orientation. When you draw that video frame to a canvas in portrait mode and call toDataURL(), the resulting PNG appears rotated 90 degrees. This produces unusable captures and only manifests on iPhone — desktop and Android are unaffected. Fix: Detect iOS with a user agent check and apply CSS transform: rotate(-90deg) to the video element. In the capture canvas drawImage() call, rotate the canvas context 90 degrees before drawing the video frame, then rotate back. Test captures on a real iPhone before launch — the Lovable or V0 desktop preview will look correct even when the mobile output is broken.
- **Canvas export throws SecurityError on sticker assets** — Drawing sticker images loaded from a different origin onto a canvas and calling toDataURL() or toBlob() throws a SecurityError ('Tainted canvases may not be exported'). This happens when sticker assets are served from Supabase Storage but the canvas origin is your app domain. The capture button appears to work until the export step, then silently fails. Fix: Serve all sticker assets from your app's same origin, or configure CORS headers on the Supabase Storage bucket. In the Supabase Dashboard, go to Storage → Policies and add a CORS configuration allowing your app's origin. Also set crossOrigin='anonymous' on the Image() objects used to preload stickers.

## Best practices

- Always use three-point landmark anchoring (forehead 10, left ear 234, right ear 454) — single-point anchoring drifts and makes the feature feel broken on head turn
- Preload all sticker Image() objects when the picker opens, not on selection — per-frame fetch latency makes the overlay visibly lag during the first second
- Show a distinct 'no face detected' state with an animated icon rather than a static camera feed — users don't know if the feature is broken or just waiting
- Cancel the requestAnimationFrame loop and stop the media stream track when the component unmounts to prevent memory leaks and lingering camera indicator lights
- Serve sticker assets from Supabase Storage with a public bucket and CORS headers set from day one — fixing CORS after captures are broken is painful
- Add an explicit autoplay attribute to the <video> element (autoplay muted playsInline) — iOS Safari requires playsInline to avoid full-screen hijack
- Compress sticker assets aggressively: WebP stickers at 150x150 px should be under 20 KB each; animated APNG under 100 KB — a slow sticker picker kills the experience
- Log the MediaPipe initialization time in development — if it exceeds 3 seconds, the locateFile CDN path is wrong and WASM is being bundled instead of fetched

## Frequently asked questions

### Can I add AR stickers without WebXR?

Yes — and for face-tracked stickers on a flat camera feed, you don't need WebXR at all. WebXR is for immersive mixed-reality experiences (headsets, AR overlays on real-world geometry). MediaPipe FaceMesh running on a standard getUserMedia() video stream is the correct approach for social-style AR sticker filters. WebXR Device API adds complexity and has worse mobile browser support.

### Do AR stickers work on iPhone Safari?

Yes, but with caveats. iOS Safari supports getUserMedia() for camera access and runs MediaPipe FaceMesh at usable frame rates on iPhone 12 and newer. Two gotchas: the video stream arrives rotated in portrait mode (requires a CSS + canvas transform fix), and Safari caps resolution at 720p. The feature works well for photo capture; video recording requires careful codec selection because MediaRecorder support in Safari is limited.

### How do I anchor a sticker to the forehead vs the nose?

MediaPipe FaceMesh returns 468 numbered landmarks. Landmark 10 is the forehead center, landmark 1 is the nose tip, landmark 33 is the left eye outer corner, and landmark 263 is the right eye outer corner. To place a sticker at a specific location, use the (x, y, z) values of the target landmark as the draw position and derive rotation from the vector between landmarks 33 and 263 (eye line angle). For a hat, landmark 10 with an upward offset proportional to face height gives the most natural placement.

### What's the difference between face-api.js and MediaPipe FaceMesh?

face-api.js TinyFaceDetector returns 68 landmarks and is lighter (~1.5 MB), but 68 points aren't enough for precise sticker placement — hat brims and glasses frames warp noticeably. MediaPipe FaceMesh returns 468 3D landmarks and is the industry standard for face AR in browsers, at the cost of a larger WASM payload (~8 MB initial load). For sticker overlays, use MediaPipe FaceMesh.

### How do I let users upload their own stickers?

Add an upload button that sends the file to a Supabase Storage bucket (e.g. 'user-stickers/{user_id}/') via the Supabase JS client, then inserts a row in the stickers table with the returned URL. Restrict uploads to SVG, WebP, and PNG under 500 KB. Apply the same CORS configuration to the user-stickers bucket as to the system stickers bucket, or captures will throw SecurityError on export.

### Can users record a video with AR stickers, not just a photo?

Yes, but it adds significant complexity. Use MediaRecorder with the canvas.captureStream(30) API to record the composited canvas as a video stream. Export as WebM (widely supported) and optionally transcode to MP4 using ffmpeg.wasm. The ffmpeg.wasm dependency adds about 3 MB to your bundle. Safari has limited MediaRecorder support, so test on both Chrome and Safari before promising video capture.

### How do I handle the 'no face detected' state gracefully?

Track a consecutive-frames-without-face counter in the requestAnimationFrame loop. After 30 frames (~1 second) with no landmarks returned, set state to 'no-face-found' and show an animated prompt (e.g. 'Move your face into frame'). Reset the counter and hide the prompt the instant FaceMesh returns landmarks again. Avoid showing the no-face state immediately — a brief look away shouldn't disrupt the experience.

### What frame rate should I target for smooth AR sticker rendering?

Target 30 fps, which MediaPipe FaceMesh can sustain on mid-range phones from 2021 onward. The requestAnimationFrame loop runs at the display refresh rate (60 Hz typical), so process FaceMesh inference every other frame on slower devices. Add a performance monitor during testing: if inference time exceeds 20 ms per frame, enable the MediaPipe FaceMesh modelComplexity: 0 option (simpler model, slightly fewer landmarks) to reduce CPU load.

---

Source: https://www.rapidevelopers.com/app-features/augmented-reality-stickers
© RapidDev — https://www.rapidevelopers.com/app-features/augmented-reality-stickers
