DEV Community

Cover image for How I Bridged AWS Face Liveness to Expo with a Native Module
Olawale Bashiru
Olawale Bashiru

Posted on

How I Bridged AWS Face Liveness to Expo with a Native Module

AWS Face Liveness has native SDKs for iOS and Android.

That works well if you are building fully native apps.

But if your team is building with Expo, the integration path is not as simple as installing a JavaScript package.

You still have to think about native iOS setup, native Android setup, camera permissions, Amplify configuration, credential handling, build configuration, and the bridge between JavaScript and native code.

That gap is what led me to build expo-face-liveness-for-aws-amplify, an Expo module that integrates AWS Amplify Face Liveness into Android and iOS Expo projects.

The business case was clear too: financial and identity-sensitive products often need to verify that a real person is completing sensitive flows like onboarding, KYC, account recovery, fraud prevention, or high-risk actions.

Face liveness does not replace every other security measure. It adds another layer of confidence that the product backend can use as part of its final decision.

This article breaks down how I approached the problem, what AWS Face Liveness needs architecturally, how the module is designed, the tradeoffs I made, and what I learned from the first release.

Here is the module running on both platforms:

iOS Android
AWS Face Liveness running on iOS AWS Face Liveness running on Android

Why this problem exists

For fully native mobile apps, the integration path is more direct.

You install the native SDK, configure the platform-specific requirements, and render the UI component provided by the SDK.

For Expo teams, the story is different.

A React Native or Expo app may share most of its product code across platforms, but native SDKs still require platform-specific setup.

That means:

  • iOS configuration
  • Android configuration
  • camera permissions
  • native dependencies
  • version compatibility
  • build configuration
  • credential handling
  • JavaScript-to-native events

Before abstracting this into a package, the painful part was not only getting the detector to render once.

The painful part was knowing that every product with the same requirement would have to repeat the same native setup decisions: how to configure iOS, how to wire Android, how to handle camera permissions, how to make Amplify credentials available, how to expose native completion events, and how to keep the backend as the source of truth.

That repetition was the real signal.

Once the same liveness requirement appeared across multiple products, including one I was leading and another a colleague reached out about, the problem stopped being β€œhow do I make this work here?” and became β€œhow do I make this easier to reuse anywhere an Expo app needs it?”

What AWS Face Liveness needs to work

Before building the module, I needed to understand what the SDK actually requires.

AWS Face Liveness is not just a screen you open in the app. It depends on a few moving parts working together.

The first part is authorization.

The liveness detector needs the app to be configured in a way that allows the SDK to communicate with AWS during the capture flow. In the standard Amplify setup, this is usually handled through Amplify Auth and Cognito-backed resources.

The second part is the liveness session.

A session is created by the backend before the client starts the liveness flow. That session acts like a reference shared between the backend, the client app, the AWS service, and the Face Liveness SDK.

At a high level, the flow looks like this:

Backend creates a Face Liveness session
App receives the session ID and region
App renders the Face Liveness detector
Native SDK performs the liveness flow
App receives a completion event
Backend verifies the final result

The important design principle is this:

The mobile app participates in the flow, but it should not own the trust boundary.

The backend should create the session and verify the final result. The client should only receive what it needs to start the liveness experience.

Why I chose runtime credentials

One of the most important design decisions was how the module should receive AWS credentials.

The standard Amplify/Cognito-oriented path works well when an application already uses Amplify Auth and Cognito-backed configuration.

In that setup, the app is usually initialized with generated Amplify client configuration, such as amplify_outputs.json or amplifyconfiguration.json, depending on the platform, SDK version, and setup path.

That configuration tells the Amplify client libraries which backend Auth resources to use, including details such as the AWS region, User Pool, User Pool Client, and Identity Pool.

The native app also needs the relevant Amplify Auth plugin setup so the SDK can resolve AWS credentials for service calls.

That is a good fit for apps already built around Amplify and Cognito.

For a reusable Expo module, though, I did not want to force every consuming app into that structure.

Some apps may already use Cognito. Some may use a different authentication system. Some may want their backend to own the full credential flow. Some may need to support multiple environments or products where credentials are resolved dynamically instead of being tied only to a generated Amplify client configuration file.

If the module assumed one Amplify/Cognito setup, it would become less reusable.

So I chose a runtime credentials path.

Instead of coupling the package to a specific Amplify Auth configuration file or Cognito setup, the host app can fetch temporary credentials from its backend and pass them into the module before rendering the detector.

In this package, that happens through setAuthCredentials.

Internally, the native layer provides those credentials to the AWS Face Liveness SDK through a custom credentials provider.

This keeps the boundary clear: the host app owns its backend and authentication model, the backend issues temporary scoped credentials, and the module focuses on the native Face Liveness integration.

Module architecture

The module is designed around a simple idea:

Keep the host application responsible for trust, and keep the package responsible for the native liveness experience.

To make that work, the package is split into a few layers.

Layer Responsibility
Backend Creates the liveness session, issues temporary credentials, and verifies the result
JavaScript API Exposes methods for setting and clearing credentials, and a detector view for the app to render
Credential store Holds temporary credentials until they are needed by the native SDK
Custom credentials provider Reads from the credential store and provides credentials to AWS Amplify
Expo config plugin Prepares native project configuration during prebuild
iOS native layer Renders the AWS Face Liveness detector on iOS
Android native layer Renders the AWS Face Liveness detector on Android
Event bridge Sends completion and error events from native code back to JavaScript

The credential store and custom credentials provider are separate on purpose.

The store is responsible for holding the latest credentials passed from JavaScript. The provider is responsible for giving those credentials to the native AWS SDK when it asks for them.

This structure keeps the package focused.

It does not try to own the backend. It does not decide how the consuming app authenticates users. It only provides the bridge needed for an Expo app to use the native Face Liveness experience on iOS and Android.

Public API design

I wanted the JavaScript-facing API to stay small, explicit, and easy to reason about.

The package exposes a few core pieces:

  • setAuthCredentials
  • clearAuthCredentials
  • FaceLivenessDetectorView
  • onAnalysisComplete
  • onError

The intended flow is simple:

Set credentials. Render the detector. Listen for completion. Verify the result on the backend.

setAuthCredentials gives the native layer the temporary credentials needed to run the liveness flow.

clearAuthCredentials lets the host app intentionally remove credentials when they are no longer needed.

FaceLivenessDetectorView is the native UI entry point in the React Native tree.

onAnalysisComplete tells the app when the capture flow has completed.

onError gives the app a way to handle failures from the native layer.

Config plugin design

For Expo projects, a package is not complete if developers still have to manually patch native files every time they run prebuild.

That is why the config plugin is an important part of the package.

The plugin helps prepare the native project configuration needed by the module, including:

  • camera permissions
  • Android desugaring
  • Android Compose compiler setup
  • native configuration required by the underlying implementation

If every consuming project has to manually rediscover the same native setup steps after prebuild, the abstraction is incomplete.

The config plugin moves those repeatable setup details closer to the package, so the consuming app can focus on the product flow.

Security and trust boundary

One of the easiest mistakes in mobile integrations is placing too much trust in the client.

For this package, I wanted the boundary to stay clear: the mobile app can start and display the liveness flow, but the backend should create the session, issue temporary credentials, and verify the final result.

Long-lived AWS credentials should not be bundled into the mobile app. Anything shipped inside a mobile app should be treated as visible eventually.

That separation keeps the mobile client as a participant in the experience, while the backend remains responsible for trust decisions.

Key decisions and tradeoffs

Decision Why it mattered
Build as an Expo module The target users are Expo teams that need native SDK integration without abandoning their Expo workflow.
Support iOS and Android AWS Face Liveness is distributed through native mobile experiences, so both platforms needed first-class support.
Use runtime temporary credentials It keeps the module reusable across apps with different backend and auth setups.
Keep result verification on the backend The backend should remain the source of truth for sensitive verification decisions.
Add a config plugin It reduces repeated native setup work across consuming Expo projects.
Add unsupported web entry points Unsupported platforms should fail clearly instead of silently behaving in unexpected ways.
Start with a focused API A smaller API is easier to document, test, reason about, and improve based on real feedback.

There are also important limitations.

The package does not work in Expo Go because it includes native iOS and Android code. It requires a custom development client or a native/EAS build.

Web is also not supported. The package is focused on the native mobile Face Liveness experience.

The consuming app still needs a backend flow. The package does not create liveness sessions or verify final results by itself. That responsibility should remain on the backend.

Not every customization option from the underlying native SDKs is exposed yet. The first release focuses on the core liveness flow.

Finally, native SDK compatibility still matters. Since the package depends on native AWS Amplify SDKs, future changes in Expo, React Native, Android, iOS, or AWS dependencies may require compatibility work.

How to use the package

To try the package, install it from npm:

npm install expo-face-liveness-for-aws-amplify
Enter fullscreen mode Exit fullscreen mode

Add the config plugin to your Expo config:

{
  "expo": {
    "plugins": [
      "expo-face-liveness-for-aws-amplify"
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

At a high level, the app flow looks like this:

import {
  FaceLivenessDetectorView,
  setAuthCredentials,
} from "expo-face-liveness-for-aws-amplify";

await setAuthCredentials({
  accessKeyId,
  secretAccessKey,
  sessionToken,
  expiration,
});

<FaceLivenessDetectorView
  sessionId={sessionId}
  region="us-east-1"
  onAnalysisComplete={() => {
    // Ask your backend to verify the result.
  }}
  onError={(event) => {
    // Handle native SDK or configuration errors.
  }}
/>;
Enter fullscreen mode Exit fullscreen mode

expiration should match the format expected by the package API for temporary credential expiration. In your implementation, document whether this should be an ISO string, timestamp, or JavaScript Date value so users do not have to guess.

For full setup instructions, backend requirements, and troubleshooting, see the GitHub documentation.

GitHub: https://github.com/olawalethefirst/expo-face-liveness-for-aws-amplify
npm: https://www.npmjs.com/package/expo-face-liveness-for-aws-amplify

Gotchas to know before using it

There are a few important things to know before trying the package.

First, it will not work in Expo Go. The package includes native iOS and Android code, so you need a custom development client or a native/EAS build.

Second, the app still needs a backend. The package does not create Face Liveness sessions or verify the final result. It only handles the native mobile liveness experience.

Third, credentials must be available before the detector is rendered. In this package, that means calling setAuthCredentials before mounting FaceLivenessDetectorView.

Fourth, native SDK compatibility still matters. Since the package depends on the underlying AWS Amplify native SDKs, changes across Expo, React Native, iOS, Android, or AWS dependencies may require compatibility updates.

Lessons learned

A few lessons stood out while building this.

First, native SDK integration is rarely just about rendering a native view. Build configuration, permissions, credentials, platform differences, and backend responsibilities all matter.

Second, reusable infrastructure often starts from repeated product pain. A one-off solution may be enough the first time. When the same requirement appears across multiple products, abstraction becomes more valuable.

Third, identity-sensitive flows need clear trust boundaries. The mobile app can provide the experience, but the backend should own session creation, credential issuance, and result verification.

Roadmap

The first release focuses on the core Face Liveness flow for Expo projects on iOS and Android.

Next, I want to improve the package around the areas that make real integrations easier: clearer backend examples, better example app coverage, broader Expo SDK compatibility testing, CI checks for native builds, and clearer documentation around native SDK customization boundaries.

The goal is to keep the roadmap practical and shaped by real usage feedback, not to make the package own every part of the liveness experience.

Conclusion

This package started from a product integration problem.

At first, the goal was to help a team move forward without rewriting an application. But as the same need appeared across more products, the problem became bigger than one implementation.

That is what led me to build expo-face-liveness-for-aws-amplify.

For me, the most valuable part was not only getting the native SDKs to work. It was designing the boundary between business need, backend trust, native implementation, and developer experience.

That is the kind of engineering I enjoy most: turning repeated product needs into infrastructure other teams can build on.

Top comments (8)

Collapse
 
olusegun_oluwole_12a938ea profile image
OLUSEGUN OLUWOLE

Great article

Collapse
 
olawalethefirst profile image
Olawale Bashiru

You're so kind Segun

Collapse
 
cosmas_egbosi_cbe41683598 profile image
Cosmas Egbosi

Finally, an article that truly educates!

Collapse
 
olawalethefirst profile image
Olawale Bashiru

Thank you Cosmas! :)

Collapse
 
oluwaseun_owolabi_0470ff4 profile image
Oluwaseun Owolabi

Wow.
This is beautiful πŸ‘πŸ½πŸ‘πŸ½πŸ‘πŸ½πŸ‘πŸ½

Collapse
 
olawalethefirst profile image
Olawale Bashiru

I appreciate you Seun

Collapse
 
victory_oshoma_8fd5ac33ba profile image
VICTORY OSHOMA

This is nicee… Well done…

Collapse
 
olawalethefirst profile image
Olawale Bashiru

Thank you Victory