To lock down a Base44 app before launch, give every entity an explicit rls rule for all four actions (create, read, update and delete), move anything that needs a real check into a backend function, then run the security scan and test your app signed out. Base44 checks those rules on its server, so when a rule says no, the data never reaches the browser.
Below: the exact rules on this site, why each one looks the way it does, and which ones I've actually tested.
Why I care about this one
Base44's security docs have a line every builder should read twice: "You are responsible for your app's security settings. Base44 provides the tools, but always review your permissions and run a security scan before you publish."
My site is a portfolio, which sounds harmless. But strangers can message me, join my newsletter and react to posts without signing in. So it has to accept writes from anyone, and still make sure only I can read what they sent. That's the job row-level security (RLS) does. My glossary entry on RLS is the two-minute version.
How Base44 RLS works, fast
An entity's rls block has four keys: create, read, update and delete. Each one takes one of three kinds of value:
truemeans everyone, including people who never signed in.falsemeans nobody. Only backend code using the service role gets past it.- A condition, like
{ "user_condition": { "role": "admin" } }, means only people who match.
One detail catches people out. A condition that uses user_condition or {{user.*}} needs a signed-in caller. A signed-out visitor has no user to compare against, so the docs say the rule blocks them outright.
And a "no" doesn't always look like a no:
That's why RLS bugs are sneaky. Here's my own site, signed out, on 28 September:
API=https://aatmanjain.com/api/apps/6ab79c694da396e0e7e13342/entitiescurl -s "$API/SiteSettings" | head -c 48[{"cal_link":"https://cal.com/aatman/quick-chat"curl -s -w " HTTP %{http_code}\n" "$API/Subscriber"[] HTTP 200curl -s -w " HTTP %{http_code}\n" "$API/ContactMessage"[] HTTP 200Settings come back, because anyone can read them. My subscriber list and inbox come back as [] with a 200. Empty, or blocked? From the outside you can't tell. (Blocked: read is admin only on both.)
The four patterns on this site
This site has ten entities of its own, plus the built-in User. Every one has an explicit rule for every action.
- entities, each with all four actions written out
- 10
- rules in total, none left on a default
- 40
- of those 40 say true, each one on purpose
- 5
They fall into four patterns, plus one small variation:
Pattern 1: published, or admin
Posts, projects, resources, testimonials and my now page updates all work like this. Anyone can read a published row. Only an admin sees drafts or writes.
"rls": {
"create": { "user_condition": { "role": "admin" } },
"read": {
"$or": [
{ "user_condition": { "role": "admin" } },
{ "data.status": "published" }
]
},
"update": { "user_condition": { "role": "admin" } },
"delete": { "user_condition": { "role": "admin" } }
}Doesn't user_condition block signed-out visitors? Not here. Inside $or, the data.status branch still matches for them. I tested this exact rule live on my old site: signed out, I got the published rows, and asking for drafts got an empty list.
Also filter by status in the query, like filter({ status: 'published' }). Otherwise you'll see drafts on your own public pages while signed in, and panic that the rule is broken.
Pattern 2: the letterbox
My contact form and newsletter sign-up. Anyone can post something in. Only I can open it.
"rls": {
"create": true,
"read": { "user_condition": { "role": "admin" } },
"update": { "user_condition": { "role": "admin" } },
"delete": { "user_condition": { "role": "admin" } }
}Same shape as the contact form example in Base44's RLS docs. But be honest about what "create": true means: anyone includes a bot calling the entity API directly and skipping your form.
My form's honeypot, validation and throttle (three messages per 15 minutes) all run in the browser, so that bot skips them. What still applies is the schema: every text field has a length limit, and a message tops out at 5,000 characters. Fine for a contact form. Not for anything that costs money per row.
The quieter trade-off: signed-out visitors can't read the subscriber list, so the sign-up form can't check for duplicates. My admin view de-duplicates by email instead. My old site checked on the server with a service role function. More work, cleaner data. Pick on purpose.
Pattern 3: public read, admin write
Site settings, like the announcement bar and my booking link:
"rls": {
"create": { "user_condition": { "role": "admin" } },
"read": true,
"update": { "user_condition": { "role": "admin" } },
"delete": { "user_condition": { "role": "admin" } }
}The variation is emoji reactions: anyone can add one and read the counts, only I can change or delete them. A reaction holds no personal data, which is the only reason I'm happy leaving two doors open.
Pattern 4: admin only
My media library. Nobody else has any reason to list my uploads, so nobody else can.
"rls": {
"create": { "user_condition": { "role": "admin" } },
"read": { "user_condition": { "role": "admin" } },
"update": { "user_condition": { "role": "admin" } },
"delete": { "user_condition": { "role": "admin" } }
}Two more patterns for apps with users
My site has exactly one person who signs in: me. Apps with real users need two more.
Owner only
Each person sees and edits only their own rows. This one is straight from Base44's RLS examples:
"rls": {
"create": true,
"read": { "created_by": "{{user.email}}" },
"update": { "created_by": "{{user.email}}" },
"delete": { "created_by": "{{user.email}}" }
}If admins need access too, wrap each rule in $or with an admin check. This is the "owner OR admin" example from the same docs page:
"read": {
"$or": [
{ "created_by": "{{user.email}}" },
{ "user_condition": { "role": "admin" } }
]
}Signed in only
Look at that "create": true again. Here's how Base44's own permissions table describes the All Users rule:
Even the docs' own "public read" example, described as "only logged-in users can create/edit their own records", uses "create": true. Read an example's values, not just its description.
If you only want signed-in people creating rows, use the docs' "multiple roles with $or" pattern on create:
"create": {
"$or": [
{ "user_condition": { "role": "admin" } },
{ "user_condition": { "role": "user" } }
]
}A signed-in user on a standard setup has the role admin or user, so one branch matches. A signed-out visitor has no user, so the rule blocks them. Added your own roles? List those too. And because this one isn't live-tested: sign out, try a create, and check for a 403.
Play with the rules first
Pick a rule and a viewer (signed out, user A, user B or an admin) and see which actions it allows on a row that belongs to user A, and why. Start with signed out. That's the one people forget.
RLS lab: who can do what
Pick a Base44 row-level security rule (public read with admin write, owner only, owner or admin, or signed-in users create) and a viewer (signed out, user A, user B or an admin).
It shows whether create, read, update and delete are allowed on a record owned by user A, and why, next to the rule's JSON in Base44's rls syntax.
It works the rules out the way the docs describe them. It isn't a live test of your app. That part's still your job.
When rules aren't enough: the service role
Some jobs don't fit in an RLS rule. The docs list $gt, $lt, regex and cross-entity checks as unsupported, and point you to backend functions instead.
An RLS rule can say
- Only admins
- Only the person who created the row
- Anyone, if
statusis published - Any of those, combined with
$or
Needs a backend function
- Only if there are seats left (
$gt,$lt) - Only if the email matches a pattern (regex)
- Only if a row in another entity allows it
- Anything that has to count, compare or look things up
Inside a backend function, base44.asServiceRole skips every rule, row level and field level. The docs put it plainly: your function "is responsible for any access checks it needs". (Backend functions need the Builder plan or above.) Here's an illustrative sketch, not code from an app of mine:
import { createClientFromRequest } from "npm:@base44/sdk";
// Sketch: a signed-in user books a seat, but only while seats are left.
// RLS can't say "only if it isn't full", so this function does.
export default async function (req: Request) {
try {
const base44 = createClientFromRequest(req);
const user = await base44.auth.me().catch(() => null);
if (!user) return Response.json({ error: "Sign in first" }, { status: 401 });
const { eventId } = await req.json();
const event = await base44.asServiceRole.entities.Event.get(eventId);
if (event.booked >= event.capacity) {
return Response.json({ error: "Full" }, { status: 409 });
}
await base44.asServiceRole.entities.Event.update(eventId, { booked: event.booked + 1 });
await base44.asServiceRole.entities.Booking.create({ event_id: eventId, user_email: user.email });
return Response.json({ ok: true });
} catch (error) {
return Response.json({ error: error.message }, { status: 500 });
}
}(A real version would also need to handle two people grabbing the last seat at the same moment. It's a sketch.)
My rules: check who's calling first, validate every input, and send back only what the caller needs. Then make the entities it writes admin only for create and update, so nobody can walk around the function through the entity API. My service role glossary entry has more.
Run the scan, then try to break it
The security scan checks seven types of issue, from data tables missing permission rules to exposed secrets, functions anyone can run and credit-using features strangers could reach.
The scan doesn't apply fixes by itself, and Base44 makes a checkpoint before each fix so you can roll it back. Careful with "Anyone can run this function": its fix makes the function require a signed-in user, which can break a public page, a webhook or an integration. Read before you click Fix all issues.
Then do what the scan can't. It doesn't know an invoice should only be visible to its customer. You do. Here's the pass I run:
Run the scan
Dashboard, then Security, then Run Security Scan. Read every suggested fix before you apply it.
Test signed out
Try to read and create on every entity. Denied creates should throw a 403, and private lists should come back empty.
Test as a normal user
Sign in as a plain user in Preview. Check you see your own rows and nobody else's.
Test as an admin
Check drafts, inboxes and admin pages all load, then sign out again.
The docs suggest testing in Preview as different roles. I'd add signed out every single time. That's the visitor who ruins your week.
My pre-launch checklist
- Every entity has an
rlsblock with all four actions written out. - Every
trueis on purpose, and I can say why a stranger needs it. - Public pages filter by status in the query too.
- Anything RLS can't express lives in a backend function that checks who's calling.
- No API keys in frontend code. They live in secrets.
- The security scan is clean, or every ignored issue has a reason.
- Every copied rule (this post's included) is tested in my own app.
Want a second pair of eyes? That's what my security reviews are for. Either way, open every entity file today and make sure each action has an answer you chose.
FAQ
What is RLS in Base44?
Row-level security is a set of rules on each Base44 entity that decides who can create, read, update and delete which records. Base44 checks the rules on its server, so a denied read never sends the data to the browser. You set them in the entity's rls block or on the table's Permissions page in the dashboard.
What does "true" mean in a Base44 RLS rule?
It means everyone can do that action, including visitors who never signed in. Base44's permissions table describes "All Users" as anyone, "even without signing in". Only use it on purpose, like the create rule on a contact form.
Does the service role bypass RLS in Base44?
Yes. base44.asServiceRole skips row-level and field-level security entirely, and it's only available in Base44-hosted backend functions. That makes the function responsible for checking who's calling and what they're allowed to do.
Is the Base44 security scan enough on its own?
No. It catches common problems like open tables, exposed secrets and functions anyone can run, and it's free on every plan. It can't know your business rules, so also test your app signed out and as each role before you launch.




