Face Local / Developers

From download to your first build.

Practical documentation. Permissions, integration and limitations — no fine print.

SDKv0.6.0

Integrate the structured result#

First read the installation, permissions and Release-build requirements in the README. The SDK does not work in Expo Go or approve in a browser.

APIs#

ImportAPI
@workspace/face-local-sdk/exporunFaceLocalVerification(options): Promise<VerificationResult>
@workspace/face-local-sdk/exporeleaseFaceLocalVerification(verificationId): Promise<void>
@workspace/face-local-sdk/expocreateExpoEngine(): EvidenceEngine
@workspace/face-local-sdkrunVerification(options) for a custom adapter
@workspace/face-local-sdkTypes VerificationResult, VerificationOptions, EvidencePhoto, EvidenceEngine

runFaceLocalVerification handles photo authorization, discovery and Face Match. The app provides consent, the camera-permission request and optional UI callbacks. There is no intermediate search or compare button, and no reference-image picker.

Integration example#

The UI functions below belong to the integrating app, not the SDK: showResult presents the result and resolves only when the user closes the screen. This avoids removing files while the screen still needs them.

ts
import {
  FaceLocalError,
  type Consent,
  type VerificationResult,
} from "@workspace/face-local-sdk";
import {
  runFaceLocalVerification,
  releaseFaceLocalVerification,
} from "@workspace/face-local-sdk/expo";
import { requestCameraPermissionsAsync } from "expo-image-picker";

async function verify(
  consentFromYourUI: Consent,
  signal: AbortSignal,
  showResult: (result: VerificationResult) => Promise<void>,
) {
  let result: VerificationResult | undefined;
  try {
    result = await runFaceLocalVerification({
      // Consent must come from your interface, not be assumed by the SDK.
      consent: { ...consentFromYourUI, photoLibrary: false },
      signal,
      policy: {
        matchThreshold: 0.83,
        maxPhotos: 100,
        captureTimeoutSeconds: 15,
        requireCaptureMetadata: true,
      },
      evidence: {
        format: "uri",     // Default. No Base64 in the object.
        ttlSeconds: 300,   // 30–300 seconds; default 300.
      },
      requestCameraAccess: async () => {
        // Requests permission; does NOT open a still camera or an image picker.
        const permission = await requestCameraPermissionsAsync();
        if (!permission.granted) {
          throw new FaceLocalError("CAMERA_PERMISSION", "Camera not authorized.");
        }
      },
    });

    await showResult(result);
  } finally {
    if (result?.verificationId) {
      // Await and handle failures; do not claim cleanup that did not happen.
      await releaseFaceLocalVerification(result.verificationId);
    }
  }
}

Display the returned images with React Native's Image:

tsx
{result.photos.liveness && (
  <Image source={{ uri: result.photos.liveness.uri }} style={{ width: 160, height: 200 }} />
)}
{result.photos.compared && (
  <Image source={{ uri: result.photos.compared.uri }} style={{ width: 160, height: 200 }} />
)}

Remove references from the interface and any image caches after release or expiry. The images are temporary, not permanent profile photos.

Status: distinguish rejection from an inability to decide#

ts
switch (result.status) {
  case "approved":
    // Continue onboarding; approved === true.
    break;
  case "not_approved":
    // The attempt did not meet the criteria; approved === false.
    break;
  case "inconclusive":
    // Insufficient evidence; approved === null.
    break;
  case "cancelled":
    // End the attempt without treating cancellation as rejection.
    break;
  case "error":
    // Handle errorCode: permission, environment, integrity, expiry or cleanup.
    break;
}

The result uses plain objects that can be serialized with JSON.stringify(result). Serialization does not mean saving, logging or sending it to a server. The JSON contains biometrics, private paths and sensitive metadata.

Failures in the high-level API flow are returned as structured statuses, including invalid options and callbacks that throw errors. Explicit cleanup operations return a Promise that can reject; handle that error in the app. The previous APIs still throw errors.

Options and callbacks#

OptionRule
consentcamera and biometricProcessing must be true after consent; photos start unauthorized
requestCameraAccessMust request actual permission and throw on denial
signalOptional AbortSignal to cancel an in-progress attempt
policy.matchThresholdFinite number from 0.5 to 0.99; default 0.83
policy.maxPhotosInteger from 1 to 1000; default 100
policy.captureTimeoutSecondsInteger from 6 to 22; default 15
policy.requireCaptureMetadataMust be true; there is no mode without the EXIF filter
evidence.formaturi by default, or explicitly base64
evidence.ttlSecondsInteger from 30 to 300, starting at the first native export
onPhaseOnboarding phase, for updating the UI
onCaptureReceipt without image/template; do not alter the session based on this callback
onSearchProgressLibrary discovery progress
onProgressComparison progress

The operating system controls permissions. You cannot force “full access” or hide the limited-access option. The API requires actual full scope; limited or denied access ends the flow without scanning unauthorized photos. The filter does not upload/download cloud originals or request GPS permission.

Optional Base64#

Use evidence.format: "base64" only when a local consumer needs the bytes in JavaScript. The result still includes local URIs; base64 contains only the encoded data, without a data: prefix.

This increases memory usage. Do not put it in AsyncStorage, a database, the clipboard, analytics or crash reports. The SDK cannot revoke or zero a JavaScript string that your code has already copied. TTL and release remove the native files, not copies of strings held by consumers; remove your own references.

Retention, expiry and cleanup#

There are two separate lifecycles:

  1. The facial reference and embedding remain in native memory for at most five minutes. They are cleared at the end of the scan or on error and are never returned in JSON.
  2. The new API explicitly requests up to two normalized JPEG copies in private cache, without original EXIF/GPS. These are the only images exported.

Before export, the five-minute capture limit applies. After the first native export, the evidence TTL is 30–300 seconds, defaulting to 300. Reading the evidence again does not extend it. expiresAt is an ISO timestamp for consumers; the native implementation also uses a monotonic deadline.

Cleanup occurs on:

  • releaseFaceLocalVerification(verificationId);
  • cancellation or error before delivery;
  • starting a new verification;
  • engine.clear();
  • expiry, when the process can run;
  • native-module initialization after a process restart.

On iOS, moving the app into the background clears the session. On Android, the host must explicitly end the flow when appropriate; entering the capture Activity does not count as leaving the app.

If the operating system suspends or kills the process, a timer cannot guarantee removal at the exact second. Access checks and startup cleanup complement the timer. A file:// URI is not a cryptographic expiry token: respect expiresAt and release explicitly. TTL is a maximum, not a guarantee of availability; system cleanup, revocation or other lifecycle events can remove files earlier. There is no guarantee of forensic erasure from flash storage or external caches.

Release is idempotent and scoped to the matching verification ID, never an arbitrary path. Releasing an older ID does not clear a newer session. Original gallery photos are never deleted. If removal fails, the session binding is retained so a later attempt does not falsely report that cleanup already occurred.

Android uses private cache excluded from backup. iOS uses complete file protection and excludes the files from backup. These measures do not protect against a compromised operating system.

Cancel and prevent late results#

ts
const controller = new AbortController();
// Pass controller.signal to runFaceLocalVerification.
// To cancel the in-progress attempt:
controller.abort();
// Await the verification Promise; the SDK discards late callbacks.

After the Promise returns a result, use releaseFaceLocalVerification to remove the copies; aborting an already completed signal is not the cleanup API. Do not run parallel verifications: the SDK has one active native session per process. A second high-level call returns BUSY without deleting the first. Do not mix manual capture/comparison calls with an in-progress onboarding flow.

Migrate from the previous report#

Previous APINew API
runFaceLocalOnboardingrunFaceLocalVerification
outcome: "positive"status: "approved", approved: true
outcome: "negative"status: "not_approved", approved: false
outcome: "inconclusive"status: "inconclusive", approved: null
Thrown errorserror or cancelled status, with reason/code
Report with library candidatesFiltered JSON with up to two private copies
No capture exportExplicit export with TTL and mandatory cleanup

The SDK retains the previous APIs for compatibility; they do not start saving image copies automatically. The demo app may still use the previous flow until it is migrated. This does not limit use of the new API in another app.

Install package 0.5+, rebuild the binary and reinstall it. Changing only Metro's JavaScript or installing a .tgz does not update an installed Android/iOS binary. Without capabilities().evidenceVersion === 1, the new API returns EVIDENCE_UNAVAILABLE. There is no fallback that fabricates photos, EXIF, a score or approval.

Custom adapter#

runVerification receives an EvidenceEngine, requestPhotoAccess and readPhotos, in addition to shared options. Detailed types are exported by the package. Permission must come from the operating system, with accessPrivileges: "all".

The adapter must:

  • preserve all BiometricEngine protections;
  • implement captureReferenceWithEvidence without accepting external images, URIs or frames;
  • return native EXIF in NativeComparison.captureMetadata, from the same bytes analyzed by the engine;
  • retain the best comparison until the first match, without associating images from another session;
  • implement clearReference to remove the template/camera while preserving only the explicitly requested copies for delivery;
  • implement getEvidence after clearing the reference, with the session, score, original compared URI, EXIF, normalized copies, hash and actual deadline; this internal object must not be exposed directly as integration JSON;
  • implement releaseEvidence and clear with actual removal, limited to files owned by the SDK, and report removal failures;
  • respect TTL without extending it on repeated reads, and reject exports from expired or mismatched sessions.

Satisfying a TypeScript interface is not proof of a working biometric implementation. Deterministic test doubles do not validate real liveness, similarity or attack resistance.

Checklist before integrating into another app#

  • Use a Release build on a physical device, not Expo Go/web.
  • Obtain actual permissions and consent; continue automatically after liveness.
  • Do not provide a reference-image picker/importer.
  • Handle status, approved and reason separately.
  • Keep results/photos in UI memory, without logs, persistence or upload.
  • Do not present similarity as accuracy/probability.
  • Do not advertise EXIF or the model profile as proof of ownership.
  • Release explicitly on completion/close; understand expiry and Base64.
  • Preserve the original gallery after release, cancellation and expiry.
  • Rebuild the native binary, including the native export code.
  • Run real Android/iPhone tests, including backgrounding, late results and airplane mode.
  • Review the plugin policy: it removes INTERNET from the entire Android Release app.
  • Respect the Face Local terms: own-brand use is permitted; SDK resale is prohibited.