WellAlly Logo
WellAlly康心伴
Development

Building a Secure React Native Wearable App with Supabase and RLS

A full-stack guide to building a secure React Native wearable app. Learn to sync health data in real-time and protect it using Supabase's Row Level Security (RLS) to ensure users can only access their own information.

W
2025-12-11
9 min read

The wearable technology and healthtech markets are exploding. As developers, this presents a massive opportunity, but it also comes with a critical responsibility: protecting sensitive user health data. A data breach in a health app isn't just an inconvenience; it's a serious violation of privacy.

In this tutorial, we'll build a full-stack, secure data pipeline from a React Native application to a Supabase backend. We'll simulate data from a fitness wearable (heart rate and steps) and sync it in real-time. The secret sauce? We'll use Supabase's powerful Row Level Security (RLS) to create database-level rules that ensure users can only ever read and write their own data, providing a foundational layer of security for our app.

What we'll build: A React Native app with user authentication where simulated wearable data is continuously generated and securely uploaded to a Supabase database. A real-time display will show the user only their own data as it syncs.

Prerequisites:

  • Node.js (LTS version) and npm/yarn installed.
  • Expo Go app on your mobile device for testing.
  • A free Supabase account.
  • Basic understanding of React Native and SQL.

Why this matters to developers: Learning to handle user data securely is non-negotiable. This project teaches you a robust, scalable pattern for building secure, real-time applications without the complexity of managing a traditional backend server.

Understanding the Problem

In a typical client-server model, you might have an API endpoint like /api/health-data. Your server-side code would be responsible for checking if the logged-in user has permission to access the requested data. It's a workable solution, but it has flaws:

  • Complexity: You have to write and maintain this authorization logic across multiple endpoints.
  • Potential for Leaks: A single bug in your API code could accidentally expose one user's data to another.
  • Defense in Depth: Relying solely on the API for security is risky. What if an API key is leaked?

Our approach shifts security to the database itself. Supabase allows you to write simple SQL policies that act as a gatekeeper for your data. Even if a request gets past your frontend, the database will reject any action that violates the RLS policy. It’s security at the lowest, most fundamental level.

Prerequisites: Setting Up Your Tools

1. Set up a Supabase Project

  1. Go to your Supabase Dashboard and click "New project".
  2. Give your project a name and a strong database password. Choose a region close to you.
  3. Once the project is created, navigate to the "API" settings in the left sidebar. You will need your Project URL and the anon public key. Keep these handy.

2. Set up a React Native Project

Open your terminal and create a new Expo project:

code
npx create-expo-app wearable-data-sync
cd wearable-data-sync
Code collapsed

Next, install the necessary dependencies to connect to Supabase:

code
npm install @supabase/supabase-js @react-native-async-storage/async-storage react-native-url-polyfill
Code collapsed
  • @supabase/supabase-js: The official JavaScript client for Supabase.
  • @react-native-async-storage/async-storage: A secure, asynchronous storage system that Supabase uses to persist user sessions in React Native.
  • react-native-url-polyfill: A small polyfill required for Supabase to function correctly in the React Native environment.

Step 1: Create the Supabase Backend Table

This is where our health data will live.

What we're doing

We'll use the Supabase SQL Editor to create a health_data table. We'll define columns for heart rate, steps, and crucially, a user_id that links each entry to a specific user.

Implementation

  1. In your Supabase project, go to the "SQL Editor".
  2. Click "+ New query" and run the following SQL:
code
-- 1. Create the health_data table
CREATE TABLE public.health_data (
  id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL,
  created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
  heart_rate INT,
  steps INT
);

-- 2. Enable Row Level Security (RLS) on the table
ALTER TABLE public.health_data ENABLE ROW LEVEL SECURITY;
Code collapsed

How it works

  • We create a simple table to store our metrics.
  • The user_id column is a foreign key that references the id in Supabase's built-in auth.users table. This database-level link is what makes our security policies possible.
  • ALTER TABLE ... ENABLE ROW LEVEL SECURITY; is the critical command. By default, with RLS enabled and no policies, it denies all access to the table. This "secure-by-default" approach prevents accidental data exposure.

Step 2: Connect React Native to Supabase

Now let's configure our mobile app to communicate with our new backend.

What we're doing

We'll create a helper file to initialize the Supabase client using the API keys from our project dashboard.

Implementation

  1. In the root of your React Native project, create a file named lib/supabase.js.
  2. Add the following code, replacing the placeholders with your actual Supabase URL and anon key.
code
// lib/supabase.js
import 'react-native-url-polyfill/auto';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { createClient } from '@supabase/supabase-js';

const supabaseUrl = 'YOUR_SUPABASE_URL';
const supabaseAnonKey = 'YOUR_SUPABASE_ANON_KEY';

export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
  auth: {
    storage: AsyncStorage,
    autoRefreshToken: true,
    persistSession: true,
    detectSessionInUrl: false,
  },
});
Code collapsed

How it works

This code creates a single, reusable supabase client instance. We configure it to use AsyncStorage for session persistence, which is the standard for React Native. The react-native-url-polyfill is imported at the top to ensure compatibility.

Step 3: Implement User Authentication

Users need to log in before they can sync data.

What we're doing

We'll create a simple UI for signing in and signing up, and manage the user's session state.

Implementation

  1. Create a new file src/components/Auth.js.
  2. Add the code for a basic email/password authentication form.
code
// src/components/Auth.js
import React, { useState } from 'react';
import { Alert, StyleSheet, View, TextInput, Button, Text } from 'react-native';
import { supabase } from '../../lib/supabase';

export default function Auth() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [loading, setLoading] = useState(false);

  async function signInWithEmail() {
    setLoading(true);
    const { error } = await supabase.auth.signInWithPassword({
      email: email,
      password: password,
    });
    if (error) Alert.alert(error.message);
    setLoading(false);
  }

  async function signUpWithEmail() {
    setLoading(true);
    const { error } = await supabase.auth.signUp({
      email: email,
      password: password,
    });
    if (error) Alert.alert(error.message);
    setLoading(false);
  }

  return (
    <View style={styles.container}>
      <Text style={styles.header}>Health Sync</Text>
      <TextInput
        style={styles.input}
        onChangeText={(text) => setEmail(text)}
        value={email}
        placeholder="email@address.com"
        autoCapitalize={'none'}
      />
      <TextInput
        style={styles.input}
        onChangeText={(text) => setPassword(text)}
        value={password}
        secureTextEntry={true}
        placeholder="Password"
        autoCapitalize={'none'}
      />
      <View style={styles.buttonContainer}>
        <Button title="Sign in" disabled={loading} onPress={() => signInWithEmail()} />
      </View>
      <View style={styles.buttonContainer}>
        <Button title="Sign up" disabled={loading} onPress={() => signUpWithEmail()} />
      </View>
    </View>
  );
}
// Add some basic styling
const styles = StyleSheet.create({ /* ... */ });
Code collapsed
  1. Now, update your main App.js to manage the session.
code
// App.js
import React, { useState, useEffect } from 'react';
import { View } from 'react-native';
import { supabase } from './lib/supabase';
import Auth from './src/components/Auth';
import HealthDashboard from './src/components/HealthDashboard'; // We will create this next

export default function App() {
  const [session, setSession] = useState(null);

  useEffect(() => {
    supabase.auth.getSession().then(({ data: { session } }) => {
      setSession(session);
    });

    supabase.auth.onAuthStateChange((_event, session) => {
      setSession(session);
    });
  }, []);

  return (
    <View>
      {session && session.user ? <HealthDashboard key={session.user.id} session={session} /> : <Auth />}
    </View>
  );
}
Code collapsed

How it works

App.js becomes a router. It checks for an active session. If a user is logged in, it shows the HealthDashboard; otherwise, it shows the Auth component. The onAuthStateChange listener automatically updates the UI when the user signs in or out. ✨

Step 4: Build the Secure Sync Pipeline

This is the core of our application. We'll simulate wearable data and sync it securely.

What we're doing

We'll create a HealthDashboard component that does three things:

  1. Simulates new heart rate and step data every few seconds.
  2. Inserts this new data into our health_data table.
  3. Subscribes to real-time updates from that table to display the data.

Implementation

code
// src/components/HealthDashboard.js
import React, { useState, useEffect } from 'react';
import { View, Text, Button, StyleSheet, FlatList, Alert } from 'react-native';
import { supabase } from '../../lib/supabase';

const HealthDashboard = ({ session }) => {
  const [healthData, setHealthData] = useState([]);
  const [loading, setLoading] = useState(false);

  // ** 1. Fetch initial data and set up real-time subscription **
  useEffect(() => {
    getHealthData();

    const channel = supabase
      .channel('health_data_changes')
      .on('postgres_changes', { event: '*', schema: 'public', table: 'health_data' }, (payload) => {
        // When new data arrives, add it to the top of our list
        setHealthData((currentData) => [payload.new, ...currentData]);
      })
      .subscribe();

    return () => {
      supabase.removeChannel(channel);
    };
  }, []);

  // ** 2. Simulate wearable and sync data **
  useEffect(() => {
    const interval = setInterval(() => {
      syncNewData();
    }, 5000); // Sync new data every 5 seconds

    return () => clearInterval(interval);
  }, []);

  const getHealthData = async () => {
    setLoading(true);
    const { data, error } = await supabase
      .from('health_data')
      .select('*')
      .order('created_at', { ascending: false }); // Most recent first

    if (error) {
      Alert.alert('Error Fetching Data', 'Have you set up RLS policies?');
      console.error('Fetch error:', error.message);
    } else {
      setHealthData(data);
    }
    setLoading(false);
  };

  const syncNewData = async () => {
    const newHeartRate = Math.floor(Math.random() * (120 - 60 + 1)) + 60; // Random heart rate 60-120
    const newSteps = Math.floor(Math.random() * 50) + 10; // Random steps 10-50

    const { error } = await supabase
      .from('health_data')
      .insert({
        user_id: session.user.id,
        heart_rate: newHeartRate,
        steps: newSteps,
      });

    if (error) {
      Alert.alert('Sync Error', error.message);
    }
  };

  return (
    <View style={styles.container}>
      <Text style={styles.header}>Welcome, {session.user.email}</Text>
      <Text style={styles.subHeader}>Real-time Health Data:</Text>
      <FlatList
        data={healthData}
        keyExtractor={(item) => item.id.toString()}
        renderItem={({ item }) => (
          <View style={styles.dataRow}>
            <Text>❤️ {item.heart_rate} bpm</Text>
            <Text>👟 {item.steps} steps</Text>
            <Text style={styles.timestamp}>{new Date(item.created_at).toLocaleTimeString()}</Text>
          </View>
        )}
      />
      <Button title="Sign Out" onPress={() => supabase.auth.signOut()} />
    </View>
  );
};
// Add styles
const styles = StyleSheet.create({ /* ... */ });

export default HealthDashboard;
Code collapsed

How it works

  • Data Simulation: setInterval in a useEffect hook calls syncNewData every 5 seconds, which generates random data.
  • Data Insertion: syncNewData uses supabase.from('health_data').insert(...) to upload the new data. Critically, it includes the session.user.id to associate the data with the logged-in user.
  • Real-time Subscription: The first useEffect sets up a subscription using supabase.channel(...). It listens for any INSERT events on our health_data table. When a change is detected, the payload is used to update the component's state, causing the UI to re-render instantly.

At this point, if you run the app, you will be able to sign up, but you will not be able to fetch or insert data! You will see an error alert. This is because RLS is enabled, but we haven't defined any "allow" policies yet. The database is correctly blocking all requests.

Step 5: Implement Row-Level Security Policies

This is where we grant permissions and enforce our security model.

What we're doing

We'll write two simple SQL policies in the Supabase dashboard:

  1. A SELECT policy to allow users to read only their own data.
  2. An INSERT policy to allow users to create records only for themselves.

Implementation

Go back to the "Authentication" -> "Policies" section of your Supabase dashboard. Select the health_data table and click "+ New Policy".

1. The SELECT (Read) Policy

  • Click "Create a new policy".
  • Choose the "Get started quickly" template for "Enable read access to everyone".
  • Change the policy name to: Users can view their own health data.
  • Ensure SELECT is the allowed operation.
  • In the USING expression, change true to:
code
auth.uid() = user_id
Code collapsed
  • Click "Review" and "Save policy".

2. The INSERT (Create) Policy

  • Click "+ New Policy" again.
  • Choose the template for "Enable insert access for authenticated users only".
  • Change the policy name to: Users can insert their own health data.
  • Ensure INSERT is the allowed operation.
  • In the WITH CHECK expression, change true to:
code
auth.uid() = user_id
*   Click "Review" and "Save policy".

### How It Works
*   `auth.uid()`: This is a special Supabase function that returns the unique ID of the currently authenticated user making the request.
*   **SELECT Policy**: The `USING (auth.uid() = user_id)` clause acts like a mandatory `WHERE` filter on every `SELECT` query. It tells PostgreSQL, "Only return rows where the `user_id` column matches the ID of the user making this request."
*   **INSERT Policy**: The `WITH CHECK (auth.uid() = user_id)` clause acts as a validation rule. It tells PostgreSQL, "Before you accept this new row, verify that the `user_id` being inserted is the same as the ID of the user performing the insert." This prevents a user from maliciously inserting data on behalf of someone else.

Now, restart your React Native app. Sign up as a new user (or sign in). You should see the dashboard, and new data points will appear every 5 seconds! If you create a second user account and log in, you will see a completely separate, empty list of data, proving your security policies are working.

## Conclusion
We've successfully built a secure, full-stack data pipeline. You now have a React Native app that can handle user authentication and sync data in real-time to a Supabase backend. Most importantly, you've locked down your data with Row Level Security, ensuring that even with direct database access, a user's sensitive health information is isolated and protected.

This pattern is the foundation for building robust, scalable, and secure applications. From here, you can take the project in many directions.

**Next steps for readers:**
*   **Real Wearable Integration**: Replace the simulator with a library like `react-native-health` or `react-native-wearables` to pull real data from Apple Health or Google Fit.
*   **Data Visualization**: Use a charting library to create graphs of the user's health data over time.
*   **Offline Support**: Implement an offline-first strategy using a local database like SQLite and a sync engine to handle cases where the user has no internet connection.

## Resources
*   **Official Supabase Docs**: [supabase.com/docs](https://supabase.com/docs)
*   **Supabase RLS Documentation**: [Supabase Row Level Security Docs](https://supabase.com/docs/guides/auth/row-level-security)
*   **Expo Documentation**: [docs.expo.dev](https://docs.expo.dev/)
*   **React Native Health**: [GitHub Repository](https://github.com/agencyenterprise/react-native-health)


Code collapsed
#

Article Tags

reactnativesupabasefullstacksecurityhealthtech
W

WellAlly's core development team, comprised of healthcare professionals, software engineers, and UX designers committed to revolutionizing digital health management.

Expertise

Healthcare TechnologySoftware DevelopmentUser ExperienceAI & Machine Learning

Found this article helpful?

Try KangXinBan and start your health management journey

© 2024 康心伴 WellAlly · Professional Health Management