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
- Go to your Supabase Dashboard and click "New project".
- Give your project a name and a strong database password. Choose a region close to you.
- Once the project is created, navigate to the "API" settings in the left sidebar. You will need your Project URL and the
anonpublic key. Keep these handy.
2. Set up a React Native Project
Open your terminal and create a new Expo project:
npx create-expo-app wearable-data-sync
cd wearable-data-sync
Next, install the necessary dependencies to connect to Supabase:
npm install @supabase/supabase-js @react-native-async-storage/async-storage react-native-url-polyfill
@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
- In your Supabase project, go to the "SQL Editor".
- Click "+ New query" and run the following SQL:
-- 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;
How it works
- We create a simple table to store our metrics.
- The
user_idcolumn is a foreign key that references theidin Supabase's built-inauth.userstable. 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
- In the root of your React Native project, create a file named
lib/supabase.js. - Add the following code, replacing the placeholders with your actual Supabase URL and anon key.
// 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,
},
});
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
- Create a new file
src/components/Auth.js. - Add the code for a basic email/password authentication form.
// 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({ /* ... */ });
- Now, update your main
App.jsto manage the session.
// 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>
);
}
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:
- Simulates new heart rate and step data every few seconds.
- Inserts this new data into our
health_datatable. - Subscribes to real-time updates from that table to display the data.
Implementation
// 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;
How it works
- Data Simulation:
setIntervalin auseEffecthook callssyncNewDataevery 5 seconds, which generates random data. - Data Insertion:
syncNewDatausessupabase.from('health_data').insert(...)to upload the new data. Critically, it includes thesession.user.idto associate the data with the logged-in user. - Real-time Subscription: The first
useEffectsets up a subscription usingsupabase.channel(...). It listens for anyINSERTevents on ourhealth_datatable. 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:
- A
SELECTpolicy to allow users to read only their own data. - An
INSERTpolicy 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
SELECTis the allowed operation. - In the
USINGexpression, changetrueto:
auth.uid() = user_id
- 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
INSERTis the allowed operation. - In the
WITH CHECKexpression, changetrueto:
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)