Supabase with a static frontend
Connect an Astro or Vite static site to Supabase without shipping a server key. Covers Row Level Security, env variables and auth redirect URLs.
Before you start
- A Supabase project with at least one table you want to read from the browser
- Node.js and npm installed locally
- An Astro or Vite project that builds with npm run build
- An account on Netlify, Vercel or Cloudflare Pages with the repository connected
This guide shows how a static site built with Astro or Vite can read and write Supabase data straight from the browser. Every step follows the official Supabase, Astro, Vite and hosting documentation linked in the text. We did not run these steps end to end for this guide, so the commands are untested until you run them on your own project. Where a step happens in a dashboard, in DNS or in a deploy, we say so.
What the browser is allowed to hold
A static site sends its JavaScript to every visitor's device, and anything in that JavaScript can be read. Supabase splits its keys with this in mind. The publishable key is meant for code that runs on a user's device. The Supabase API keys page describes it like this: "Anyone can read it, so it only reaches what Row Level Security allows" (Supabase API keys).
The secret key is a different thing. It bypasses Row Level Security, so the same page warns: "Never put one in a browser, a shipped application, or source control." Supabase also blocks secret keys sent from a browser. The page says it matches on the User-Agent header and returns HTTP 401 Unauthorized.
Older projects may still use the legacy anon and service_role keys. Supabase says it is deprecating them by the end of 2026 and recommends the publishable and secret keys instead. The same page notes that a long key starting with eyJ was written for the legacy keys. If your tutorial or AI assistant tells you to copy one of those into browser code, use the publishable key from the Connect dialog instead.
Rule of thumb for this guide: the browser gets the project URL and the publishable key. Nothing else.
Step 1: Turn on Row Level Security for every exposed table
Supabase's Row Level Security page gives the rule: "Enable RLS on every table in an exposed schema." It also states: "Once RLS is enabled, no data is accessible through the API when using a publishable key, until you create policies" (Supabase Row Level Security). That means a new table is locked until you add grants and policies, which is what you want.
Run this in the SQL editor for a table called notes, then repeat it for each table your site reads:
alter table public.notes enable row level security;
revoke all on table public.notes from anon, authenticated;
grant select on table public.notes to authenticated;
create policy notes_read_signed_in on public.notes for select to authenticated using (true);
The policy above is an example only. It lets every signed-in user read every row. Replace using (true) with a rule that matches your data before you launch. The same page warns that a policy reading to anon with using (true) gives every unauthenticated visitor read access. It also notes that views bypass RLS by default, because they are usually created with the postgres user. If you expose a view, check it separately.
We did not run this step for this guide.
Step 2: Create the environment file with the right prefix
Build tools decide which variables reach the browser by name. Astro's documentation says: "only environment variables prefixed with PUBLIC_ are available in client-side code for security purposes" (Astro environment variables). Vite's documentation says: "VITE_* variables should not contain sensitive information such as API keys" (Vite environment variables). Both rules point the same way. Only the URL and the publishable key get a public prefix.
Open your project's Connect dialog in Supabase. It shows the URL and publishable key for the framework you select, ready to paste. Put them in a local .env file, using a sample value for each item in this guide:
PUBLIC_SUPABASE_URL=<your project URL from the Connect dialog>
PUBLIC_SUPABASE_PUBLISHABLE_KEY=<your publishable key>
For a Vite project, use VITE_SUPABASE_URL and VITE_SUPABASE_PUBLISHABLE_KEY instead. Never add a secret key to this file. A secret key belongs only in the secret store of a server function, which is covered in the serverless host guide.
Check that .env is listed in .gitignore before you commit. The file is for local builds, and each host has its own place for the same values (Step 4).
Step 3: Install the client and create it once
Supabase's JavaScript reference says you can install the library from the terminal with npm. Then create one client and import it wherever you need it (supabase-js reference):
npm install @supabase/supabase-js
Create a small module, for example src/lib/supabase.js in an Astro project:
import { createClient } from '@supabase/supabase-js';
export const supabase = createClient(
import.meta.env.PUBLIC_SUPABASE_URL,
import.meta.env.PUBLIC_SUPABASE_PUBLISHABLE_KEY
);
In a Vite project, read import.meta.env.VITE_SUPABASE_URL and import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY instead. Then query a table you secured in Step 1:
const { data, error } = await supabase.from('notes').select('*');
if (error) console.error(error.message);
The supabase-js library returns a data object for success and an error object for failures, so check error before you use data. Keep the call inside your page script, not inside a server-only file.
We did not run this step for this guide.
Step 4: Set the same variables on your host
Astro documents that its variables are statically replaced at build time, and Vite says its values are bundled into the source at build time. In practice this means the host must have the variables when the build runs. Change a value later and you need a new build for the change to reach visitors.
Each host has its own place to set them:
- Netlify: its environment variable overview describes scopes. A variable set with the Builds scope is available during the build, which is what a static site needs. The overview is at Netlify environment variables. Use the Netlify UI to add the two values, then trigger a deploy.
- Vercel: variables are set per environment, and the page describes Production and Preview scopes. Preview variables apply to deployments from any Git branch that is not the production branch. The Vercel page also says: "Your source code can read these values to change behavior during the Build Step or during Function execution" (Vercel environment variables).
- Cloudflare Pages: go to Workers & Pages, select the project, then Settings > Environment variables. Cloudflare also injects system variables such as CF_PAGES_URL during the build (Cloudflare Pages build configuration).
For the build command, use the framework default. The Cloudflare page lists Astro with the output directory dist, and Vite projects such as React (Vite) with dist as well.
We did not run this step for this guide.
Step 5: Set the auth redirect URLs
If your site signs users in, Supabase Auth sends them back to a URL you allow. The Supabase redirect URL page says: "The Site URL in URL Configuration defines the default redirect URL when no redirectTo is specified in the code" (Supabase redirect URLs). Set the Site URL to your production address, not localhost.
The same page says the allow list supports wildcards for preview deployments from Netlify and Vercel. It also recommends setting the exact production redirect path for the site URL. A safe pattern is to use the exact production URL plus a wildcard entry only for previews you trust. Add localhost entries for development as well.
Email confirmation and magic links use these URLs too. If your app passes redirectTo in code, make sure the same URL is on the allow list.
We did not run this step for this guide. Changing the Site URL and the allow list happens in the Supabase dashboard.
Step 6: Build and check the output
Run the build locally and look at what it produced:
npm run build
grep -r "sb_secret_" dist
The first command builds the site. The second searches the output for the prefix of a Supabase secret key. It should print nothing. If it prints a file name, a secret key ended up in browser code. Remove it from the source, rotate the key in the Supabase dashboard, and build again. Supabase's API keys page says a leaked secret key should be rotated, so do not only delete the line.
Then deploy to your host and open the site. In the browser developer tools, check the Network tab. Requests to Supabase should carry your publishable key, and the query should return only the rows your policies allow. Try the same query while signed out to confirm the policy behaves as you intended.
We did not run this step for this guide. The commands are standard but untested on your project.
What to watch
Keep the table grants and the policies in step. Supabase's RLS page says Postgres checks grants first and then policies, so a table can be blocked for one reason or the other. Re-read the policies when you add a column that holds personal data.
Keep the Site URL and the allow list in step with your domain. If you move the site, update the Site URL and the redirect entries the same day, or sign-in will send users to the old address.
Keep a separate secret key for any server code, and never import it into a file that the build bundles for the browser. The Supabase page recommends a separate secret key for each backend component, so one leak only means one rotation.
Checklist before you launch
- Every exposed table has RLS enabled and matching grants and policies.
- Only the publishable key and project URL use a public prefix (PUBLIC_ for Astro, VITE_ for Vite).
- grep for sb_secret_ in the build output prints nothing.
- Each host has the variables set for the build, and a new build ran after the last change.
- The Site URL is your production address, and the redirect allow list has the exact paths you use.
- A signed-out visitor sees only the rows you meant to publish.
Expected result: The built site loads in the browser, its network requests to Supabase use the publishable key, no secret key appears in the built files, and a query to a protected table returns only the rows your policies allow.
If something goes wrong
| Problem | Fix |
|---|---|
| Browser requests to Supabase return HTTP 401 Unauthorized | A secret key is being used in browser code. Switch the browser client to the publishable key and keep the secret key on a server only.supabase.com |
| Table query returns no rows after RLS was turned on | Enabling RLS blocks everything until policies exist. Add a select policy for the role your app uses, and grant the matching table privileges.supabase.com |
| Client variable is undefined in the browser | Astro only exposes variables that start with PUBLIC_ to client code. Vite uses the VITE_ prefix. Rename the variable, then build again.docs.astro.build |
| Login redirects to localhost or to the wrong domain after deploy | Set the Site URL to your production URL in the Supabase URL Configuration, and add the exact redirect paths your app uses.supabase.com |
| Built site has an empty Supabase URL or key | The variables were not set for the build. Add them to the host with the Builds scope (Netlify) or the matching environment (Vercel, Cloudflare Pages), then trigger a new build.docs.netlify.com |