◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/Getting Started
◈ Getting Started/2026-04-05Intermediate

Build a Matching App with Rork: Implementing Swipe UI and Chat Features

Build a matching app with Rork: swipe UI, Supabase match detection, and real-time chat. A step-by-step tutorial for intermediate React Native developers.

matching appswipe UISupabase37React Native239tutorial20

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.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

◈ Getting Started2026-09-14
Rork No Longer Creates New Expo Projects: Keep Yours, or Rebuild It in SwiftUI?
You can no longer start an Expo project in Rork, but existing ones keep building and shipping. Here is how to decide whether to stay on Expo or rebuild in SwiftUI, and what quietly goes missing when you rebuild.
◈ Getting Started2026-09-08
Whether You Need Rork Max Is Answered by Your Permissions, Not the Price Page
When I cannot decide between standard Rork and Rork Max, I stop reading the price page. Instead I pull every OS capability my app actually asks for out of app.json and Info.plist, then sort them into what Expo covers, what a config plugin covers, and what truly needs native Swift.
◈ Getting Started2026-10-16
The night the Play Console wouldn't let me submit — release notes cap at 500 characters per language
Google Play release notes are limited to 500 characters per language. Here is how I got unstuck after pasting a long update memo, plus a short check script and what to do when an old draft release blocks a new one.