When we started building RaiseGate, speed was the priority. Early momentum matters, and Supabase made that easy.
We looked at Firebase, Supabase, and Appwrite, but Supabase stood out because setup was quick and the documentation didn’t slow us down. Within a short time, authentication was working and data was flowing.
We already knew React, so the stack became React on the frontend and Supabase behind it. Nothing fancy. Just enough to start building.
And it worked. Users could sign up. Data saved. Dashboards loaded. From our point of view, the system felt controlled. If the UI didn’t allow something, it felt like the system didn’t either.
Explanation
Supabase is a hosted Postgres database with an API in front of it. Instead of building your own backend server, your app acts like a client that talks directly to the database over the internet. This removes a massive amount of boilerplate setup and lets teams move incredibly fast.
To make this possible, Supabase intentionally exposes the database. Your app doesn’t own the connection. It just uses it.
The Illusion of Control
At the time, we thought the frontend was the boundary. Buttons were hidden. Inputs were validated. Certain actions just weren’t possible through the UI. Since everything behaved as expected, we didn’t question the setup.
There was no reason to.
Explanation
The frontend isn’t a boundary. It’s just a nice interface.
Anything your frontend can do, someone else can reproduce without your app. They don’t need your UI. They only need to understand the network requests your app is making, and modern browsers make those trivial to inspect. UI restrictions suggest behavior. They don’t enforce it.
The Wake Up Call
This only became obvious when we started giving RaiseGate out for beta testing.
A friend testing the product messaged us. He casually mentioned he could directly fetch information for all users from the users table. Not just his own data—everyone’s. Then he added that he could also update user information. Not passwords, but profile data that was never meant to be editable by anyone else.
He wasn’t using the app. He wasn’t "hacking" anything. He was just calling the backend directly.
That was the moment the system stopped feeling safe.
Explanation
What he did wasn’t advanced. He didn’t exploit a bug. He simply removed the frontend from the equation.
Because all the rules lived in the UI, once the UI was gone, the rules were gone too. The database had no way of knowing which rows belonged to which user effectively, so it didn’t stop the request.
Our first instinct was to defend the system. "But the UI doesn’t allow any of this! You couldn’t see other users. You couldn’t edit someone else’s information."
That argument fell apart immediately, because the UI wasn’t involved anymore.
Explanation
The database does not know your UI exists. It doesn’t know what screens you designed or what flows you intended. From its point of view, a request from your app and a request sent directly via cURL look the same.
If there are no rules inside the database itself, it has no reason to reject either.
Luck is Not a Strategy
Nothing catastrophic happened during beta. No accounts were taken over. No data was lost. But it was clear we weren’t protected. We were just lucky.
This forced us to deeply learn the tool we were using.
Explanation
This is what Row Level Security (RLS) is meant to solve. RLS defines rules inside the database that decide which rows a request is allowed to read or modify.
Without RLS, ownership is an assumption. With RLS, ownership becomes a hard rule the database enforces on every single request.
Much later, as we added server-side logic, we learned about another part of Supabase’s model: the service role key. We weren’t using it during beta, but understanding it reframed everything about how we viewed "public" vs "private".
Explanation
The service role key represents the application, not a user. Requests made with this key bypass all Row Level Security checks. The database assumes the request is coming from a trusted system component and executes it directly.
This is intentional. Backend jobs and admin workflows need this level of access.
- The
anonkey is designed to be public. - The
service_rolekey is designed to be private. - One is about what users can do.
- The other is about what the system can do.
The Server as a Boundary
That’s when the idea of a server boundary stopped feeling optional. We didn’t move to Next.js just because it was the trendy framework. We moved because we needed a place where secrets could live and decisions could be enforced without being exposed to the client.
Explanation
A server creates a real trust boundary. The frontend requests actions. The server decides whether they’re allowed. Row Level Security remains the final guardrail inside the database.
Each layer assumes the one before it can be bypassed.
The biggest shift wasn’t technical. It was mental. We stopped assuming that a working system was a safe one. Security isn’t about frameworks or stacks. It’s about where trust lives.
If trust lives in the browser, it can be copied. If it lives in the server and the database, it can be enforced.
Looking back, the mistake wasn’t choosing Supabase or React. Both did exactly what they promised. The mistake was trusting the most convenient place instead of the correct one.
We built fast. We just trusted the wrong thing first.