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
Connect the Supabase data source plugin
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.
Design the database schema in Supabase
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.
1-- Injection point: Supabase Dashboard → SQL Editor2-- Run this SQL to create the profiles and projects tables34-- 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 NULL12);1314-- Enable RLS immediately15ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;1617-- 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 NULL25);2627ALTER TABLE public.projects ENABLE ROW LEVEL SECURITY;2829-- 3. Auto-create profile on user signup (trigger)30CREATE OR REPLACE FUNCTION public.handle_new_user()31RETURNS trigger AS $$32BEGIN33 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;4243CREATE TRIGGER on_auth_user_created44 AFTER INSERT ON auth.users45 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.
Write Row-Level Security policies for multi-tenant data isolation
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.
1-- Injection point: Supabase Dashboard → SQL Editor2-- RLS policies for profiles table34CREATE POLICY "Users can view own profile"5 ON public.profiles FOR SELECT6 USING ((SELECT auth.uid()) = id);78CREATE POLICY "Users can update own profile"9 ON public.profiles FOR UPDATE10 USING ((SELECT auth.uid()) = id)11 WITH CHECK ((SELECT auth.uid()) = id);1213-- RLS policies for projects table1415CREATE POLICY "Users can view own projects"16 ON public.projects FOR SELECT17 USING ((SELECT auth.uid()) = user_id);1819CREATE POLICY "Users can create own projects"20 ON public.projects FOR INSERT21 WITH CHECK ((SELECT auth.uid()) = user_id);2223CREATE POLICY "Users can update own projects"24 ON public.projects FOR UPDATE25 USING ((SELECT auth.uid()) = user_id)26 WITH CHECK ((SELECT auth.uid()) = user_id);2728CREATE POLICY "Users can delete own projects"29 ON public.projects FOR DELETE30 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.
Configure Supabase Auth in WeWeb
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.
Build the signup and login pages
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.
Create Supabase collections for data display
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).
Add Stripe payments via Supabase Edge Functions
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.
1// Injection point: Supabase Edge Function (outside WeWeb)2// File: supabase/functions/create-checkout-session/index.ts3// Deploy: supabase functions deploy create-checkout-session45import { 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';89const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY') ?? '', {10 apiVersion: '2024-04-10',11 httpClient: Stripe.createFetchHttpClient()12});1314serve(async (req: Request) => {15 const corsHeaders = {16 'Access-Control-Allow-Origin': '*',17 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type'18 };1920 if (req.method === 'OPTIONS') {21 return new Response('ok', { headers: corsHeaders });22 }2324 // Verify user is authenticated25 const authHeader = req.headers.get('Authorization');26 if (!authHeader) {27 return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 });28 }2930 const supabase = createClient(31 Deno.env.get('SUPABASE_URL') ?? '',32 Deno.env.get('SUPABASE_ANON_KEY') ?? '',33 { global: { headers: { Authorization: authHeader } } }34 );3536 const { data: { user } } = await supabase.auth.getUser();37 if (!user) {38 return new Response(JSON.stringify({ error: 'User not found' }), { status: 401 });39 }4041 const { priceId, successUrl, cancelUrl } = await req.json();4243 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 });5152 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.
Enable Supabase Realtime for live data updates
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
1// Injection point: Supabase Edge Function (outside WeWeb)2// File: supabase/functions/stripe-webhook/index.ts3// Deploy: supabase functions deploy stripe-webhook4// Configure in Stripe Dashboard → Webhooks → Add endpoint5// Events to listen for: checkout.session.completed, customer.subscription.updated, customer.subscription.deleted67import { 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';1011const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY') ?? '', {12 apiVersion: '2024-04-10',13 httpClient: Stripe.createFetchHttpClient()14});1516serve(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') ?? '';2021 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 }2829 // 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 );3435 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;3940 if (userId) {41 await supabase42 .from('profiles')43 .update({44 plan: 'pro',45 stripe_customer_id: customerId46 })47 .eq('id', userId);48 }49 }5051 if (event.type === 'customer.subscription.deleted') {52 const subscription = event.data.object as Stripe.Subscription;53 const customerId = subscription.customer as string;5455 await supabase56 .from('profiles')57 .update({ plan: 'free' })58 .eq('stripe_customer_id', customerId);59 }6061 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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation