On-Device Face Capture

On-device face capture runs the full face-analysis pipeline (detection, positioning feedback, liveness, age estimation) inside the user's browser via WebAssembly. The captured selfie image never leaves the device — the SDK submits only the analysis results JSON to the server. Video recording is skipped on this path.

Opt-in. Default Selfie / Authentication runs server-side face analysis, where the captured frame is encrypted and uploaded for the backend pipeline to score.

When to enable it

Reach for on-device face capture when:

  • Privacy or data-residency posture requires keeping biometric image bytes off Incode's infrastructure.
  • Bandwidth-sensitive environments (slow / unstable networks) make image upload unreliable but result JSON is small enough to send.
  • Regulatory requirements bar the raw image from leaving the client.

If none of those apply, the server-side default is the recommended path — the backend pipeline has access to additional anti-spoof signals that aren't replicated on-device.

Requirements

Four things must line up. Missing any one of them and the path fails — usually at submission time, when the server rejects the request.

  1. E2EE configured at setup() — the on-device face-results endpoint (/omni/add/face-results) is only served on the E2EE-provisioned host. You need both:
    • encryption: true (or { mgf1: 'sha256' }, whichever your environment expects)
    • The dedicated E2EE apiURL Incode provisioned for your account
    • The /0 suffix on apiURL or customHeaders: { 'x-api-key': '<api key>' } so the tenant can be identified See End-to-End Encryption for the provisioning dance.
  2. WASM binary transport — implied by E2EE, but worth stating. The on-device pipeline runs inside the same WASM binary as the encrypted transport. setup({ wasm: false, ... }) is incompatible.
  3. The onDeviceSelfie WASM pipeline. Optional preload: pass wasm: { pipelines: ['selfie', 'onDeviceSelfie'] } to setup() so the models are warm before the user reaches the camera step. If you omit 'onDeviceSelfie', the SDK lazy-loads it on first camera open — fine for slow flows, noticeable for fast ones.
  4. The config flag. Set onDeviceFaceResultsSubmissionEnabled: true on the Selfie or Authentication config:
    const selfie = document.querySelector('incode-selfie');
    selfie.config = {
      onDeviceFaceResultsSubmissionEnabled: true,
      // ...other SelfieConfig fields
    };

Full integration example

Use the standard <incode-selfie> web component and set onDeviceFaceResultsSubmissionEnabled: true on its config. The component drives capture and submission; you only need to wire onFinish / onError.

Vanilla HTML / TypeScript

<incode-selfie></incode-selfie>

<script type="module">
  import { setup } from '@incodetech/core';
  import { initializeSession } from '@incodetech/core/session';
  import '@incodetech/web/selfie';
  import '@incodetech/web/selfie/styles.css';

  // 1. Configure the SDK with E2EE + the on-device pipeline pre-warmed.
  await setup({
    apiURL: 'https://<your-incode-e2ee-host>/0', // E2EE host + /0 for JWT-derived API key
    encryption: { mgf1: 'sha256' }, // pin SHA-256 when your E2EE env is provisioned for it
    wasm: { pipelines: ['selfie', 'onDeviceSelfie'] },
  });

  // 2. Activate your session token.
  await initializeSession({ token: 'your-session-token' });

  // 3. Configure <incode-selfie> with the on-device flag enabled.
  const selfie = document.querySelector('incode-selfie');
  selfie.config = {
    showTutorial: true,
    showPreview: false,
    assistedOnboarding: false,
    enableFaceRecording: false, // ignored on the on-device path anyway
    autoCaptureTimeout: 10,
    captureAttempts: 3,
    validateLenses: true,
    validateFaceMask: true,
    validateHeadCover: true,
    validateClosedEyes: true,
    validateBrightness: true,
    deepsightLiveness: 'SINGLE_FRAME',
    onDeviceFaceResultsSubmissionEnabled: true, // opt in
  };
  selfie.onFinish = () => console.log('On-device face capture complete');
  selfie.onError = (err) => console.error('Selfie error:', err);
</script>

React

React 18 or earlier: add the one-time JSX augmentation from Framework Integration → TypeScript: JSX support for incode-* tags so TypeScript recognizes the Incode tags. The example below uses a ref + useEffect to assign config / onFinish / onError because JSX attributes only accept strings. React 19+ doesn't need this — see Framework Integration → React 19+ shortcut for the simpler inline-prop form.

import { useEffect, useRef } from 'react';
import { setup } from '@incodetech/core';
import { initializeSession } from '@incodetech/core/session';
import type { SelfieConfig } from '@incodetech/core/selfie';
import '@incodetech/web/selfie';
import '@incodetech/web/selfie/styles.css';

type SelfieElement = HTMLElement & {
  config?: SelfieConfig;
  onFinish: () => void;
  onError: (error: string) => void;
};

await setup({
  apiURL: 'https://<your-incode-e2ee-host>/0',
  encryption: { mgf1: 'sha256' }, // pin SHA-256 when your E2EE env is provisioned for it
  wasm: { pipelines: ['selfie', 'onDeviceSelfie'] },
});
await initializeSession({ token: 'your-session-token' });

export function OnDeviceSelfie() {
  const ref = useRef<SelfieElement>(null);

  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    el.config = {
      showTutorial: true,
      showPreview: false,
      assistedOnboarding: false,
      enableFaceRecording: false,
      autoCaptureTimeout: 10,
      captureAttempts: 3,
      validateLenses: true,
      validateFaceMask: true,
      validateHeadCover: true,
      validateClosedEyes: true,
      validateBrightness: true,
      deepsightLiveness: 'SINGLE_FRAME',
      onDeviceFaceResultsSubmissionEnabled: true,
    };
    el.onFinish = () => console.log('On-device face capture complete');
    el.onError = (err) => console.error('Selfie error:', err);
  }, []);

  return <incode-selfie ref={ref} />;
}

Inside <incode-flow>? When <incode-selfie> runs as part of <incode-flow> (or createOrchestratedFlowManager), the orchestrator passes the dashboard-configured FlowModuleConfig['SELFIE'] to the component automatically — you do not set selfie.config yourself. Enable onDeviceFaceResultsSubmissionEnabled on the SELFIE module configuration in the Incode dashboard instead, and the flag flows through.

Going fully custom UI, or using Authentication? The same onDeviceFaceResultsSubmissionEnabled: true flag works on the headless managers. See Headless Mode.

What changes on this path

AspectDefault (server-side)On-device
Image bytes leave the deviceYes — encrypted uploadNo — only analysis results JSON
Video recording (enableFaceRecording)Honored — local recording is assembled and uploaded if configuredSkipped — the recording service is never created
Required WASM pipelinesselfieselfie + onDeviceSelfie
Setup complexityStandard setup({ apiURL, token })E2EE provisioning + dedicated apiURL + /0 or x-api-key + the onDeviceSelfie pipe

Caveats

  • Video recording is skipped. The recording service is never created on this path, so enableFaceRecording: true is silently ignored. If you need a local video artifact, the on-device path isn't compatible — use the server-side default or the capture-only flow.
  • Locked at boot with E2EE. You cannot flip between on-device and server-side at runtime in the same SDK boot — see End-to-End Encryption → Constraints to know about.

See also