Designing a Secure API with Cloudflare Workers × Supabase
In this article, I explain how I approached Supabase and RLS, and how I incorporated them into my API design, based on a personal project built with:
React Native → Cloudflare Workers (Hono) → Supabase
The API assumes a simple use case: user-specific notes.
Supabase is powerful but very flexible, which makes it difficult to decide how much responsibility to delegate to it.
This article exists to clarify:
- where I chose to rely on Supabase,
- where I deliberately did not,
- and why I made those decisions.
The concepts discussed here are demonstrated in the following repository:
https://github.com/umemura-dev/hono-supabase-auth-security-demo
1. How I Positioned Supabase (Initial Design Principles)
Supabase provides many features out of the box: database, authentication, storage, and auto-generated APIs.
Without clear boundaries, it’s easy for responsibilities to blur and the system to become complex.
So I fixed the following principles at the very beginning.
Principle 1
Supabase is responsible for data integrity and safety.
Principle 2
Final access decisions must always be enforced by RLS.
Principle 3
Business logic, input validation, and error handling live in Workers (the API layer).
Principle 4
Client-side privileges are kept minimal, and Service Role keys are never exposed.
By defining these four rules, each layer’s responsibility became clear,
and I avoided pushing application logic into Supabase unnecessarily.
2. Supabase Components Used in This Project
Auth (Authentication)
The role of Supabase Auth in this design is simple:
to reliably identify the user (UID).
On the Workers side, the flow is:
- Extract the JWT from the
Authorizationheader - Verify its signature using Supabase’s
JWT_SECRET - Treat the
subclaim (UID) asuserId
This userId is stored in the request context and reused for database operations.
Database (PostgreSQL)
This demo uses a single table, demo_notes,
designed to store user-specific data.
Example columns:
- id
- user_id
- title
- body
- created_at
- updated_at
- deleted_at
The deleted_at column enables soft deletes, avoiding physical row deletion.
RLS (Row Level Security)
RLS is a native PostgreSQL feature that determines:
“Is this user allowed to access this row?”
Supabase tightly integrates Auth and RLS,
allowing the JWT’s UID (sub) to be referenced as auth.uid().
Even if Workers validates userId,
without RLS there would still be a risk of data exposure if a malicious client accessed Supabase directly.
For that reason, RLS is always treated as the final defensive layer.
3. Example RLS Policies (demo_notes)
Below is a minimal RLS setup used in this project.
In real-world systems, you would typically extend this with read-only roles, admin roles, etc.
All policies use to authenticated, meaning:
only users authenticated via Supabase Auth are eligible.
This cleanly blocks unauthenticated or malicious access.
SELECT (Only fetch your own notes)
create policy "select_own_notes"
on demo_notes
for select
to authenticated
using (
auth.uid() = user_id
);
INSERT (Only create notes for yourself)
create policy "insert_own_notes"
on demo_notes
for insert
to authenticated
with check (
auth.uid() = user_id
);
UPDATE (Only update your own notes)
create policy "update_own_notes"
on demo_notes
for update
to authenticated
using (auth.uid() = user_id)
with check (auth.uid() = user_id);
Note:
DELETE is not exposed via the API.
Instead, notes are soft-deleted by updating deleted_at.
4. How I Designed the Workers (Hono API)
Responsibilities are clearly split as follows.
Workers Responsibilities
- JWT verification (authentication)
- Extracting
userId - Input validation (Valibot)
- Application-specific logic (e.g. soft delete)
- Preparing data for Supabase
- Logging, rate limiting, error handling
Supabase (RLS) Responsibilities
- Determining row-level access permissions
- Blocking unauthorized access even if the API layer is bypassed
No matter how careful the Workers layer is,
it can never be considered perfectly secure.
By enabling RLS, I ensure that even if a request reaches the database, other users’ data remains inaccessible.
5. Why I Use Supabase RLS (Compared to Plain PostgreSQL)
RLS is available in PostgreSQL everywhere,
but I chose Supabase for three main reasons.
Automatic Auth–RLS Integration
Supabase provides a built-in bridge between JWTs and auth.uid(),
dramatically reducing design and implementation complexity.
Safe Even with Direct Client Access
Even when using Supabase’s official PostgREST API or client SDKs,
RLS policies are always enforced.
Security does not depend on routing everything through Workers.
The client SDK also supports automatic token refresh,
which simplifies client-side implementation.
That said, designing a system that safely supports direct access requires:
- careful RLS design,
- query design,
- cache strategies,
- and well-defined permission boundaries.
Since this project focuses on API security design,
I intentionally centralized access through Workers.
Strong Security Even for Personal Projects
RLS policies are defined in simple SQL and can be customized with conditions.
Once you understand the syntax, it becomes relatively easy
to enforce strong guarantees—even in small personal projects.
6. Summary: What This Architecture Prioritizes
Although this is a small demo project, the design emphasizes:
- Workers (API) handle application behavior
- RLS (DB) provides the final security boundary
- Supabase Auth establishes user identity
With this structure,
I believe the risk of catastrophic data leaks is significantly reduced.
Going forward, I plan to expand on this foundation by documenting:
- request signing strategies,
- and additional API-side security patterns.
Demo Implementation
Everything described in this article is implemented in the following repository:
- Cloudflare Workers + Hono API
- Supabase Auth (JWT) + PostgreSQL RLS
- App Guard (signature-based client authentication)
- CI passing (Biome / TypeScript / Tests)
GitHub:
https://github.com/umemura-dev/hono-supabase-auth-security-demo
Top comments (0)