Dating and matching apps are one of the most in-demand app categories today. Swipe interactions, match notifications, and real-time chat — building all of this from scratch can seem overwhelming. But with Rork, even developers with limited coding experience can put together the core features of a matching app.
Below, we build a matching app with Rork step by step, from designing the swipe UI to wiring up the chat experience.
What We're Building
- A swipe UI with profile cards (swipe right = like, swipe left = pass)
- Match detection when two users mutually like each other
- Real-time chat between matched users (powered by Supabase Realtime)
- Profile photo upload (Supabase Storage)
Prerequisites
To follow this tutorial, you'll need:
- A Rork account (the free plan works fine to get started)
- A Supabase account (has a generous free tier)
- A basic understanding of React Native (helpful but not required)
If you're new to Rork, check out Getting Started with Rork before diving in.
Setting Up Supabase
You'll need four tables in your Supabase database:
- profiles — user data (name, age, bio, avatar URL)
- swipes — records of who liked or passed on whom
- matches — pairs who mutually liked each other
- messages — chat messages between matched users
Run the following SQL in the Supabase SQL Editor:
-- User profiles
create table profiles (
id uuid references auth.users primary key,
name text not null,
age integer,
bio text,
avatar_url text,
created_at timestamptz default now()
);
-- Swipe records
create table swipes (
id uuid default gen_random_uuid() primary key,
swiper_id uuid references profiles(id),
swiped_id uuid references profiles(id),
direction text check (direction in ('like', 'pass')),
created_at timestamptz default now(),
unique(swiper_id, swiped_id)
);
-- Match records
create table matches (
id uuid default gen_random_uuid() primary key,
user1_id uuid references profiles(id),
user2_id uuid references profiles(id),
created_at timestamptz default now()
);
-- Chat messages
create table messages (
id uuid default gen_random_uuid() primary key,
match_id uuid references matches(id),
sender_id uuid references profiles(id),
body text not null,
created_at timestamptz default now()
);
-- Enable Realtime for messages
alter publication supabase_realtime add table messages;After running this SQL, set up Row Level Security (RLS) policies in Supabase under Authentication → Policies to ensure users can only access their own data.
Implementing the Swipe UI
Create your project in Rork and enter a prompt to generate the swipe screen. Rork will produce React Native code from your description.
Example Rork Prompt
Build a swipe screen for a dating/matching app.
- Show a profile card in the center of the screen with photo, name, age, and bio
- Swipe right to like, swipe left to pass
- Use React Native Gesture Handler for smooth animations
- Add "❌ Pass" and "❤️ Like" buttons below the card as well
- Fetch profile data from a Supabase table called profiles
The code Rork generates will look something like this:
// screens/SwipeScreen.tsx (Rork-generated example)
import { PanGestureHandler } from 'react-native-gesture-handler';
import Animated, {
useAnimatedGestureHandler,
useAnimatedStyle,
useSharedValue,
withSpring,
runOnJS,
} from 'react-native-reanimated';
export function SwipeScreen() {
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const gestureHandler = useAnimatedGestureHandler({
onActive: (event) => {
translateX.value = event.translationX;
translateY.value = event.translationY;
},
onEnd: (event) => {
if (event.translationX > 100) {
// Swiped right — like
runOnJS(handleLike)();
} else if (event.translationX < -100) {
// Swiped left — pass
runOnJS(handlePass)();
}
translateX.value = withSpring(0);
translateY.value = withSpring(0);
},
});
return (
<PanGestureHandler onGestureEvent={gestureHandler}>
<Animated.View style={[styles.card, animatedStyle]}>
{/* Profile card content here */}
</Animated.View>
</PanGestureHandler>
);
}Match Detection Logic
After each swipe, record the interaction in Supabase and check for a mutual like:
// utils/swipe.ts
import { supabase } from './supabase';
export async function handleSwipe(
swiperId: string,
swipedId: string,
direction: 'like' | 'pass'
) {
// Record the swipe
await supabase.from('swipes').insert({
swiper_id: swiperId,
swiped_id: swipedId,
direction,
});
if (direction === 'like') {
// Check if the other user already liked back
const { data } = await supabase
.from('swipes')
.select('id')
.eq('swiper_id', swipedId)
.eq('swiped_id', swiperId)
.eq('direction', 'like')
.single();
if (data) {
// Mutual like — it's a match!
await supabase.from('matches').insert({
user1_id: swiperId,
user2_id: swipedId,
});
return { matched: true };
}
}
return { matched: false };
}When a match occurs, show a celebratory modal and navigate the user to the chat screen.
Real-Time Chat
Once two users have matched, they can start chatting. Supabase Realtime makes it easy to deliver messages instantly to both parties without polling.
// screens/ChatScreen.tsx
import { useEffect, useState } from 'react';
import { supabase } from '../utils/supabase';
export function ChatScreen({ matchId }: { matchId: string }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
// Load existing messages
fetchMessages();
// Subscribe to new messages in real time
const channel = supabase
.channel(`match:${matchId}`)
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'messages',
filter: `match_id=eq.${matchId}`,
},
(payload) => {
setMessages((prev) => [...prev, payload.new]);
}
)
.subscribe();
return () => supabase.removeChannel(channel);
}, [matchId]);
const sendMessage = async (body: string) => {
await supabase.from('messages').insert({
match_id: matchId,
sender_id: currentUserId,
body,
});
// Realtime handles delivery to the other user automatically
};
return (
// Chat UI
);
}For a deeper dive into real-time chat architecture, check out Building a Real-Time Chat App with Rork and Supabase.
Profile Photo Upload
Letting users upload their own photos is essential for any matching app. Here's how to handle it with Supabase Storage:
// utils/uploadAvatar.ts
import * as ImagePicker from 'expo-image-picker';
import { supabase } from './supabase';
import { decode } from 'base64-arraybuffer';
export async function uploadAvatar(userId: string): Promise<string | null> {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [1, 1], // Square crop for profile photos
quality: 0.8,
base64: true,
});
if (result.canceled || !result.assets[0].base64) return null;
const fileName = `avatars/${userId}-${Date.now()}.jpg`;
const { error } = await supabase.storage
.from('profiles')
.upload(fileName, decode(result.assets[0].base64), {
contentType: 'image/jpeg',
upsert: true,
});
if (error) throw error;
const { data } = supabase.storage.from('profiles').getPublicUrl(fileName);
return data.publicUrl;
}Common Errors and Fixes
RLS policy error (403 Forbidden)
If you see "permission denied for table swipes" or similar errors, your Row Level Security policies are missing or misconfigured. In the Supabase dashboard under Authentication → Policies, add policies that allow authenticated users to read and write their own rows.
Swipe animation feels choppy
Check how you're using runOnUI vs runOnJS in Reanimated. Heavy work (like database calls) belongs on the JS thread, while animation values should stay on the UI thread. Mixing them up causes jank.
Supabase Realtime not firing
Double-check that you ran alter publication supabase_realtime add table messages;. Without this step, the Realtime subscription won't receive any events.
Looking back
With Rork, you can build the core pillars of a matching app — swipe interactions, match detection, and real-time chat — without having to wire everything together from scratch. The key is to nail your Supabase data model first, then use Rork's prompting workflow to generate and iterate on the UI.
Matching apps have plenty of room to grow: push notifications, location-based filtering, premium subscriptions, and more. Start with an MVP, ship it, and evolve based on how real users engage with it.
For a comprehensive look at building and monetizing apps as an indie developer, React Native Expo Architecture Guide is a great next read.