Supabase is a platform as a service (PaaS) that provides all the backend tools you need to build apps as quickly and safely as possible. This service includes an authentication product, which is the focus of this tutorial. The authentication product allows developers to add login and sign-up to their applications easily.
However, because email and passwords are insecure, it is critical to be able to validate the user using a possession factor (i.e. a verified mobile phone number). If the application cannot actively verify the phone number, then the user may not be who they say they are. tru.ID's PhoneCheck helps resolve this problem.
The tru.ID PhoneCheck API confirms the ownership of a mobile phone number by confirming the presence of an active SIM card with the same number. A mobile data session is created with a request to a Check URL unique to this SIM card. tru.ID then resolves a match between the phone number entered by the user and the phone number that the mobile network operator (MNO) identifies as the owner of the mobile data session.
Before you begin
To follow along with this tutorial, you'll need:
- A tru.ID Account
- A Supabase Account
- A mobile phone with an active data SIM card
Getting started
In your terminal, clone the starter-files
branch with the following command:
git clone -b starter-files https://github.com/tru-ID/supabase-passwordless-authentication.git
If you're only interested in the finished code in main
, then run:
git clone -b main https://github.com/tru-ID/supabase-passwordless-authentication.git
A tru.ID Account is needed to make the PhoneCheck
API requests, so make sure you've created one.
You'll also need the tru.ID CLI, which is used to create a project and run a local server for development. To install this CLI, run the following command in your terminal:
npm i -g @tru_id/cli
Set up the CLI with the tru.ID credentials which you can find within the tru.ID console.
$ tru setup:credentials {YOUR_CLIENT_ID} {YOUR_CLIENT_SECRET} EU
For this tutorial, you'll be using the development server plugin, which runs a development server from the tru.ID CLI. Instructions to install the development server can be found on our GitHub page.
Now, create a new tru.ID project within the root directory via:
tru projects:create rn-supabase-auth --project-dir .
Run the development server, pointing it to the directory containing the newly created project configuration. This command will also open up a localtunnel
to your development server, making it publicly accessible to the Internet so that your mobile phone can access it when only connected to mobile data. Please note the LocalTunnel URL, as you will need to use it later in this tutorial.
tru server -t
Setting up Supabase
As previously stated, a Supabase account is needed. So, if you haven't already, create a Supabase account.
Once logged in, you'll see an interface like the one shown below:

Select ‘new project’, which will take you to a page similar to the one shown below. On this page, enter your desired credentials for this project.

You'll then get redirected to the dashboard, where you can find and click on the icon on the left showing two people or the ‘Try Auth’ button.

You're then taken to the following page which shows settings for your project, such as the Site URL
and Email Auth
. Under ‘Email Auth’, toggle off ‘Enable Email Confirmations’, as shown below:

Next, in your project, copy the file .env.example
to .env
:
cp .env.example .env
Replace SUPABASE_URL
with the URL
value found under ‘Settings > API > Config > URL’ in your project dashboard. Also, replace SUPABASE_PUBLIC_ANON
with the anon public
value found under ‘Settings > API > Project API keys > anon public’.
Start the project
You've now completed the configuration and setup parts required to begin this tutorial. It's time to get started with the project. First, you need to install the dependencies. In your terminal, make sure you're currently in the project directory, and run the following command:
npm install
To test everything is working from the start, make sure you have a physical device connected and run one of the following two commands, depending on which device you're testing with:
npm run android#ornpm run ios
On your phone, you'll see a screen similar to the one displayed below:

Get the user's credentials on the mobile device
The next step is to determine how to receive a user's credentials on the mobile device. To store this information, you'll be using state management to the store, and some UI changes are also required for the user to input their information.
Authentication with Supabase Auth will require the user to input an email
and their desired password
. They'll also need to input their phone_number
required to perform the PhoneCheck.
Open src/App.js
and locate the line const App = () => {
. Below this line, add the following additions for your state management:
const baseURL = '<YOUR_LOCAL_TUNNEL_URL>'const [email, setEmail] = useState('')const [password, setPassword] = useState('')const [loading, setLoading] = useState(false)const signUpHandler = async () => {}
Update the UI to support these bits of information in your store. Find the line <Text style={styles.heading}>Sign Up</Text>
, and below this, add the following two TextInput
, ActivityIndicator
, and TouchableOpacity
components:
<TextInputstyle={styles.textInput}placeholder="Email"placeholderTextColor="#d3d3d3"keyboardType="default"value={email}editable={!loading}onChangeText={(value) => setEmail(value.replace(/\s+/g, ''))}/><TextInputstyle={styles.textInput}placeholder="Password"placeholderTextColor="#d3d3d3"keyboardType="default"secureTextEntryvalue={password}editable={!loading}onChangeText={(value) => setPassword(value.replace(/\s+/g, ''))}/>{loading ? (<ActivityIndicatorstyle={styles.spinner}size="large"color="#00ff00"/>) : (<TouchableOpacity onPress={signUpHandler} style={styles.button}><Text style={styles.buttonText}>Sign Up</Text></TouchableOpacity>)}
If you reload the app on your device, the screen will update to show what's displayed below:

Create reusable error handlers
The application needs two methods for handling different results:
- The
errorHandler
function requires two parameters, thetitle
and themessage
prop. It then renders anAlert
to the screen using those props. - The
successHandler
renders anAlert
.
Above the return
in src/App.js
add the following two handler functions:
const errorHandler = ({ title, message }) => {return Alert.alert(title, message, [{text: 'Close',onPress: () => console.log('Alert closed'),},])}const successHandler = () => {Alert.alert('Login Successful', '✅', [{text: 'Close',onPress: () => console.log('Alert closed'),},])}
Currently, these two handlers don't get called in your application, but they are needed later in the tutorial.
Handle user signup
A third handler function is needed to handle the user's signup request. Superbase will be used to authenticate the user with their email and password. Find the line const signUpHandler = async () => {}
and replace it with:
const signUpHandler = async () => {const { session, error } = await supabase.auth.signUp({email,password,})if (!error && session) {setLoading(false)successHandler()return} else {console.log(JSON.stringify(error))setLoading(false)errorHandler({ title: 'Something went wrong.', message: error.message })return}}
The user signs in by inputting the email
and password
fields on the form. The session
and error
responses are returned to determine the outcome of the request.
A check is made to see if there is an error
and a session
value returned to the signUp
request. If there is no value in the error
variable, the loading
icon is no longer needed, and the code calls the successHandler
function. Otherwise, the application calls the errorHandler
.
A successful registration is shown in the image below:

Adding SIM card-based authentication to our existing workflow
So far, you've implemented a workflow to handle authentication requests using Supabase Auth successfully. The next step is to add SIM swap detection to the sign-up flow using the tru.ID PhoneCheck API.
First, you need to define the phoneNumber
state and update the UI responsible for receiving the user's phone number. Find the line that defines the password
state, and below this, add the definition for the phoneNumber
state as shown below:
const App = () => {...const [phoneNumber, setPhoneNumber] = useState('')...}
In the return()
, underneath the TextInput
for the password
, add a new component to take the user's phoneNumber
. This component is shown below:
<TextInputstyle={styles.textInput}placeholder="Number ex. +448023432345"placeholderTextColor="#d3d3d3"keyboardType="phone-pad"value={phoneNumber}editable={!loading}onChangeText={(value) => setPhoneNumber(value.replace(/\s+/g, ''))}/>
Refreshing the app on the device will present you with an updated screen similar to the one shown below:

An API request to tru.ID's Reachability API confirms whether the user's mobile network operator supports PhoneCheck or not. This request is made easier with the use of the tru.ID React Native SDK, which you'll need to install with the following command:
npm install @tru_id/tru-sdk-react-native
Add the import of the tru.ID React Native SDK to the top of src/App.js
:
import TruSDK from '@tru_id/tru-sdk-react-native'
Now, find your signUpHandler
and replace the contents with what's shown below:
const signUpHandler = async () => {setLoading(true)// check if we have coverage using the `isReachable` functionconst reachabilityDetails = await TruSDK.isReachable()console.log('Reachability details are', reachabilityDetails)const info = JSON.parse(reachabilityDetails)if (info.error && info.error.status === 400) {errorHandler({title: 'Something went wrong.',message: 'Mobile Operator not supported',})setLoading(false)return}let isPhoneCheckSupported = falseif (info.error && info.error.status !== 412) {isPhoneCheckSupported = falsefor (const { product_name } of info.products) {console.log('supported products are', product_name)if (product_name === 'Phone Check') {isPhoneCheckSupported = true}}} else {isPhoneCheckSupported = true}// If the PhoneCheck API is supported, proceed with PhoneCheck verification and Supabase Authif (isPhoneCheckSupported) {// proceed with Supabase Authconst { session, error } = await supabase.auth.signUp({email,password,})if (!error && session) {setLoading(false)successHandler()return} else {console.log(JSON.stringify(error))setLoading(false)errorHandler({ title: 'Something went wrong.', message: error.message })return}} else {const { session, error } = await supabase.auth.signUp({email,password,})if (!error && session) {setLoading(false)successHandler()return} else {setLoading(false)errorHandler({ title: 'Something went wrong.', message: error.message })return}}}
The code above calls the isReachable
function, which returns the network_id
, network_name
, country_code
, and products
. The products
array is an optional array supported by the MNO. There is also an error
object which contains any potential errors.
Next, the application creates the variable isPhoneCheckSupported
. If the error status is not a 412
, the application loops through the products and checks whether the product_name
equals Phone Check
; if it does, then isPhoneCheckSupported
is set to true
.
If the isPhoneCheckSupported
variable is false
by the end of the loop, the PhoneCheck is not supported. If isPhoneCheckSupported
is false, then it calls the errorHandler
. Otherwise, the PhoneCheck is possible.
Now the application needs functionality to create the PhoneCheck, so below successHandler
add the following createPhoneCheck
function:
const createPhoneCheck = async (phoneNumber) => {const body = { phone_number: phoneNumber }console.log('tru.ID: Creating PhoneCheck for', body)const response = await fetch(`${baseURL}/phone-check`, {body: JSON.stringify(body),method: 'POST',headers: {'Content-Type': 'application/json',},})const json = await response.json()return json}
Now, inside the signUpHandler
function, find the line if (isPhoneCheckSupported) {
, and below this, add the following trigger to create a PhoneCheck
:
const phoneCheckResponse = await createPhoneCheck(phoneNumber)await TruSDK.check(phoneCheckResponse.check_url)
The variable phoneCheckResponse
returns a check_id
and check_url
values.
With the check_url
opened, you now need to get the PhoneCheck result using the check_id
value. To create a function for data fetching, beneath the function createPhoneCheck
add the following:
const getPhoneCheck = async (checkId) => {const response = await fetch(`${baseURL}/phone-check?check_id=${checkId}`)const json = await response.json()return json}
Now in the signUpHandler
find the line await TruSDK.check(phoneCheckResponse.check_url)
. Below this line, add the call to the newly created method:
const phoneCheckResult = await getPhoneCheck(phoneCheckResponse.check_id)// if we do not have a match, do not proceed with Supabase authif (!phoneCheckResult.match) {setLoading(false)errorHandler({title: 'Something Went Wrong',message: 'PhoneCheck verification unsuccessful.',})return}
The constant PhoneCheckResult
returns a match
value. If there is no match
, the app renders the errorHandler
and stops execution – it does not need to proceed with Supabase Auth.
If there is not a match, then the application will be displayed as shown in the image below:

Wrapping up
That's it! With everything in place, you now have a seamless sign-up onboarding flow with minimal UX friction, resulting in reduced user drop-offs.