# 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](doc:web-sdk-2-e2ee) 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:
   ```ts
   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

```html
<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](doc:web-sdk-2-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](doc:web-sdk-2-framework-integration#react-19-shortcut) for the simpler inline-prop form.

```tsx
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](doc:web-sdk-2-headless-mode).

## What changes on this path

| Aspect                            | Default (server-side)                                                       | On-device                                                                                |
| --------------------------------- | --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| Image bytes leave the device      | Yes — encrypted upload                                                      | No — only analysis results JSON                                                          |
| Video recording (`enableFaceRecording`) | Honored — local recording is assembled and uploaded if configured     | Skipped — the recording service is never created                                         |
| Required WASM pipelines           | `selfie`                                                                    | `selfie` + `onDeviceSelfie`                                                              |
| Setup complexity                  | Standard `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](doc:web-sdk-2-module-selfie-1#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](doc:web-sdk-2-e2ee#constraints-to-know-about).

## See also

- [End-to-End Encryption](doc:web-sdk-2-e2ee) — required prerequisite.
- [Module: Selfie](doc:web-sdk-2-module-selfie-1) — config catalogue and full Selfie reference.
- [Module: Authentication](doc:web-sdk-2-module-authentication) — same flag, returning-user re-auth.
- [WASM Configuration](doc:web-sdk-2-wasm#when-wasm-is-required) — the `onDeviceSelfie` pipeline entry.
- [Headless Mode](doc:web-sdk-2-headless-mode) — driving the Selfie / Authentication managers from custom UI.
