With Rork Max, building a fully functional e-commerce app—something that would typically require a team of engineers—is now achievable by a solo developer. Over this tutorial you build that shopping app from scratch — product catalog, cart, Stripe checkout, order management, and push notifications — and take it all the way to production-ready.
What you'll ship by the end of this tutorial:
- Product listing and detail screens (with Supabase Storage for images)
- Cart management (local state synced to Supabase)
- Stripe checkout (card, Apple Pay, and Google Pay)
- Order history and status tracking
- Push notifications for shipping and delivery updates
This tutorial assumes you're familiar with Rork basics. If you're just getting started, check out the Getting Started with Rork guide first.
Prerequisites and Environment Setup
What You'll Need
| Tool | Purpose | Cost |
|---|---|---|
| Rork Max | App development & AI generation | Paid plan |
| Supabase | Database, auth, storage | Free tier available |
| Stripe | Payment processing | Transaction fees only |
| Expo EAS | Build & push notifications | Free tier available |
Setting Up Supabase
Create a new Supabase project and run the following SQL to set up your schema. You can ask Rork Max to help you design the tables by prompting: "Help me design a Supabase schema for an e-commerce app."
-- Products table
CREATE TABLE products (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
price INTEGER NOT NULL, -- in smallest currency unit (e.g., cents)
stock INTEGER DEFAULT 0,
image_url TEXT,
category TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Orders table
CREATE TABLE orders (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
status TEXT DEFAULT 'pending', -- pending / paid / shipped / delivered
total_amount INTEGER NOT NULL,
stripe_payment_intent_id TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Order line items
CREATE TABLE order_items (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
order_id UUID REFERENCES orders(id) ON DELETE CASCADE,
product_id UUID REFERENCES products(id),
quantity INTEGER NOT NULL,
unit_price INTEGER NOT NULL
);
-- Enable Row Level Security
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can only view their own orders"
ON orders FOR SELECT
USING (auth.uid() = user_id);Initializing Your Rork Max Project
Open Rork and start a new project with this prompt:
Build an e-commerce app with:
- Product listing, detail, cart, checkout, and profile screens
- Supabase integration (URL: xxx, anon key: xxx)
- React Native / Expo base
- Tab navigation: Home, Cart, Profile
Rork Max will scaffold the entire project. From there, you'll refine specific screens with targeted prompts.
Architecture Overview
How the Pieces Fit Together
[React Native / Expo App]
↕ (Supabase JS Client)
[Supabase]
├── Auth (email / social login)
├── Database (products, orders)
└── Storage (product images)
↕ (API calls)
[Stripe]
├── Payment Intents (create charge)
└── Webhooks (payment confirmation)
↕
[Expo Push Notifications]
└── Triggered on order status changes
State Management Strategy
Cart state is managed with React Context + useReducer, and synced to Supabase for logged-in users. This lets guests add items to a cart before signing up, then seamlessly carry that cart over after authentication.
Step-by-Step Implementation
Step 1: Product Listing Screen
After Rork Max generates the initial screen, refine it with this implementation to properly fetch in-stock products:
// screens/ProductListScreen.tsx
import { useEffect, useState } from 'react';
import {
FlatList, View, Text, Image,
TouchableOpacity, StyleSheet, ActivityIndicator
} from 'react-native';
import { supabase } from '../lib/supabase';
type Product = {
id: string;
name: string;
price: number;
image_url: string;
stock: number;
};
export default function ProductListScreen({ navigation }: any) {
const [products, setProducts] = useState<Product[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchProducts();
}, []);
async function fetchProducts() {
const { data, error } = await supabase
.from('products')
.select('*')
.gt('stock', 0) // only show in-stock items
.order('created_at', { ascending: false });
if (!error && data) setProducts(data);
setLoading(false);
}
if (loading) return <ActivityIndicator style={{ flex: 1 }} />;
return (
<FlatList
data={products}
numColumns={2}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<TouchableOpacity
style={styles.card}
onPress={() => navigation.navigate('ProductDetail', { product: item })}
>
<Image source={{ uri: item.image_url }} style={styles.image} />
<Text style={styles.name}>{item.name}</Text>
<Text style={styles.price}>${(item.price / 100).toFixed(2)}</Text>
</TouchableOpacity>
)}
/>
);
}
const styles = StyleSheet.create({
card: { flex: 1, margin: 8, backgroundColor: '#fff', borderRadius: 12,
overflow: 'hidden', elevation: 2 },
image: { width: '100%', aspectRatio: 1 },
name: { fontSize: 14, fontWeight: '600', padding: 8 },
price: { fontSize: 16, color: '#e53e3e', paddingHorizontal: 8, paddingBottom: 8 },
});Expected output: A two-column grid of in-stock products. Tapping any card navigates to the product detail screen.
Step 2: Stripe Checkout
Payment processing is handled server-side via a Supabase Edge Function. Prompt Rork Max with: "Create a Supabase Edge Function that creates a Stripe Payment Intent and saves a pending order to the database."
// supabase/functions/create-payment-intent/index.ts
import Stripe from 'npm:stripe@14.21.0';
import { createClient } from 'npm:@supabase/supabase-js@2';
const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY')!);
Deno.serve(async (req) => {
const { amount, currency, items, userId } = await req.json();
try {
// 1. Create a Payment Intent
const paymentIntent = await stripe.paymentIntents.create({
amount,
currency: currency ?? 'usd',
automatic_payment_methods: { enabled: true },
metadata: { userId, itemCount: items.length.toString() },
});
// 2. Save a pending order to Supabase
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
);
const { data: order } = await supabase
.from('orders')
.insert({
user_id: userId,
total_amount: amount,
status: 'pending',
stripe_payment_intent_id: paymentIntent.id,
})
.select()
.single();
// 3. Save order line items
const orderItems = items.map((item: any) => ({
order_id: order.id,
product_id: item.productId,
quantity: item.quantity,
unit_price: item.price,
}));
await supabase.from('order_items').insert(orderItems);
return new Response(JSON.stringify({
clientSecret: paymentIntent.client_secret,
orderId: order.id,
}), { headers: { 'Content-Type': 'application/json' } });
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
});Expected output: Returns { clientSecret, orderId } which the app uses to present the Stripe payment sheet.
On the client side:
// screens/CheckoutScreen.tsx (excerpt)
import { useStripe } from '@stripe/stripe-react-native';
export default function CheckoutScreen({ cartItems }: any) {
const { confirmPayment } = useStripe();
async function handlePay() {
const totalAmount = cartItems.reduce(
(sum: number, item: any) => sum + item.price * item.quantity, 0
);
const res = await fetch(`${SUPABASE_URL}/functions/v1/create-payment-intent`, {
method: 'POST',
headers: { 'Content-Type': 'application/json',
'Authorization': `Bearer ${session.access_token}` },
body: JSON.stringify({ amount: totalAmount, items: cartItems,
userId: session.user.id }),
});
const { clientSecret, orderId } = await res.json();
const { error } = await confirmPayment(clientSecret, {
paymentMethodType: 'Card',
});
if (error) {
Alert.alert('Payment failed', error.message);
} else {
navigation.navigate('OrderComplete', { orderId });
}
}
// ...
}Step 3: Push Notifications
Register device tokens and send push notifications when order status changes.
// lib/notifications.ts
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import { supabase } from './supabase';
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: false,
}),
});
export async function registerPushToken(userId: string) {
if (!Device.isDevice) return;
const { status } = await Notifications.requestPermissionsAsync();
if (status !== 'granted') return;
const token = (await Notifications.getExpoPushTokenAsync({
projectId: 'YOUR_EXPO_PROJECT_ID',
})).data;
// Persist the token in Supabase
await supabase
.from('user_push_tokens')
.upsert({ user_id: userId, token, updated_at: new Date().toISOString() });
return token;
}A Stripe Webhook handler in a Supabase Edge Function listens for payment_intent.succeeded, updates the order status to paid, and fires an Expo push notification to the user.
Advanced Patterns
Real-Time Inventory Updates
Supabase Realtime lets you push stock changes to the app the moment another user completes a purchase:
useEffect(() => {
const channel = supabase
.channel('product-stock')
.on(
'postgres_changes',
{ event: 'UPDATE', schema: 'public', table: 'products',
filter: `id=eq.${product.id}` },
(payload) => {
setStock(payload.new.stock);
if (payload.new.stock === 0) {
Alert.alert('Sold out', 'This item just sold out.');
}
}
)
.subscribe();
return () => supabase.removeChannel(channel);
}, [product.id]);Discount Codes with Stripe Coupons
Stripe's Coupon API makes discount codes straightforward. Prompt Rork Max: "Add an Edge Function that validates a Stripe coupon code and returns the discount amount." The AI will generate the server-side validation logic and help you wire it into the checkout screen.
Multi-Currency Support
For international markets, change the currency parameter in the Payment Intent call based on the user's locale. Stripe supports 135+ currencies, so no additional integration is required.
Troubleshooting
Order status isn't updating after a successful payment
Cause: Stripe Webhook isn't reaching your Supabase Edge Function endpoint. Fix:
- In the Stripe Dashboard → Webhooks, verify the endpoint URL is correct.
- Check Edge Function logs with
supabase functions logs. - Make sure you're verifying the Stripe webhook signature on every request.
Supabase queries feel slow
Cause: Missing database indexes. Fix: Add indexes for the most common query patterns:
CREATE INDEX idx_products_category ON products(category);
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status);Push notifications not being delivered
Checklist:
- iOS: Is your APNs certificate configured in EAS Build?
- Android: Is your FCM server key set in your Expo project?
- Has the user granted notification permission?
- Is the push token saved in Supabase? (Check via the Supabase Table Editor)
RLS policy blocking requests (403 error)
Cause: Row Level Security is enabled but no policy exists for that operation. Fix: For admin-only write operations (like updating order status), use a Service Role Key inside an Edge Function—never in client-side code. Exposing the Service Role Key in the app is a critical security vulnerability.
Publishing and Monetization
App Store and Google Play Submission
Key points when submitting an e-commerce app:
- In-App Purchase rules: Physical goods can use external payment processors like Stripe. Digital goods (subscriptions, virtual items) must use Apple/Google's in-app purchase systems.
- Privacy policy: Required for any app handling payment information.
- Review prompts: Use
expo-store-reviewat appropriate moments—never in response to a user action that was negative.
For a complete App Store submission walkthrough, see App Store Publishing Guide.
Monetization Models
Three common e-commerce monetization models and how to implement them with Rork Max:
- Direct sales: Sell your own products with maximum margin (this tutorial's approach)
- Marketplace: Charge listing or transaction fees using Stripe Connect
- SaaS platform: License the shop infrastructure as a monthly subscription using Stripe Billing
For the subscription approach, see Rork Subscription App Monetization.
Performance Optimization
Product image loading speed is critical for e-commerce conversion rates. Store images in Supabase Storage and serve them through Cloudflare Image Resizing for device-optimized delivery. This alone can cut image transfer size by 60–80%. See Rork + Cloudflare AI Backend for the full setup.
Summary
In this tutorial, you built a production-grade e-commerce app using Rork Max, Supabase, and Stripe. Here's a quick recap:
- Architecture: Rork Max (frontend) + Supabase (backend) + Stripe (payments) is the most cost-effective stack for solo developers.
- Security: Row Level Security and Webhook signature verification are non-negotiable.
- Checkout flow: Create a Payment Intent server-side, then call
confirmPayment()on the client. - Push notifications: Store Expo Push Tokens in Supabase and trigger them via Stripe Webhooks.
- Real-time: Supabase Realtime gives you live inventory updates with minimal extra code.
Next steps to consider: an admin sales dashboard, SEO optimization with Next.js, and AI-powered product recommendations. For solo developer business strategy, see Solo Developer App Business.