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

WeWeb Security: Frontend Risks, Backend Protection, and GDPR

WeWeb generates a Vue.js SPA where all client-side code, data, and API calls are visible in the browser. Private pages and conditional visibility are UX features, not security measures. Real security lives in your backend: Supabase RLS policies, Supabase Edge Functions as API proxies for private keys, JWT validation, and HTTPS. This tutorial maps the complete threat model and the fixes for each risk.

What you'll learn

  • Why WeWeb's frontend-only architecture means all page restrictions and hidden elements are bypassable
  • How to identify and fix API key exposure in WeWeb's REST API plugin using Supabase Edge Functions as proxies
  • How Supabase RLS policies are the real security layer and how to write them correctly for your schema
  • How WeWeb handles JWT token storage and what XSS and CSRF exposure looks like in practice
  • GDPR and compliance considerations including WeWeb's data residency limitations
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Advanced16 min read60–90 minWeWeb Free plan and aboveMarch 2026RapidDev Engineering Team
TL;DR

WeWeb generates a Vue.js SPA where all client-side code, data, and API calls are visible in the browser. Private pages and conditional visibility are UX features, not security measures. Real security lives in your backend: Supabase RLS policies, Supabase Edge Functions as API proxies for private keys, JWT validation, and HTTPS. This tutorial maps the complete threat model and the fixes for each risk.

WeWeb Security: The Frontend Threat Model and Backend Mitigations

WeWeb is a frontend-only builder — it generates a Vue.js SPA that runs entirely in the user's browser. This architecture has fundamental security implications that every WeWeb developer must understand before launching a production app. Everything loaded into the browser is visible: JavaScript code, API calls, data returned from the backend, variable values. Private pages prevent the page from rendering but do not prevent direct API calls. Conditional visibility hides elements but not the data used to generate them. This tutorial walks through the complete threat model for a WeWeb application — what is actually at risk, what the browser exposes, how JWT handling works, how to proxy API calls to protect private keys, how RLS policies block unauthorized access at the data layer, and what GDPR compliance looks like for WeWeb-hosted apps.

Prerequisites

  • An existing WeWeb project with authentication and some protected data
  • A Supabase project connected to WeWeb
  • Basic understanding of what an API key and JWT token are
  • Completion of the weweb-user-authentication tutorial is helpful but not required

Step-by-step guide

1

Understand what the browser exposes in a WeWeb SPA

Open your published WeWeb app in Chrome. Press F12 to open DevTools. Go to the Network tab and reload the page. You will see every API request your app makes — including the Supabase URL, the anon key in request headers, and the data returned from your database. Go to Application tab → Local Storage — you will see the Supabase auth session token (JWT). Go to Sources tab — you can see WeWeb's compiled JavaScript, and if you examine it, you can find the configuration your app was built with. This is not a flaw unique to WeWeb — all SPAs work this way. What this means: (1) Your Supabase anon key IS visible — this is expected and fine because the anon key's access is controlled by RLS policies. (2) The data returned from API calls is visible — only request data you want the current user to be able to see. (3) Your backend API URL is visible — do not put secret logic in URL paths. (4) JavaScript logic is visible — do not put private keys or sensitive algorithms in WeWeb workflows. The anon key being visible is fine. The SERVICE_ROLE key being visible would bypass all RLS — never use it in WeWeb.

Expected result: You understand what is visible in browser DevTools for your WeWeb app, and can identify the difference between expected (anon key) and dangerous (service_role key, private API keys) exposure.

2

Identify and fix API key exposure in the REST API plugin

CRITICAL: WeWeb's REST API plugin sends all configured headers directly from the browser. If you have added a third-party API (OpenAI, Stripe, Twilio, SendGrid, a custom API) using the REST API plugin with an Authorization or API-Key header, that key is visible to anyone using your app. Open DevTools → Network tab → find requests to your third-party API → inspect the request headers. If your private API key appears there, it is exposed. The fix: never call APIs that require private keys directly from WeWeb's REST API plugin. Use a Supabase Edge Function as a server-side proxy. The Edge Function stores the private key as a Supabase Secret (Deno.env.get()) and your WeWeb app calls the Edge Function (which uses the Supabase anon key for authentication) — the private key never leaves the server. WeWeb's OpenAI plugin is an exception — it processes the key server-side through WeWeb's infrastructure, so the key is not exposed in browser requests. Always test: add the API key to the REST API plugin, open DevTools Network tab, make an API call, and check if the key appears in request headers. If it does, move the call to an Edge Function.

typescript
1// Injection point: Supabase Edge Function
2// File: supabase/functions/openai-proxy/index.ts
3// Deploy via: Supabase Dashboard → Edge Functions → Deploy
4// Then call this from WeWeb: Supabase → Invoke Edge Function
5
6import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
7
8const corsHeaders = {
9 'Access-Control-Allow-Origin': '*',
10 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
11};
12
13serve(async (req) => {
14 // Handle CORS preflight
15 if (req.method === 'OPTIONS') {
16 return new Response('ok', { headers: corsHeaders });
17 }
18
19 try {
20 const { prompt, model = 'gpt-4o-mini' } = await req.json();
21
22 // Private key lives here — never in WeWeb frontend
23 const openaiKey = Deno.env.get('OPENAI_API_KEY');
24 if (!openaiKey) throw new Error('OPENAI_API_KEY not configured');
25
26 const response = await fetch('https://api.openai.com/v1/chat/completions', {
27 method: 'POST',
28 headers: {
29 'Authorization': `Bearer ${openaiKey}`,
30 'Content-Type': 'application/json',
31 },
32 body: JSON.stringify({
33 model,
34 messages: [{ role: 'user', content: prompt }],
35 max_tokens: 500,
36 }),
37 });
38
39 const data = await response.json();
40 return new Response(JSON.stringify(data), {
41 headers: { ...corsHeaders, 'Content-Type': 'application/json' },
42 });
43
44 } catch (error) {
45 return new Response(
46 JSON.stringify({ error: error.message }),
47 { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
48 );
49 }
50});

Expected result: Third-party API calls go through Supabase Edge Functions. Private keys are stored as Supabase Secrets. Browser DevTools no longer shows private API keys in request headers.

3

Understand and configure Supabase RLS as the real security layer

OUTSIDE WEWEB — this step happens entirely in Supabase. RLS (Row Level Security) is a PostgreSQL feature that allows you to define data-access policies at the database level. When enabled on a table, every query — from any client, including direct API calls bypassing your WeWeb frontend — is checked against the policy. If the policy evaluates to false for a row, that row is not returned or modified. Enable RLS on every table that stores user or sensitive data: in Supabase Dashboard → Table Editor → select table → click 'RLS' → 'Enable Row Level Security'. When RLS is enabled with no policies, ALL operations are blocked — the table is completely locked down. Then add policies to allow authorized operations. A well-secured app has: (1) tables with no user data have no RLS needed (e.g., public product catalog), (2) tables with user data have RLS enabled + policies that check auth.uid(), (3) tables with admin-only data have RLS + role-check policies. The minimum RLS test: enable RLS on a test table, make a call from WeWeb without being logged in, confirm you get 0 rows (not an error). Then log in as a user, confirm you see only your rows. Then try as admin, confirm you see all rows.

typescript
1-- Injection point: Supabase SQL editor (run OUTSIDE WeWeb)
2-- Security audit: check which tables have RLS disabled
3-- Run this query to find tables without RLS enabled
4
5SELECT
6 schemaname,
7 tablename,
8 rowsecurity as rls_enabled,
9 CASE WHEN rowsecurity THEN 'SECURE'
10 ELSE 'WARNING: RLS DISABLED'
11 END as security_status
12FROM pg_tables
13WHERE schemaname = 'public'
14ORDER BY rowsecurity ASC, tablename;
15
16-- List all existing RLS policies:
17SELECT
18 schemaname,
19 tablename,
20 policyname,
21 cmd as operation,
22 qual as using_expression,
23 with_check as with_check_expression
24FROM pg_policies
25WHERE schemaname = 'public'
26ORDER BY tablename, cmd;
27
28-- Quick fix: enable RLS on all public tables that are missing it
29-- CAUTION: This locks down all access add policies immediately after
30-- ALTER TABLE public.your_table ENABLE ROW LEVEL SECURITY;

Expected result: You can query which tables have RLS enabled and audit your existing policies. Every table with user or sensitive data has RLS enabled and appropriate policies.

4

Understand JWT handling and session security in WeWeb

WeWeb's authentication plugins (Supabase Auth, Auth0) manage JWT tokens automatically. When a user logs in, the JWT (access token) is stored in localStorage by the Supabase client SDK. This is a deliberate choice — localStorage persists across browser sessions but is accessible to JavaScript code running on the page. The security trade-off: JWTs in localStorage are vulnerable to XSS attacks (malicious JavaScript injected into the page can read localStorage). WeWeb applications benefit from Vue.js's built-in XSS protections — Vue's template compiler escapes all dynamic values by default, preventing script injection via bound text content. However, custom JavaScript in WeWeb workflows runs without Vue's protections — never use `innerHTML` with user-supplied content in Custom JavaScript actions. For CSRF: WeWeb's Supabase calls use the JWT in the Authorization header rather than cookies, making traditional CSRF attacks (which require session cookies) ineffective against API calls. Token expiry: Supabase access tokens expire after 1 hour by default. The SDK auto-refreshes using the refresh token (7-day expiry). You do not need to handle token refresh manually.

Expected result: You understand how JWTs are stored and the XSS/CSRF implications. Your Custom JavaScript workflow actions do not use innerHTML with dynamic user data.

5

Implement cookie consent and privacy controls

WeWeb has a built-in Privacy & Consent feature. Go to Project settings → Privacy & Consent. Two consent modes: Explicit (opt-in — users must actively accept before any tracking scripts run) and Implicit (opt-out — scripts run by default, users can opt out). Enable the privacy settings → configure which scripts require consent (analytics, marketing, essential). WeWeb's implementation is basic — for GDPR compliance in European markets or complex regulatory requirements (CCPA, LGPD), use a third-party Consent Management Platform. Recommended options: CookieYes (has a free tier, easy to integrate) or Osano. Integration: add the CookieYes script to App Settings → Custom Code → Head. Configure CookieYes to block your analytics scripts until consent is given. The GTM plugin can be configured to fire tags only after consent signals from CookieYes. One important data residency note: WeWeb's WeWeb Auth plugin stores user data on AWS infrastructure in the United States. If your users are EU-based and you use WeWeb Auth, this creates GDPR data residency concerns. Supabase Auth is hosted in your chosen region (including EU regions). Choose your auth provider accordingly.

Expected result: Your app shows a cookie consent banner. Analytics scripts do not fire until user consent is given. You have documented your data processing activities for GDPR purposes.

6

Secure Supabase Edge Functions with auth checks

When WeWeb calls a Supabase Edge Function via 'Supabase → Invoke Edge Function', the user's Supabase JWT is automatically included in the request. Inside the Edge Function, verify this JWT before processing the request — do not assume all callers are authenticated. Extract the Authorization header, verify it with Supabase's JWT library, and check the user's identity and roles before executing any logic. This prevents unauthenticated callers (or users with insufficient roles) from triggering server-side operations. The Edge Function has access to the Supabase service_role key via `Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')` — it can perform admin-level database operations. This is the correct place to use the service_role key (server-side only, never in WeWeb frontend).

typescript
1// Injection point: Supabase Edge Function
2// File: supabase/functions/admin-action/index.ts
3// This pattern validates the caller's JWT before performing admin operations
4
5import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
6import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
7
8const corsHeaders = {
9 'Access-Control-Allow-Origin': '*',
10 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
11};
12
13serve(async (req) => {
14 if (req.method === 'OPTIONS') {
15 return new Response('ok', { headers: corsHeaders });
16 }
17
18 try {
19 // Verify the caller's JWT
20 const authHeader = req.headers.get('Authorization');
21 if (!authHeader) {
22 return new Response(JSON.stringify({ error: 'Unauthorized' }), {
23 status: 401,
24 headers: corsHeaders,
25 });
26 }
27
28 const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
29 const supabaseKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
30 const anonKey = Deno.env.get('SUPABASE_ANON_KEY')!;
31
32 // Verify user JWT using anon client
33 const userClient = createClient(supabaseUrl, anonKey, {
34 global: { headers: { Authorization: authHeader } },
35 });
36
37 const { data: { user }, error } = await userClient.auth.getUser();
38 if (error || !user) {
39 return new Response(JSON.stringify({ error: 'Invalid token' }), {
40 status: 401, headers: corsHeaders,
41 });
42 }
43
44 // Check admin role
45 const adminClient = createClient(supabaseUrl, supabaseKey);
46 const { data: userRoles } = await adminClient
47 .from('users_roles')
48 .select('roles!inner(name)')
49 .eq('user_id', user.id);
50
51 const isAdmin = userRoles?.some(r => r.roles?.name === 'admin');
52 if (!isAdmin) {
53 return new Response(JSON.stringify({ error: 'Forbidden' }), {
54 status: 403, headers: corsHeaders,
55 });
56 }
57
58 // Perform admin operation here
59 const body = await req.json();
60 // ... your admin logic ...
61
62 return new Response(JSON.stringify({ success: true }), {
63 headers: { ...corsHeaders, 'Content-Type': 'application/json' },
64 });
65
66 } catch (error) {
67 return new Response(JSON.stringify({ error: error.message }), {
68 status: 500, headers: corsHeaders,
69 });
70 }
71});

Expected result: Edge Functions verify the caller's JWT and role before executing sensitive operations. Unauthenticated or unauthorized API calls return 401 or 403 responses.

7

Perform a security audit before launch

Before going live, run through this security checklist systematically. In WeWeb: confirm no private API keys are in the REST API plugin headers (check Network tab in DevTools). Check that all pages intended to be private have access control configured AND backend RLS. Verify that conditional rendering hiding sensitive UI does not expose the data via collections that fetch it regardless. In Supabase: run the RLS audit SQL query (from Step 3) to confirm all user-data tables have RLS enabled. Test as an unauthenticated user — try direct Supabase API calls with the anon key but no JWT. Test as a low-privilege user — confirm they cannot read admin data. In your app: confirm logout actually clears the session and redirects correctly. Check that error messages do not leak sensitive information (e.g., 'User not found' vs 'Invalid credentials'). Check that form inputs that accept user content are not used in innerHTML-equivalent bindings. For compliance: document what user data you collect and store, where it is stored (Supabase region), and your data retention policy. This is minimum GDPR due diligence.

Expected result: A documented pre-launch security checklist has been completed. All critical vulnerabilities are remediated before the app is publicly accessible.

Complete working example

security-audit.sql
1-- Injection point: Supabase SQL editor (run OUTSIDE WeWeb)
2-- Comprehensive security audit queries for WeWeb + Supabase apps
3
4-- 1. Tables without RLS (security risk if they contain user data)
5SELECT tablename, 'RLS DISABLED - RISK' as status
6FROM pg_tables
7WHERE schemaname = 'public'
8 AND rowsecurity = false
9ORDER BY tablename;
10
11-- 2. Tables with RLS but NO policies (completely locked - no access at all)
12SELECT t.tablename, 'RLS ENABLED - NO POLICIES' as status
13FROM pg_tables t
14LEFT JOIN pg_policies p ON t.tablename = p.tablename
15 AND t.schemaname = p.schemaname
16WHERE t.schemaname = 'public'
17 AND t.rowsecurity = true
18 AND p.policyname IS NULL
19ORDER BY t.tablename;
20
21-- 3. Policies that allow unrestricted access (potential over-permissive policy)
22SELECT tablename, policyname, cmd,
23 CASE WHEN qual = 'true' OR qual IS NULL
24 THEN 'WARNING: unrestricted access'
25 ELSE 'OK'
26 END as risk_level
27FROM pg_policies
28WHERE schemaname = 'public'
29ORDER BY tablename;
30
31-- 4. Check if any table has a policy allowing anonymous (unauthenticated) access
32-- (Sometimes needed for public data, but flag for review)
33SELECT tablename, policyname, cmd, qual
34FROM pg_policies
35WHERE schemaname = 'public'
36 AND qual NOT LIKE '%auth.uid()%'
37 AND qual NOT LIKE '%auth.role()%'
38 AND qual != 'false'
39ORDER BY tablename;
40
41-- 5. Users with admin role (verify the list is correct)
42SELECT u.email, u.created_at, r.name as role
43FROM auth.users u
44JOIN public.users_roles ur ON u.id = ur.user_id
45JOIN public.roles r ON ur.role_id = r.id
46WHERE r.name = 'admin'
47ORDER BY u.created_at;

Common mistakes

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

How to avoid: The service_role key bypasses all RLS policies — anyone who sees it (visible in Network tab) can make unrestricted database queries. The WeWeb Supabase plugin should always use the anon key. The service_role key belongs only in server-side code (Supabase Edge Functions), never in any frontend configuration. Check your Supabase plugin settings and verify you are using a key that starts with 'eyJ' with 'anon' encoded, not 'service_role'.

Why it's a problem: Assuming 'Private page' in WeWeb means the data is secure

How to avoid: Private pages prevent the WeWeb page component from rendering, but the Supabase collections that page uses can still be queried directly via the Supabase REST API with the anon key. The only thing preventing unauthorized data access is RLS. Enable RLS on all tables with sensitive data and verify policies work by testing direct API calls via browser DevTools.

Why it's a problem: Adding OpenAI, Stripe, or other private API keys directly in WeWeb's REST API plugin request headers

How to avoid: Any header in a REST API plugin collection is visible in browser DevTools. Create a Supabase Edge Function that makes the API call server-side. Store the private key as a Supabase Edge Function secret (Deno.env.get()). WeWeb calls the Edge Function, which calls the third-party API. The private key never touches the browser.

Why it's a problem: Not enabling CORS restrictions on Supabase Edge Functions, allowing any origin to call them

How to avoid: Add origin validation to your Edge Functions' CORS headers. Instead of 'Access-Control-Allow-Origin': '*', restrict to your WeWeb app's domain: 'Access-Control-Allow-Origin': 'https://yourdomain.com'. This prevents other websites from calling your Edge Functions using a stolen user's JWT. Also implement JWT verification (see Step 6) so even if a call reaches the function, it validates the user.

Best practices

  • Treat WeWeb as the presentation layer only — all security enforcement must happen in your backend (Supabase RLS, Edge Functions, API gateway)
  • Run a security audit query before launch to identify tables without RLS enabled
  • Never use the Supabase service_role key in WeWeb — only the anon key; use service_role only in server-side Edge Functions
  • Proxy all third-party API calls requiring private keys through Supabase Edge Functions — private keys must never appear in WeWeb workflow configurations
  • Add JWT verification at the top of every Supabase Edge Function that performs sensitive operations
  • Test your security by impersonating attacks: make direct Supabase API calls from the browser console without authentication and confirm they are blocked
  • Choose Supabase Auth over WeWeb Auth for GDPR compliance — Supabase supports EU data residency while WeWeb Auth stores data in the US
  • Document your threat model and mitigation decisions — this is required for any app handling personal data under GDPR

Still stuck?

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

ChatGPT Prompt

I'm building a production app in WeWeb (Vue.js SPA) with Supabase as the backend. I need to understand my security risks and mitigations. My app has: authenticated users with roles (admin/member), a REST API call to OpenAI that currently uses the API key in the request header, and Supabase tables for user profiles and orders. What are my critical security vulnerabilities, and what specific changes do I need to make to the Supabase RLS policies and API call architecture to secure this before launch?

WeWeb Prompt

In my WeWeb app, I'm using the REST API plugin to call the OpenAI API with my API key in the Authorization header. I know this is exposed in the browser. Help me: (1) create a Supabase Edge Function that proxies the OpenAI call with the key stored as a Supabase Secret, (2) add JWT verification in the Edge Function so only logged-in users can call it, (3) configure the WeWeb workflow to call the Edge Function using 'Supabase → Invoke Edge Function' instead of the direct REST API call.

Frequently asked questions

Is the Supabase anon key safe to expose in a WeWeb app?

Yes — this is by design. The Supabase anon key is a public key intended to be visible in client-side code. Its capabilities are limited by your RLS policies. The anon key cannot bypass RLS. What is NOT safe to expose is the service_role key, which bypasses all RLS. Think of the anon key as a 'guest badge' that only grants access to rooms (tables) where you have explicitly left the door open via an RLS policy. The service_role key is a 'master key' — keep it exclusively in server-side Edge Functions.

Does WeWeb have SOC 2 or ISO 27001 certification?

As of March 2026, WeWeb does not hold SOC 2 Type II or ISO 27001 certifications. WeWeb has confirmed they are working toward these certifications but no public timeline has been announced. For enterprise deployments with strict compliance requirements, this may be a limiting factor. Supabase does hold SOC 2 Type II certification, which covers the data layer. For the frontend (WeWeb), assess your organization's risk tolerance for using a non-certified SaaS platform as the application layer.

Can a user with a stolen JWT token access another user's data in my WeWeb app?

If your RLS policies are correct, no. Supabase RLS policies use auth.uid() to identify the current user from the JWT. A stolen JWT token can only access what that user's account is authorized to access — the same as if the legitimate user were making the request. An attacker with a stolen JWT can impersonate that specific user but cannot access other users' data. The risk is account takeover for the compromised user, not a systemic data breach. Mitigate by using short JWT expiry times and implementing logout-on-suspicious-activity.

How do I make my WeWeb app GDPR compliant if my users are in the EU?

Key GDPR requirements for WeWeb apps: (1) Host Supabase in an EU region (Frankfurt, EU West) — configure this when creating the Supabase project. Do not use WeWeb Auth if EU residency is required (it stores data in the US). (2) Use Supabase Auth or Auth0 EU region hosting. (3) Add a cookie consent banner for analytics scripts — use WeWeb's built-in privacy settings or a CMP like CookieYes. (4) Create a privacy policy documenting what data you collect, why, and for how long. (5) Implement data deletion — add a 'Delete my account' workflow that calls Supabase to delete the user's auth record and all their data. (6) Do not share user data with third parties without consent. RapidDev can help assess and implement GDPR compliance for complex WeWeb applications.

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.