Arkitekturöversikt
Alla tenants delar samma Supabase-databas. Isolering upprätthålls av Row-Level Security (RLS)-policyer, PostgreSQL-nivåregler som filtrerar varje fråga baserat på den autentiserade användarens JWT-anspråk.
När en användare loggar in innehåller deras JWT ett workspace_id-anspråk. Varje RLS-policy kontrollerar detta anspråk mot en workspace_id-kolumn i tabellen. Användare kan bokstavligen inte fråga data från andra arbetsplatser, databasen upprätthåller det, inte applikationskoden.
Detta mönster är den industristandard som bolag som Klarna och Spotify använder i sina interna verktyg, och med Supabase kan du implementera det på en bråkdel av tiden och kostnaden.
Sätta upp workspace-strukturen
Tabeller du behöver:
1. workspaces (id, name, plan, created_at) 2. workspace_members (id, workspace_id, user_id, role, created_at) 3. Alla dina affärstabeller (med workspace_id-kolumn)
När en användare registrerar sig skapar du en arbetsplats och en workspace_members-post i en Supabase databasfunktion. workspace_id läggs in i användarens JWT via en anpassad claim-hook.
För svenska B2B SaaS-bolag rekommenderar vi att även lagra organisationsnummer (org_nr) i workspaces-tabellen. Det förenklar faktureringen och möjliggör automatisk berikande av kunddata via Bolagsverkets API.
Skriva RLS-policyer
För varje tenant-scopad tabell aktiverar du RLS och lägger till dessa policyer:
SELECT-policy: CREATE POLICY "users can read own workspace data" ON your_table FOR SELECT USING (workspace_id = (auth.jwt() ->> 'workspace_id')::bigint);
INSERT-policy: CREATE POLICY "users can insert to own workspace" ON your_table FOR INSERT WITH CHECK (workspace_id = (auth.jwt() ->> 'workspace_id')::bigint);
Detta upprätthåller isolering på databasnivå, oavsett vad din frontend eller API gör. En felaktig WeWeb-bindning eller en bugg i Xano kan aldrig läcka data mellan kunder, databasen är det sista försvarslinjen.
Rollbaserad åtkomstkontroll
Inom en arbetsplats har olika användare olika behörigheter (owner, admin, member). Lagra detta i workspace_members.role.
För data-nivå RBAC, använd Supabase-funktioner i dina RLS-policyer:
CREATE FUNCTION get_user_role() RETURNS text AS $$ SELECT role FROM workspace_members WHERE user_id = auth.uid() AND workspace_id = (auth.jwt() ->> 'workspace_id')::bigint $$ LANGUAGE sql STABLE;
Sedan i policyer: USING (get_user_role() IN ('admin', 'owner'))
För svenska enterprise-kunder är det vanligt att också vilja ha avdelningsbaserad åtkomst (t.ex. "bara HR kan se lönedata"). Detta byggs enkelt som ytterligare en roll eller en separat permissions-tabell.
Koppla ihop det i WeWeb
I WeWeb konfigurerar du din Supabase-datakälla med den autentiserade användarens token. WeWeb skickar JWT automatiskt med varje begäran.
För din apps navigation och UI, läs rollen från en global variabel (populerad från workspace_members-tabellen vid inloggning). Visa/dölj menyobjekt, knappar och hela sektioner baserat på roll, detta är enbart presentation, den verkliga säkerheten finns i RLS.
Aldrig hårdkoda tenant-ID:n i WeWeb. All datafiltrering ska komma från den autentiserade användarens JWT, Supabase hanterar resten. Det gör din WeWeb-app trygg att distribuera även till tekniskt kunniga kunder som inspekterar nätverkstrafik.
Att skriva RLS-policyer som faktiskt fungerar: vanliga misstag
Det vanligaste RLS-misstaget: skriva policyn men glömma att aktivera RLS på tabellen. I Supabase måste du köra ALTER TABLE your_table ENABLE ROW LEVEL SECURITY;, utan detta skapas policyerna men gör ingenting. Verifiera att varje tabell har RLS aktiverat i Supabase-dashboarden under Table Editor > Policies.
Det andra vanliga misstaget: saknad WITH CHECK-klausul på INSERT- och UPDATE-policyer. USING-klausulen filtrerar vad användare kan läsa. WITH CHECK-klausulen validerar vad de kan skriva. Om du skriver en policy med enbart USING och utelämnar WITH CHECK på en UPDATE-policy kan användare potentiellt uppdatera rader så att de pekar på ett annat workspace_id, en tyst kringgång av tenant-isolering. Skriv alltid båda klausulerna.
Det tredje misstaget: exponering av service role-nyckeln. Supabase har två nycklar: anon-nyckeln (respekterar RLS) och service role-nyckeln (kringgår RLS helt). Exponera aldrig service role-nyckeln i din WeWeb-frontend eller i någon klientsidig kod. Den hör hemma enbart i serversidig kod (Xano, Edge Functions) för operationer som legitimt behöver kringgå RLS, som admin-uppgifter eller webhook-bearbetning.
Tenant-onboardingflöde: den kompletta implementationen
Ett rent tenant-onboardingflöde är avgörande, det sätter upp arbetsplatsen korrekt och grindar in användaren i sin scopade upplevelse. Här är det kompletta flödet vi implementerar i varje multi-tenant WeWeb-projekt.
Steg 1: Användaren klickar på "Registrera dig" och skickar in e-post + lösenord. Supabase Auth skapar användarkontot. Steg 2: En Supabase-databastrigger (AFTER INSERT ON auth.users) utlöser en funktion som: skapar en workspaces-rad med en standardplan, skapar en workspace_members-rad som länkar användaren som ägare, och sätter workspace_id-anspråket i JWT via Supabases auth-hook. Steg 3: Användaren omdirigeras till en onboardingsida i WeWeb där de sätter sitt arbetsplatsnamn, bjuder in teammedlemmar och slutför konfigurationen. Steg 4: Alla efterföljande sidladdningar använder en JWT som innehåller workspace_id, varje Supabase-fråga blir automatiskt tenant-scopad.
För inbjudna användare (inte den som skapade arbetsplatsen) skiljer sig flödet: ägaren genererar en inbjudningstoken (lagrad i en workspace_invitations-tabell), den inbjudne klickar på länken och skapar ett konto, och en databastrigger skapar deras workspace_members-post med den inbjudna rollen. Vi låter alltid inbjudningstoken gå ut efter 72 timmar och validerar att de inte redan använts innan vi bearbetar dem.
Anpassade domäner per tenant
Anpassade domäner är en premiumfunktion i många SaaS-produkter, som låter enterprise-kunder komma åt appen på app.kundforetag.se istället för app.dinsaas.se. Att implementera detta i en WeWeb + Supabase-stack kräver några samordnade delar.
På infrastruktursidan kan WeWeb distribueras till Vercel eller Netlify, båda stöder anpassade domäner via sina API:er. När en kund lägger till en anpassad domän anropar din Xano-backend Vercel API:et för att registrera domänen och provisionera ett SSL-certifikat. Kunden lägger sedan till en CNAME-post som pekar deras subdomän mot din Vercel-driftsättning.
På autentiseringssidan behöver Supabase Auth konfigureras för att tillåta den anpassade domänen som en omdirigerings-URL efter inloggning. Lägg till kundens domän i Supabases lista över tillåtna omdirigerings-URL:er via Supabase Management API (återigen utlöst från Xano). WeWebs auth-omdirigerings-URL bör läsa det aktuella värdnamnet dynamiskt istället för att ha en hårdkodad callback-URL, använd JavaScript för att bestämma den aktuella ursprungsadressen vid körning.
För själva WeWeb-appen, detektera den anpassade domänen vid laddning och slå upp motsvarande workspace_id i en custom_domains-tabell. Detta workspace_id används för att scopa inloggningssidan och allt publikt innehåll innan autentisering. I våra projekt har vi implementerat anpassade domäner för två enterprise-kunder, den totala implementationstiden är 2-3 dagar av WeWeb- och Xano-konfiguration. Behöver du hjälp? Våra WeWeb-utvecklare är specialiserade på multi-tenant SaaS-arkitektur.
Faktureringsisolering: koppla Stripe till din tenant-modell
Varje tenant måste ha exakt en Stripe-kundpost, kopplad till sin arbetsplats. Skapa Stripe-kunden när arbetsplatsen skapas (i din Xano onboarding-webhook eller Supabase-databastrigger) och lagra stripe_customer_id på workspaces-tabellen. Denna koppling är grunden för alla faktureringsoperationer.
För prenumerationshantering, skapa en workspace_subscriptions-tabell: workspace_id, stripe_subscription_id, plan (free/pro/enterprise), status (trialing/active/past_due/cancelled), current_period_end. Håll denna tabell uppdaterad via Stripe-webhooks i Xano. Dina RLS-policyer kan referera till denna tabell, till exempel begränsa vissa funktioner till arbetsplatser där status = 'active'.
En viktig isoleringsfråga: Stripe-webhooks bearbetas serversidigt i Xano, men prenumerationsdatan behöver återspeglas i UI:t. Vi använder Supabase Realtime för att pusha prenumerationsstatusändringar till den aktiva WeWeb-sessionen. När Xano bearbetar en lyckad betalningswebhook och uppdaterar workspace_subscriptions, tar WeWeb-frontenden emot Realtime-ändringshändelsen och uppdaterar UI:t utan att kräva en sidomladdning. Detta ger användare omedelbar feedback när deras betalning bearbetas, kritiskt för uppgraderingsflöden där användaren väntar på skärmen.