Skip to main content
RapidDev - Software Development Agency
weweb-tutorial

Build a SaaS with WeWeb + Supabase: Auth, Database, and Stripe

Build a production SaaS with WeWeb and Supabase by connecting the native Supabase plugin, designing your database schema with Row-Level Security policies, setting up Supabase Auth for user signup and login, creating collections for data fetching, adding Stripe for payments via Edge Functions, and enabling Supabase Realtime for live updates. This is the flagship full-stack WeWeb integration — the complete tutorial takes about 2 hours.

What you'll learn

  • How to connect the native Supabase plugin to WeWeb using Guided OAuth or Custom mode
  • How to design a multi-tenant SaaS database schema in Supabase with Row-Level Security policies
  • How to configure Supabase Auth in WeWeb for email signup, login, and social OAuth
  • How to create Supabase collections in WeWeb and bind live data to page elements
  • How to add Stripe payments via a Supabase Edge Function and handle subscription webhooks
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Advanced14 min read90-120 minWeWeb Essential plan or above (for production deployment); Supabase Free tier sufficient to startMarch 2026RapidDev Engineering Team
TL;DR

Build a production SaaS with WeWeb and Supabase by connecting the native Supabase plugin, designing your database schema with Row-Level Security policies, setting up Supabase Auth for user signup and login, creating collections for data fetching, adding Stripe for payments via Edge Functions, and enabling Supabase Realtime for live updates. This is the flagship full-stack WeWeb integration — the complete tutorial takes about 2 hours.

End-to-End SaaS Architecture with WeWeb and Supabase

WeWeb is the frontend layer. Supabase is the backend layer — PostgreSQL database, authentication, file storage, Edge Functions, and realtime subscriptions. Together they form a complete full-stack SaaS platform without managing your own servers. The native Supabase plugin in WeWeb handles the connection, auto-generates database types, and provides workflow actions for CRUD operations, auth, storage, and realtime. This tutorial builds a complete SaaS from scratch: database design, RLS security policies, user authentication, data collections, Stripe payment integration via Edge Functions, and realtime subscription updates. RapidDev Engineering Team has used this exact stack for dozens of production SaaS applications.

Prerequisites

  • A WeWeb project open in the editor (Essential plan recommended for custom domain deployment)
  • A Supabase account at supabase.com with a new project created
  • A Stripe account at stripe.com with test mode API keys available
  • Basic understanding of SQL for writing RLS policies (examples provided in this tutorial)
  • Your Supabase project URL and anon key from Supabase Dashboard → Project Settings → API

Step-by-step guide

1

Connect the Supabase data source plugin

In WeWeb editor, click the plug icon in the left sidebar to open Plugins. Under 'Data sources', find 'Supabase' and click Add. Choose your connection method: 'Guided' uses OAuth to auto-connect to your Supabase account and lists all your projects for selection — the easiest option. 'Custom' requires you to manually paste your Supabase project URL and anon key from Supabase Dashboard → Project Settings → API → Project URL and anon public key. After connecting, configure your environments: set 'Production' to your live Supabase project and optionally 'Editor' to the same project or a separate staging project. WeWeb will use the Editor config when building in the editor and the Production config when your app is published. Click Save to complete the data source connection.

Expected result: Supabase plugin appears under Plugins → Data sources with a green connected indicator and your project name visible.

2

Design the database schema in Supabase

Open your Supabase Dashboard (outside WeWeb) → Table Editor. Create the core SaaS tables. Start with a `profiles` table that extends Supabase's built-in `auth.users`. Create the table with columns: `id` (uuid, primary key, references auth.users(id)), `full_name` (text), `avatar_url` (text), `plan` (text, default 'free'), `stripe_customer_id` (text), `created_at` (timestamptz, default now()). Next create a `projects` table: `id` (uuid, primary key, default gen_random_uuid()), `user_id` (uuid, references auth.users(id), not null), `name` (text, not null), `description` (text), `status` (text, default 'active'), `created_at` (timestamptz, default now()). Enable Row-Level Security on both tables immediately after creation — every table in a production SaaS should have RLS enabled.

typescript
1-- Injection point: Supabase Dashboard SQL Editor
2-- Run this SQL to create the profiles and projects tables
3
4-- 1. Profiles table (extends auth.users)
5CREATE TABLE public.profiles (
6 id uuid REFERENCES auth.users(id) ON DELETE CASCADE PRIMARY KEY,
7 full_name text,
8 avatar_url text,
9 plan text DEFAULT 'free' NOT NULL,
10 stripe_customer_id text,
11 created_at timestamptz DEFAULT now() NOT NULL
12);
13
14-- Enable RLS immediately
15ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
16
17-- 2. Projects table (multi-tenant data)
18CREATE TABLE public.projects (
19 id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
20 user_id uuid REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL,
21 name text NOT NULL,
22 description text,
23 status text DEFAULT 'active' NOT NULL,
24 created_at timestamptz DEFAULT now() NOT NULL
25);
26
27ALTER TABLE public.projects ENABLE ROW LEVEL SECURITY;
28
29-- 3. Auto-create profile on user signup (trigger)
30CREATE OR REPLACE FUNCTION public.handle_new_user()
31RETURNS trigger AS $$
32BEGIN
33 INSERT INTO public.profiles (id, full_name, avatar_url)
34 VALUES (
35 NEW.id,
36 NEW.raw_user_meta_data->>'full_name',
37 NEW.raw_user_meta_data->>'avatar_url'
38 );
39 RETURN NEW;
40END;
41$$ LANGUAGE plpgsql SECURITY DEFINER;
42
43CREATE TRIGGER on_auth_user_created
44 AFTER INSERT ON auth.users
45 FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();

Expected result: Tables appear in Supabase Table Editor with RLS enabled (yellow shield icon). The trigger is visible in Database → Triggers.

3

Write Row-Level Security policies for multi-tenant data isolation

RLS policies control which rows each authenticated user can access. Without them, every user can read every other user's data. In Supabase Dashboard → Authentication → Policies (or Database → Policies), add the following policies. For the `profiles` table: SELECT policy — users can only read their own profile; UPDATE policy — users can only update their own profile. For the `projects` table: SELECT policy — users can only see their own projects; INSERT policy — users can only create projects with their own user_id; UPDATE and DELETE policies scoped to the user's own rows. All policies use `(SELECT auth.uid()) = user_id` for performance — the subquery form is faster than the inline form `auth.uid() = user_id` in PostgreSQL.

typescript
1-- Injection point: Supabase Dashboard SQL Editor
2-- RLS policies for profiles table
3
4CREATE POLICY "Users can view own profile"
5 ON public.profiles FOR SELECT
6 USING ((SELECT auth.uid()) = id);
7
8CREATE POLICY "Users can update own profile"
9 ON public.profiles FOR UPDATE
10 USING ((SELECT auth.uid()) = id)
11 WITH CHECK ((SELECT auth.uid()) = id);
12
13-- RLS policies for projects table
14
15CREATE POLICY "Users can view own projects"
16 ON public.projects FOR SELECT
17 USING ((SELECT auth.uid()) = user_id);
18
19CREATE POLICY "Users can create own projects"
20 ON public.projects FOR INSERT
21 WITH CHECK ((SELECT auth.uid()) = user_id);
22
23CREATE POLICY "Users can update own projects"
24 ON public.projects FOR UPDATE
25 USING ((SELECT auth.uid()) = user_id)
26 WITH CHECK ((SELECT auth.uid()) = user_id);
27
28CREATE POLICY "Users can delete own projects"
29 ON public.projects FOR DELETE
30 USING ((SELECT auth.uid()) = user_id);

Expected result: Policies appear in Supabase Dashboard → Authentication → Policies for both tables. No user can access another user's data.

4

Configure Supabase Auth in WeWeb

Back in WeWeb, open Plugins → Authentication → find 'Supabase Auth' and click Add. WeWeb will auto-detect your Supabase connection and link them. In the Supabase Auth plugin config, set the 'Unauthenticated redirect page' to your login page (this is the page WeWeb redirects to when an unauthorized user hits a protected page). Click 'Generate' — this creates a `roles` table and a `users_roles` join table in your Supabase project, which enables WeWeb's role-based page access control. Set the 'Sign-in page' to your login page. Under 'Social login', you can optionally enable Google OAuth — this requires configuring an OAuth provider in Supabase Dashboard → Authentication → Providers first. Configure SMTP for email confirmations in Supabase Dashboard → Authentication → SMTP Settings (use Resend, Postmark, or SendGrid).

Expected result: Supabase Auth plugin shows as active. The Generate button creates roles and users_roles tables in Supabase. Login page is configured as the unauthenticated redirect.

5

Build the signup and login pages

Create a new page named 'Login' in WeWeb with a public access setting (Page settings → Access: Public). Add a Form Container element to the page. Inside it, add an Email input and a Password input and a Submit button. Select the Submit button → Workflows tab → On click workflow. Add a 'Supabase Auth - Sign in' action. Bind the email parameter to `formContainer.formData.email` and password to `formContainer.formData.password`. Add a Branching action after it — in the success branch add 'Navigate to page' pointing to your dashboard. In the error branch, display an error message by setting a page variable. For signup: duplicate this page, change the workflow action to 'Supabase Auth - Sign up', and pass `data` with full_name from the form. The trigger will auto-create the Supabase profile via the database trigger set up in Step 2.

Expected result: Login form signs users in and redirects to dashboard. Signup form creates the Supabase Auth user and the corresponding profiles row via database trigger.

6

Create Supabase collections for data display

Open the Data panel (database icon in left sidebar) and click '+ New'. Select 'Supabase' as the source. You will see your Supabase tables listed. Select the 'projects' table. Configure the collection: choose 'Select' as the operation, leave the filter empty (RLS handles user-scoping automatically — each user only sees their own rows). Name the collection 'user-projects'. Set the fetch mode to 'On page load' so it populates when the dashboard opens. Click Save. Back on your dashboard page, add a Container element. In its Settings tab, bind 'Repeat items' to `collections['user-projects'].data`. Inside the container add a Text element bound to `item.name` and another bound to `item.status`. Add a delete button with a workflow: 'Supabase DB - Delete' action on the projects table, filter by `id = item.id`, then 'Fetch collection' to refresh the list.

Expected result: Dashboard page shows a list of the current user's projects from Supabase. Each user only sees their own data (enforced by RLS).

7

Add Stripe payments via Supabase Edge Functions

Install the Stripe plugin in WeWeb: Plugins → Extensions → Stripe → Add. Enter your Stripe Test Secret Key and Test Publishable Key. However, Stripe Checkout Sessions (for subscriptions) must be created server-side — never client-side. Create a Supabase Edge Function outside WeWeb: in your terminal, run `supabase functions new create-checkout-session`. Write the function to create a Stripe Checkout Session using the Stripe Node.js SDK, passing the user's email, a success URL, and a cancel URL. Deploy with `supabase functions deploy create-checkout-session`. Back in WeWeb, add an upgrade button with workflow: 'Supabase - Invoke Edge Function' action pointing to `create-checkout-session`, pass the user's email and desired plan. In the success branch, navigate to the Stripe checkout URL returned by the function. For webhook handling: create another Edge Function `stripe-webhook` that receives Stripe payment events and updates the `profiles.plan` column accordingly.

typescript
1// Injection point: Supabase Edge Function (outside WeWeb)
2// File: supabase/functions/create-checkout-session/index.ts
3// Deploy: supabase functions deploy create-checkout-session
4
5import { serve } from 'https://deno.land/std@0.177.0/http/server.ts';
6import Stripe from 'https://esm.sh/stripe@14.21.0?target=deno';
7import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
8
9const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY') ?? '', {
10 apiVersion: '2024-04-10',
11 httpClient: Stripe.createFetchHttpClient()
12});
13
14serve(async (req: Request) => {
15 const corsHeaders = {
16 'Access-Control-Allow-Origin': '*',
17 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type'
18 };
19
20 if (req.method === 'OPTIONS') {
21 return new Response('ok', { headers: corsHeaders });
22 }
23
24 // Verify user is authenticated
25 const authHeader = req.headers.get('Authorization');
26 if (!authHeader) {
27 return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 });
28 }
29
30 const supabase = createClient(
31 Deno.env.get('SUPABASE_URL') ?? '',
32 Deno.env.get('SUPABASE_ANON_KEY') ?? '',
33 { global: { headers: { Authorization: authHeader } } }
34 );
35
36 const { data: { user } } = await supabase.auth.getUser();
37 if (!user) {
38 return new Response(JSON.stringify({ error: 'User not found' }), { status: 401 });
39 }
40
41 const { priceId, successUrl, cancelUrl } = await req.json();
42
43 const session = await stripe.checkout.sessions.create({
44 customer_email: user.email,
45 line_items: [{ price: priceId, quantity: 1 }],
46 mode: 'subscription',
47 success_url: successUrl + '?session_id={CHECKOUT_SESSION_ID}',
48 cancel_url: cancelUrl,
49 metadata: { user_id: user.id }
50 });
51
52 return new Response(JSON.stringify({ url: session.url }), {
53 headers: { ...corsHeaders, 'Content-Type': 'application/json' }
54 });
55});

Expected result: Clicking 'Upgrade' invokes the Edge Function, which creates a Stripe Checkout Session and returns a URL. WeWeb navigates the user to that URL to complete payment.

8

Enable Supabase Realtime for live data updates

Supabase Realtime lets WeWeb react to database changes instantly without polling. First, enable Realtime on your tables: in Supabase Dashboard → Database → Replication, toggle on INSERT/UPDATE/DELETE for the tables you want. In WeWeb, go to your Supabase data source plugin config → find the tables list → enable Realtime for 'projects'. Now add a workflow in WeWeb with trigger 'On realtime - database change'. Configure it to listen to the 'projects' table for INSERT events. Add a 'Fetch collection' action to refresh the user-projects collection when a new project is inserted. For more targeted updates, use 'Change variable value' to prepend the new item to your existing collection array without refetching. This gives your SaaS app live updates — when any change happens in the database, users see it immediately.

Expected result: When a new project is created (by the current user or by an admin in Supabase Dashboard), the project list on the dashboard updates automatically without page refresh.

Complete working example

stripe-webhook.ts
1// Injection point: Supabase Edge Function (outside WeWeb)
2// File: supabase/functions/stripe-webhook/index.ts
3// Deploy: supabase functions deploy stripe-webhook
4// Configure in Stripe Dashboard → Webhooks → Add endpoint
5// Events to listen for: checkout.session.completed, customer.subscription.updated, customer.subscription.deleted
6
7import { serve } from 'https://deno.land/std@0.177.0/http/server.ts';
8import Stripe from 'https://esm.sh/stripe@14.21.0?target=deno';
9import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
10
11const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY') ?? '', {
12 apiVersion: '2024-04-10',
13 httpClient: Stripe.createFetchHttpClient()
14});
15
16serve(async (req: Request) => {
17 const body = await req.text();
18 const signature = req.headers.get('stripe-signature') ?? '';
19 const webhookSecret = Deno.env.get('STRIPE_WEBHOOK_SECRET') ?? '';
20
21 let event: Stripe.Event;
22 try {
23 event = await stripe.webhooks.constructEventAsync(body, signature, webhookSecret);
24 } catch (err) {
25 console.error('Webhook signature verification failed:', err);
26 return new Response(JSON.stringify({ error: 'Invalid signature' }), { status: 400 });
27 }
28
29 // Initialize Supabase admin client (bypasses RLS)
30 const supabase = createClient(
31 Deno.env.get('SUPABASE_URL') ?? '',
32 Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
33 );
34
35 if (event.type === 'checkout.session.completed') {
36 const session = event.data.object as Stripe.Checkout.Session;
37 const userId = session.metadata?.user_id;
38 const customerId = session.customer as string;
39
40 if (userId) {
41 await supabase
42 .from('profiles')
43 .update({
44 plan: 'pro',
45 stripe_customer_id: customerId
46 })
47 .eq('id', userId);
48 }
49 }
50
51 if (event.type === 'customer.subscription.deleted') {
52 const subscription = event.data.object as Stripe.Subscription;
53 const customerId = subscription.customer as string;
54
55 await supabase
56 .from('profiles')
57 .update({ plan: 'free' })
58 .eq('stripe_customer_id', customerId);
59 }
60
61 return new Response(JSON.stringify({ received: true }), { status: 200 });
62});

Common mistakes

Why it's a problem: Forgetting to enable RLS on Supabase tables, leaving all user data publicly accessible

How to avoid: Always run ALTER TABLE public.your_table ENABLE ROW LEVEL SECURITY; immediately after creating any table. Without RLS, anonymous users can query all rows via the Supabase API. Test RLS by checking the table in Supabase Table Editor — a yellow shield icon indicates RLS is enabled.

Why it's a problem: Creating Stripe Checkout Sessions from WeWeb workflows directly using the Stripe plugin

How to avoid: Stripe Checkout Session creation requires the Stripe Secret Key, which must never be used in client-side code. Always create Checkout Sessions in a Supabase Edge Function and return only the session URL to WeWeb. WeWeb then navigates to that URL.

Why it's a problem: Setting the OAuth redirect page to private, causing an infinite redirect loop after social login

How to avoid: The post-login redirect page must be set to Public in Page settings → Access control. The OAuth flow sets a cookie and redirects — if the destination page is private, WeWeb redirects away before the auth plugin can read the cookie. The auth plugin reads the cookie on a public page and then you can redirect to a private dashboard.

Why it's a problem: Using the Supabase service_role key in WeWeb's plugin configuration

How to avoid: Use only the anon public key in WeWeb. The service_role key bypasses all RLS policies and should never be in client-side code. The service_role key is only used in server-side Supabase Edge Functions via Deno.env.get('SUPABASE_SERVICE_ROLE_KEY').

Best practices

  • Always enable RLS on every Supabase table before connecting it to WeWeb — no exceptions, even for tables that seem non-sensitive
  • Use the subquery form of auth.uid() in RLS policies — (SELECT auth.uid()) = user_id — for better query performance on large tables
  • Create a database trigger to auto-generate a profiles row on user signup — this prevents null reference errors when WeWeb queries profile data immediately after registration
  • Handle Stripe webhooks in Supabase Edge Functions, never in WeWeb workflows — payment events must be processed server-side to be trusted
  • Use Supabase Realtime for live dashboard updates instead of polling — it is more efficient and provides true push notifications
  • Set up separate Supabase projects for production and development/staging — never use your production database for testing
  • Store all secrets (Stripe keys, API keys) in Supabase Dashboard → Project Settings → Edge Functions → Secrets, not hardcoded in Edge Function code
  • RapidDev Engineering Team recommends testing your RLS policies with the Supabase Policy Editor's built-in test tool before connecting WeWeb — this catches access control bugs before users see them

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I'm building a SaaS app with WeWeb as the frontend and Supabase as the backend. I need: 1) a multi-tenant database schema with user profiles and projects tables, 2) RLS policies so each user only sees their own data, 3) a Supabase Edge Function that creates Stripe Checkout Sessions for Pro plan upgrades, 4) a webhook handler that updates the user's plan after payment. Write the complete SQL for the schema and RLS policies, and the TypeScript Edge Function code.

WeWeb Prompt

In WeWeb, I have the Supabase plugin connected and Supabase Auth configured. I want to create a dashboard that shows the current user's projects from a Supabase 'projects' table. How do I create the collection, configure it to use RLS filtering, and bind it to a repeating container? Also show me how to add a 'Create project' workflow that inserts a row with the current user's ID.

Frequently asked questions

Can I use Supabase's free tier for a production WeWeb SaaS app?

Yes for getting started. Supabase Free includes 2 projects, 500MB database, 1GB storage, 50,000 monthly active users, and 500K Edge Function invocations per month. For serious production traffic, Supabase Pro ($25/month) provides 8GB database, 100GB storage, 100K MAU, and daily backups. Supabase Free projects pause after 1 week of inactivity, which can surprise you during development.

How does WeWeb's Supabase plugin handle authentication tokens and session refresh?

The Supabase Auth plugin in WeWeb automatically manages JWT storage and refresh. Supabase JWTs expire after 1 hour by default, but the plugin silently refreshes them using the stored refresh token. You do not need to write any token refresh code. The plugin stores tokens in the browser's localStorage and reads them on page load to restore the session.

Can I use both Supabase and Firebase in the same WeWeb project?

Technically yes, but it is not recommended. You can install the native Supabase plugin and also manually inject the Firebase SDK via Custom Code. However, managing two auth systems simultaneously causes confusion about which user session WeWeb recognizes. Choose one backend — Supabase is strongly preferred for WeWeb projects due to the native plugin integration.

What is the difference between WeWeb's Supabase 'Guided' and 'Custom' connection modes?

Guided mode uses OAuth to connect directly to your Supabase account and lists all your projects. It auto-populates the API URL and keys. Custom mode requires you to manually enter your Supabase project URL and anon key — use this if you have a self-hosted Supabase instance, if you need to connect to a shared Supabase project owned by a different account, or if you prefer not to grant OAuth access to your Supabase account.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.