React Native Supabase Template: Auth, RLS, Storage

How Applighter ships React Native templates with Supabase auth, RLS policies, and Storage already wired — clone, run migrations, build features.

Profile photo of RishavRishav
7th May 2026
Featured image for React Native Supabase Template: Auth, RLS, Storage

React Native Supabase Template: Auth, RLS, Storage

A production-ready React Native Supabase template is supposed to save you the boring weeks: wiring up auth, writing row-level-security policies, configuring a storage bucket, and connecting all of it to Expo without leaking a service role key into your client bundle. Most "boilerplates" hand you screens and a supabase.from('users') call, then leave the actual security model as homework. Applighter takes the opposite stance: every template ships with the Postgres schema, the RLS policies, the auth flow, and the storage helpers already wired — and the marketing site you're reading runs on the same patterns.

Short answer: Applighter's React Native templates ship with a complete Supabase backend — Postgres migrations, hashed magic-link auth tokens, owner-scoped RLS policies, and pre-configured Storage buckets with public-URL helpers. You clone, set two env vars, run the migrations, and have a working authenticated app instead of a six-week setup project.

Developer working on a laptopDeveloper working on a laptop

Why "Supabase out of the box" is harder than it sounds

You can install @supabase/supabase-js in five minutes. The work that actually takes time — and that almost no template ships — is the part between createClient() and a feature that's safe to put in front of a paying user:

  • A separate server client that uses the service role key and never leaks into the React Native bundle.
  • RLS policies that scope every table by auth.uid() instead of trusting the client.
  • A storage bucket with a documented path layout and a getPublicUrl helper your screens can call.
  • An auth flow that doesn't store plaintext tokens, doesn't leak whether an email exists, and rate-limits magic-link sends.
  • Migrations committed to source control so a teammate can supabase db push and get the same schema on day one.

If you've ever read a "build a React Native app with Supabase" tutorial, you've seen step one (install the SDK) and step ten (deploy to TestFlight). Steps two through nine — the security model — are usually missing. That's the gap Applighter's React Native templates are designed to close.

What's actually inside an Applighter template

Open app/apps/config/index.ts in the Applighter repo and you'll see the registry of templates that ship today: weather-app, fitness-app, e-learning-app, taxi-booking-app, ai-calorie-tracker, ai-voice-notes, and chat-with-pdf. Each one is a separate React Native + Expo app, but they share the same Supabase patterns:

LayerWhat ships in the templateWhere it lives
Server SDK clientService-role client, never imported by client codemodules/db/supabaseServer.ts
Browser/native SDK clientAnon-key client for the app shellmodules/db/supabaseClient.ts
Auth flowMagic-link with SHA-256 hashed tokens, 15-min expiry, rate limitingapp/api/auth/magic-link/route.ts + verify/route.ts
RLSOwner-scoped policies on every user-data tablesupabase/migrations/*.sql
StoragePre-named bucket + path convention + public-URL helperapp/apps/lib/data/server.ts
SchemaMigrations checked into git, replayable on a fresh projectsupabase/migrations/

That last row is the one most boilerplates skip. We'll come back to it.

The two-client pattern (and why it matters)

The single most common Supabase mistake in React Native projects is using one client for everything. The service role key bypasses RLS — if it ever ends up in your Metro bundle, every row in your database is readable from any phone running your app.

Applighter splits this in two. Here's modules/db/supabaseServer.ts:

export const supabaseServer = () => {
  const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
  const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
  if (!supabaseUrl || !supabaseServiceKey) return null;
  return createClient(supabaseUrl, supabaseServiceKey);
};

And modules/db/supabaseClient.ts:

export const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);

The factory returning null is deliberate: if SUPABASE_SERVICE_ROLE_KEY is missing, callers see a null and can fall back gracefully instead of throwing in production. The anon-key client, by contrast, is a singleton — there's no scenario where you'd want React Native screens spinning up new clients per render.

In a template, the rule that follows is simple: anything that needs the service role goes through supabaseServer() and lives behind an API route. Everything else goes through supabase and is subject to RLS. This is the same pattern Supabase recommends in its official Expo + React Native quickstart, but written down as code instead of prose.

Code editor with Supabase configurationCode editor with Supabase configuration

Row Level Security: the part everyone gets wrong

RLS is the single Supabase feature that actually keeps your users' data private. It's also the feature most tutorials skip with a hand-wave: "remember to enable RLS on your tables." That's not enough — RLS enabled with no policies means nothing is readable, which fails open to a frustrated developer who then disables RLS to "fix" it.

Every Applighter template ships with policies, not just an enable flag. Two real examples from supabase/migrations/:

Public-read for catalog data (20260113120000_create_blog_posts_table.sql):

CREATE POLICY "Allow public read access to published blogs"
  ON public.blog_posts
  FOR SELECT
  USING (is_published = true);

Owner-scoped for user data (20260309120000_enable_rls_support_requests.sql):

create policy "Users can create support requests"
  on public.support_requests
  for insert
  to authenticated
  with check (auth.uid() = user_id);

create policy "Users can view own support requests"
  on public.support_requests
  for select
  to authenticated
  using (auth.uid() = user_id);

The shape matters. WITH CHECK runs on insert; USING runs on select, update, and delete. Mix them up and you'll either let users insert rows for other users (a quiet horizontal-privilege bug) or block legitimate reads. The Supabase docs cover this in their RLS guide, but having a working pattern in front of you — committed to git, replayable on a fresh project — is the difference between "I read about this once" and "I shipped it correctly."

A template like AI Voice Notes extends the same pattern: every transcript row carries a user_id, and every policy filters on auth.uid() = user_id. There's no "admin override" smuggled in via the anon key — admin operations go through a server route that uses supabaseServer() explicitly.

Storage: a bucket with a path layout

Most "Supabase storage tutorial" articles stop at "call .storage.from(bucket).upload()." They don't tell you how to organize your bucket. Applighter's templates ship a path convention.

From app/apps/lib/data/server.ts:

const { data: files } = await supabase.storage
  .from("templates_assets")
  .list(`apps/${slug}/assets/images`);

const { data: urlData } = supabase.storage
  .from("templates_assets")
  .getPublicUrl(`apps/${slug}/assets/images/${f.name}`);

Three things to notice:

  1. One bucket per use case, not one bucket per feature. templates_assets holds public marketing media; user-uploaded content (like AI voice notes audio) goes into a separate, RLS-protected bucket.
  2. Predictable paths. apps/{slug}/assets/images/{filename} — no random hashes, no UUID directories. You can list() a slug and get exactly that template's assets.
  3. getPublicUrl for read, signed URLs for private. Public marketing assets use getPublicUrl. User-private assets (audio recordings, uploaded PDFs in Chat with PDF) use createSignedUrl with a short TTL.

For more on the trade-offs, the Supabase Storage docs walk through the public/private/signed model in detail.

Cloud storage and server infrastructureCloud storage and server infrastructure

Auth that doesn't leak

The auth flow in Applighter's marketing site — and the pattern its templates inherit — is magic-link, but with three details most tutorials skip:

1. Tokens are stored hashed. app/api/auth/magic-link/route.ts generates a 32-byte random token, hashes it with SHA-256, and stores only the hash in the magic_link_tokens table. The plaintext token only exists in the email link. If someone dumps the database, they have hashes — not working login URLs.

const token = crypto.randomBytes(32).toString("hex");
const tokenHash = crypto.createHash("sha256").update(token).digest("hex");

await supabase.from("magic_link_tokens").insert({
  email,
  token_hash: tokenHash,
  expires_at: new Date(Date.now() + 15 * 60 * 1000),
});

2. The endpoint never reveals whether an email exists. Both "email found" and "email not found" return the same response and take roughly the same time. This blocks email-enumeration attacks — a category most React Native auth tutorials don't even acknowledge.

3. Rate limiting is built in. Three magic-link requests per minute per email, server-side. You can't grind a user's inbox by spamming the endpoint.

Compare that to the typical "add Supabase auth to your Expo app" article, which calls supabase.auth.signInWithOtp() and stops there. You get a working login. You don't get any of the production-grade hardening above.

Comparison: Applighter vs. building it yourself vs. generic boilerplates

What you getBuild from scratchGeneric RN boilerplateApplighter template
React Native + Expo project skeletonYou write it
Supabase client (server + browser split)You write it⚠️ usually one client
RLS policies committed to gitYou write them❌ usually missing
Magic-link auth with hashed tokensYou write it⚠️ plaintext common
Storage bucket + path conventionYou write it⚠️ ad-hoc
Stripe license-grant flowYou write it
Migrations replayable on a fresh projectYou write them⚠️ inconsistent
Time to first authenticated screen4–6 weeks1–2 weeks< 1 day

For reference, mature Supabase SaaS kits like MakerKit ship similar patterns for Next.js web apps — Applighter focuses the same idea on React Native + Expo specifically, where the "no service role key in the bundle" rule is even more important.

A 60-second walkthrough of cloning a template

Once you buy a template from the Applighter catalog, the setup is:

  1. Clone the repo. Each template ships with app/, supabase/, and modules/ directories pre-populated.
  2. Create a Supabase project. Copy the URL and the anon + service-role keys.
  3. Run migrations. supabase db push against the new project. This creates every table, every RLS policy, and every storage bucket the template needs — no manual SQL editor work.
  4. Set env vars. NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY. The service role key only ever lives in .env.local and EAS secrets — never in the bundle.
  5. Run the app. npx expo start. Sign-in works. RLS works. Storage works.

If you've ever spent a Friday rebuilding the auth wiring for the third time, the value here is exactly that: you don't.

Where this fits in your stack

Applighter doesn't try to be a full SaaS framework. The opinion is narrower: start with a working app, not an empty repo. Expo handles the build pipeline. Supabase handles the backend. NativeWind handles styling. Stripe (via the marketing-site license grants) handles purchase. The template's job is to glue those four things together correctly once so you can spend your time on whatever your app actually does — calorie tracking, voice notes, weather, taxi booking — instead of re-deriving the auth flow.

Browse the full template catalog or jump straight to a specific build like the AI Calorie Tracker, which uses every layer described above — auth, RLS-scoped meal entries, and image uploads to a private storage bucket.

FAQ

Do I need a Supabase Pro plan to use Applighter templates? No. Every template runs on the Supabase free tier for development. You'll likely upgrade once you have real users (for higher database CPU and storage limits), but nothing in the template pattern depends on a paid feature.

Can I swap Supabase for Firebase or my own Postgres? You can, but it's not a five-minute job. The migrations, RLS policies, and storage helpers all assume Supabase's auth model (auth.uid()) and Storage SDK. Swapping backends means rewriting the security model, not just the client SDK calls.

Are RLS policies enough on their own? RLS is your last line of defense, not your only one. Applighter templates also gate sensitive operations (license grants, admin actions) behind server routes that use the service-role client. RLS protects the database layer; server routes protect the business-logic layer. Use both.

How do you handle storage for private user uploads? Two-bucket model: templates_assets for public marketing media (uses getPublicUrl), and a separate user-content bucket with RLS-style storage policies and createSignedUrl for time-limited access. The template documents both patterns.

What does this cost? Templates are a one-time $79–$89 per template — no subscriptions. License terms and refund policy are on the refund policy page.

Stay Updated on the Latest UI Templates and Features

Be the first to know about new React Native UI templates and kits, features, special promotions and exclusive offers by joining our newsletter.