Skip to content

Writing

Privacy by Design for Camera Features: On-Device Virtual Try-On

How TryOnIt keeps camera frames on the device, which network requests it makes, how to self-host for zero third-party requests, and the CSP it needs.

Published 4 min read

  • Privacy
  • Security
  • Content Security Policy
  • WebAssembly
  • Camera

Asking for someone’s camera is asking for trust. With virtual try-on the request is even more personal: the feature only works if it can see the shopper’s face, hands or body. If that video went to a server, every store using it would have to answer hard questions about where faces are processed, how long they are kept and who can access them.

I designed TryOnIt so that those questions have short answers: camera frames are processed on the device and never leave it. This article explains what that means in practice, every network request the SDK makes, and how to lock it down further for strict environments.

Where the camera frames go

The path of a frame is short and stays inside the browser:

  1. The camera stream comes from getUserMedia and plays in a <video> element.
  2. MediaPipe, running locally through WebAssembly and WebGL, reads each frame to find landmarks.
  3. The renderer draws the product over the frame on a canvas.

TryOnIt never uploads, stores or sends frames anywhere. There is no server component at all, which is also why there is no per-try fee.

Captured photos follow the same principle. A capture is created in the browser as a Blob, and it only leaves the device if your code sends it, for example from an onCapture handler, or if the shopper chooses to share or download it:

<TryOn asset={asset} onCapture={(blob) => saveToWishlist(blob)} />

If you never send it, it never goes anywhere.

Every network request, listed

On-device processing still needs code and models to arrive somehow. TryOnIt requests exactly three kinds of files:

  1. The MediaPipe runtime. The JavaScript chunk is bundled with your app; the WebAssembly files load from wasmBaseUrl, which defaults to jsDelivr pinned to the exact version the SDK was tested with.
  2. Model files from modelBaseUrl (or per-model URLs in models), which default to Google’s model bucket on storage.googleapis.com.
  3. Your product files: the asset manifests, 3D models and images that you configure.

That is the complete list. There are no analytics, cookies, fingerprinting or telemetry calls. Being able to state the full list in three lines is itself a design goal: it is what a privacy review needs.

Self-hosting for zero third-party requests

The two defaults that point at third parties, jsDelivr and Google’s model storage, are conveniences, not requirements. Copy the four model files and the WebAssembly folder to your own static hosting and point the engine at them:

import { createTryOnEngine } from '@tryonit/web';

createTryOnEngine({ modelBaseUrl: '/tryon/models', wasmBaseUrl: '/tryon/wasm' });

The React components take the same options through engineOptions:

<TryOnProvider engineOptions={{ modelBaseUrl: '/tryon/models', wasmBaseUrl: '/tryon/wasm' }}>
  <TryOnButton asset="/tryon/aviator.json" />
</TryOnProvider>

With self-hosting, a try-on session makes zero third-party requests. As a bonus, it usually makes loading faster too, because the files are served from your own CDN with your own cache headers.

@tryonit/react-native supports the same idea. Besides the model and WebAssembly locations, its cdn prop lets you host the MediaPipe bundle and three.js yourself, so the camera view inside the app also talks only to your servers.

A Content Security Policy that works

Strict sites run a Content Security Policy, and WebAssembly needs one explicit permission. With everything self-hosted, a matching policy typically needs:

  • script-src: your origin, where the WebAssembly loader script lives, plus 'wasm-unsafe-eval'. Without that keyword, a strict policy blocks the browser from compiling WebAssembly at all.
  • connect-src: your origin, for models, manifests and GLB files.
  • img-src: blob:, for capture previews.

As a starting point, the relevant directives look like this:

Content-Security-Policy:
  script-src 'self' 'wasm-unsafe-eval';
  connect-src 'self';
  img-src 'self' blob:

Every site’s policy has other directives for its own needs, so treat this as the part TryOnIt adds, and test your exact policy before shipping. A local playground with the real policy applied catches violations far sooner than a production error report.

Permission UX that earns the “Allow”

Privacy is not only about where data goes. It is also about not surprising people. TryOnIt’s default flow follows a few rules:

  • Ask at the moment of intent. The camera is requested only after the shopper opens try-on, or presses Start when autoStart is turned off. Nothing prompts on page load.
  • Make “no” a working answer. If permission is denied, TryOnIt explains how to re-enable the camera and offers a photo upload instead. The uploaded photo is processed locally in exactly the same way, so declining the camera does not mean giving up on the feature.
  • Stop the moment the shopper is done. Closing the dialog stops the camera immediately, and the browser’s camera indicator turns off with it.

Errors carry stable codes such as CAMERA_DENIED, CAMERA_NOT_FOUND, CAMERA_IN_USE and INSECURE_CONTEXT, along with retryable and canUsePhotoFallback flags. The default UI in @tryonit/react uses them to pick the right message and recovery path, and custom interfaces can do the same.

What you still owe your users

Processing on the device removes the hardest compliance questions, but it does not remove the need to tell people what is happening. TryOnIt does not process biometric identifiers on any server; you are still responsible for your privacy notice. A short, plain sentence is usually enough:

Virtual try-on runs entirely on your device. Your camera feed is never uploaded.

The default UI shows a similar note, and like every string in the React package it can be changed through labels.privacyNote.

Summary

  • Frames are processed on the device by MediaPipe and never uploaded.
  • Photos leave the device only through your code or the shopper’s own action.
  • The SDK requests the MediaPipe runtime, the models and your product files, and nothing else.
  • Self-host the models and WebAssembly to make zero third-party requests, and add 'wasm-unsafe-eval' to script-src for a strict CSP.
  • Request the camera on intent, offer a photo fallback, and stop the camera as soon as try-on closes.

Self-hosting also helps loading speed, which is covered with the rest of the performance work in Keeping Web AR Fast. The engine is available on npm as @tryonit/web.

More articles

Have an app in mind?

Tell me what you're building. I reply to every serious enquiry within two working days.