●FUNDING — Rork raised a $15M seed led by Left Lane Capital, with Peak XV, True Ventures, Goodwater, and a16z Speedrun joining●ENGINE — Rork Max runs on Claude Code and Claude Opus 4.6; it drew 8M+ views on X and doubled annual revenue in two weeks●SWIFT — Rork Max is the first web-based Swift app builder, positioned to replace Apple's traditional Xcode●PRODUCT — Rork Max covers the whole Apple ecosystem: iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessage●CLASSIC — The original Rork uses React Native (Expo), building iOS/Android apps from a plain-English description●PRICING — Start free; paid plans begin at $25/mo, and Rork Max is $200/mo●FUNDING — Rork raised a $15M seed led by Left Lane Capital, with Peak XV, True Ventures, Goodwater, and a16z Speedrun joining●ENGINE — Rork Max runs on Claude Code and Claude Opus 4.6; it drew 8M+ views on X and doubled annual revenue in two weeks●SWIFT — Rork Max is the first web-based Swift app builder, positioned to replace Apple's traditional Xcode●PRODUCT — Rork Max covers the whole Apple ecosystem: iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessage●CLASSIC — The original Rork uses React Native (Expo), building iOS/Android apps from a plain-English description●PRICING — Start free; paid plans begin at $25/mo, and Rork Max is $200/mo
Three Implementations You Always Face with Next.js and Supabase — RLS, State, and Real-Time
Past building the foundation of a web app lie three things you always get stuck on — DB access control (RLS), screen state management, and real-time updates — explained at the implementation level with Next.js and Supabase code. Down to production pitfalls like memory leaks from forgotten unsubscribes and how auth ties into RLS.
The previous article drew the big picture of a web app and the move of leaving the foundation to a BaaS. This follow-up steps into the three implementations you will inevitably face on that foundation: DB access control, screen state management, and real-time updates. Each is the classic "I got something working, but I stall before shipping."
As an indie developer building app backends, I got stuck on each of these three in a different way. Let me sort them out in order, with code.
Why "anyone can hit the DB directly" is dangerous
The flip side of a BaaS's convenience is that the browser can call the DB directly. That leads to the natural worry, "could a malicious person see other people's data too?" That worry is correct, and what answers it is RLS (Row Level Security).
RLS is the feature of "setting, on each row of a table, who can access it, in the DB itself." The query is the same whoever sends it, but RLS looks at the sender and automatically narrows the rows it returns.
-- enforce "you only see tasks you created" at the DB levelalter table tasks enable row level security;create policy "own tasks only" on tasks for select using (auth.uid() = owner_id);
In ordinary programming you tend to think "access control is written in app code (if statements)," but in the BaaS world the DB itself holds the rule. So even if there is a bug in the frontend code, leakage of other people's data is prevented at the DB level. My recommendation is to always make "create a table, enable RLS, write the policy" one single unit of work. Put it off, and it ships to production as is and becomes an incident.
Auth and RLS are linked through the same token
The auth.uid() that appeared in the RLS policy is the very token representing login state. Hand-rolling login is hard, but with a BaaS you handle it like a library.
// sign inconst { data, error } = await supabase.auth.signInWithPassword({ email: 'user@example.com', password: 'password123',});// get the currently signed-in userconst { data: { user } } = await supabase.auth.getUser();
What helps to know is that login state is managed by a token stored in the browser. The browser holds the token received at login and sends it along on later requests, telling the server "this is that signed-in user." That token is the contents of the RLS auth.uid(). So auth and RLS are linked through the same token. Once that connects in your mind, the whole picture of access control clicks at once.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦How to write RLS policies that protect other users' data when the browser hits the DB directly, and how they tie to the auth token
✦A minimal global state management (Zustand) implementation that avoids prop drilling, plus when to use it
✦Concrete useEffect cleanup code that prevents memory leaks from forgotten Realtime unsubscribes
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
In React, each component can hold state. But when you want to share the same data (say, the signed-in user) across multiple screens, that alone is inconvenient — you end up prop-drilling through many layers.
What solves this is a global state management library. A lightweight one like Zustand prepares "a shared place accessible from the whole app" with minimal code.
// lib/store/user.tsimport { create } from 'zustand';export const useUserStore = create((set) => ({ user: null, setUser: (user) => set({ user }),}));// callable directly from any component (no prop drilling)function UserMenu() { const user = useUserStore((state) => state.user); return <div>{user?.name}</div>;}
The criterion is "does this data need to be shared across multiple distant components?" Yes means global state; no means ordinary local state is enough. Making everything global from the start, conversely, leaves you unable to track "where the data changed," so I recommend not over-reaching here. I, too, globalized everything early and later lost the thread.
What real-time is, and the unsubscribe pitfall
Features where "another person's action reflects on your screen too," like chat or collaboration tools, look mysterious at first. The mechanism: where ordinary HTTP is one-way ("the browser goes to ask the server"), it uses a separate channel called WebSocket to create a state where the server can actively push data to the browser.
With Supabase Realtime, you can subscribe to DB changes and auto-update the screen.
useEffect(() => { const channel = supabase .channel('tasks-changes') .on('postgres_changes', { event: '*', schema: 'public', table: 'tasks' }, (payload) => { // update the screen state here }) .subscribe(); return () => { supabase.removeChannel(channel); // always unsubscribe when you leave the screen };}, []);
The pitfall you must close before shipping is forgetting to unsubscribe. If you start a subscription inside useEffect, you must write a teardown that unsubscribes when the component leaves the screen. Forget it, and channels pile up every time you visit the page, wasting memory and connections — a memory leak. I missed this and burned time hunting why behavior got heavy during development. The workaround to remember: writing the cleanup function is part of the same single move as subscribing.
Think of form validation as separate from types
A form that takes user input must always check "is the input the right shape?" Doing this with hand-written conditionals balloons, so a library that defines a schema first and checks automatically (like Zod) is common.
import { z } from 'zod';const taskSchema = z.object({ title: z.string().min(1, 'Title is required').max(100), dueDate: z.date().optional(),});const result = taskSchema.safeParse(formData);if (!result.success) { console.log(result.error.issues); // tells you what is wrong}
What to grasp here is that a TypeScript type and runtime validation are different things. The type only checks at the time you write code; what the user actually inputs is unknown until runtime. A library like Zod plays the role of verifying at runtime that the actual value is correct. In production, on the premise that unexpected values arrive from outside, I recommend always inserting this runtime check as a layer.
As a pre-production checklist
Finally, let me arrange the above as an order of implementation. Create a table, then enable RLS and write the policy. Make global only what truly needs sharing. Always write real-time subscriptions paired with cleanup. Insert runtime validation on forms, separate from types. Make these four a single-set habit from the start, and you greatly cut the work of digging up incidents later.
The jargon looks heavy, but each piece is past knowledge translated into the web context. I myself got stuck on these three in order on my first app, and got past each by understanding the mechanism. If you are stuck in the same place, I hope a map in hand helps you take one step forward. For now, start by adding one RLS enablement to a small app.
Share
Thank You for Reading
Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.