Does Base44 have a bulk delete?
Base44's SDK reference documents deleteMany(query) as the call that "Permanently removes all records that match the provided query", and it returns a count of what it deleted. Run it from a backend function that checks the caller is an admin, count the matches with count() first, and export the records, because the SDK reference describes the delete as permanent.
An earlier version of this page said Base44 had no server-side bulk delete. That is wrong for the current SDK. The entities reference lists deleteMany(query), which "Deletes multiple records matching a query", next to bulkCreate(), updateMany() and bulkUpdate(). Base44's developer changelog dates updateMany() and bulkUpdate() to March 16, 2026; neither it nor the product changelog gives a date for deleteMany().
Base44's docs describe four ways to delete many records at once:
| Way to delete many records | Runs from | What it removes | Entity automations | What the docs say about recovery |
|---|---|---|---|---|
deleteMany(query) | App code or a backend function | Every match the caller may delete | Not triggered (by-query call) | "Permanently removes all records that match the provided query" |
delete(id), one call per record | App code or a backend function | One record per call | Triggered (single-record call) | "Permanently removes a record from the database" |
| Data tab: select rows, or Clear table records | The app dashboard | Selected rows, or the whole table | Not documented | Kept in Recently deleted for 30 days |
| REST Apps API, Delete entity records (beta) | Your own script, with a personal API key | Matches the Delete rule allows | Not triggered | Records "move to the entity's trash" |
Base44 marks the REST Apps API as beta and says to "avoid depending on it in production", so the code on this page uses the SDK. Every method and quote here was checked against Base44's documentation on 17 October 2026.
What still goes wrong with bulk deletes at scale
deleteMany() removes the need for a hand-written loop, not the risk. Six things still decide whether a bulk delete removes what you meant.
The query matches more than you meant
deleteMany() takes the same EntityFilterQuery as filter() and count(): exact values, an array to match any of several values, null, field operators such as $in and $gte, and $and, $or and $nor at the root. The reference is blunt about the result: "Every matching record is deleted." A dropped condition widens the match, and field names are case-sensitive, so check every key against the schema. Base44's REST reference for its by-filter delete adds that an empty object, {}, "deletes every record in the entity". Build the query inside your function, never from request input, and refuse to run it with no conditions.
Delete rules decide what a user-scoped delete removes
Called through base44.entities from your app, deleteMany() runs as the signed-in user: the entities reference says access "is scoped to the current user's permissions". That cuts both ways. If an entity's Delete permission is All Users, which Base44's permissions guide defines as "Anyone can perform this action, even without signing in", any visitor can send a bulk delete for that entity. If the rule is stricter than your query, some matches stay: the REST reference says its by-filter delete leaves records the Delete rule blocks "alone rather than failing the call". The SDK reference does not say how deleteMany() reports blocked records, so compare its deleted count with your preview.
Service role deletes everything the query matches
Inside a backend function, base44.asServiceRole.entities skips every rule. Base44's entity security docs say service role "bypasses both RLS and FLS entirely, regardless of how the entity is configured", and that "your function is responsible for any access checks it needs." A service-role deleteMany() behind a function anyone can call is a delete-everything button. Check the caller's role before the first query runs.
Entity automations do not fire
Base44's automations reference says entity automations "only fire for single-record create, update, and delete calls made through the entities API", and that by-query operations do not trigger them. deleteMany() works by query, and the REST reference says its own by-filter delete "doesn't trigger the app's webhooks, automations, or workflows." An on-delete automation that removes child records, files or search entries will not run, so do that cleanup yourself or delete one record at a time.
Per-record loops stop at 5,000 or get rate-limited
Code that fetches matches and deletes them one by one runs into three documented limits:
- The 5,000-item cap. Since November 27, 2025, Base44 returns at most 5,000 items per request (docs.base44.com), and the array form of
filter()defaults to 5,000 results. A loop that fetches once deletes the first 5,000 matches and stops. - Rate limits. Base44's security overview says "All public endpoints are rate-limited by default." Thousands of single deletes in a burst can draw 429 responses.
- The function time limit. Backend functions "have a maximum execution time of 5 minutes" (docs.base44.com), and Base44 terminates longer requests, which can cut a long loop off partway.
The SDK reference calls the delete permanent
The SDK reference says deleteMany() "Permanently removes all records that match the provided query" and that delete() "Permanently removes a record from the database." Other Base44 pages describe a trash. The data management guide says "Deleted records are kept for 30 days" under Recently deleted, and the REST reference says records deleted through its endpoint "move to the entity's trash". The SDK's public source shows deleteMany() sending one DELETE request to the same entity path as that REST endpoint, with your query as the body. Base44 does not say the two behave the same, so plan as if SDK deletes are permanent: export first, and treat Recently deleted as a place to check, not a backup.
How to run deleteMany safely, step by step
The pattern below is an admin cleanup that removes archived orders. Swap Order and the query for your own entity and conditions.
1. Export the records first
Open the table under Data in your app's dashboard, filter it to the records you plan to remove, then choose More actions and Export. With a search or filter on, Base44 says the option "reads Export filtered records and exports only the matching records." Its own tip: "Before you delete records, consider exporting the table so you have a backup copy of your original data." If the dashboard filters cannot express your query, export the whole table.
2. Put the delete in a backend function with an admin check
// Backend function: purgeArchivedOrders
import { createClientFromRequest } from "npm:@base44/sdk";
Deno.serve(async (req) => {
const base44 = createClientFromRequest(req);
// Fail closed: only a signed-in admin gets past this point.
const user = await base44.auth.me().catch(() => null);
if (!user) return Response.json({ error: "Unauthorized" }, { status: 401 });
if (user.role !== "admin") return Response.json({ error: "Forbidden" }, { status: 403 });
// A missing or unreadable body means "count only".
const { dryRun = true, expected } = await req.json().catch(() => ({}));
// The query is fixed here, never read from the request.
const query = { status: "archived" };
const orders = base44.asServiceRole.entities.Order;
const matches = await orders.count(query);
if (dryRun) return Response.json({ dryRun: true, matches });
// Refuse if the data changed since the admin saw the preview.
if (matches !== expected) {
return Response.json({ error: "Match count changed", matches, expected }, { status: 409 });
}
const { deleted } = await orders.deleteMany(query);
const remaining = await orders.count(query);
return Response.json({ deleted, remaining });
});
The query lives in the function, so no caller can widen it. A call without a body only counts, and the real run refuses to start if the number of matches changed after the admin saw the preview. Service role keeps the count and the delete on the same footing: once the role check has passed, neither one follows the entity's rules.
3. Preview the count, then confirm
import { base44 } from "@/api/base44Client";
export async function purgeArchivedOrders() {
// Dry run: counts what the query matches. Nothing is deleted.
const { data: preview } = await base44.functions.invoke("purgeArchivedOrders", { dryRun: true });
if (!window.confirm(`Permanently delete ${preview.matches} archived orders?`)) return null;
// Real run, pinned to the count the admin just approved.
const { data: result } = await base44.functions.invoke("purgeArchivedOrders", {
dryRun: false,
expected: preview.matches,
});
return result; // deleted and remaining
}
You can also rehearse in preview with test data, which Base44 offers on the Builder plan and higher. Its test data guide says that with testing mode on, "all data reads and writes go to your test data." The test database starts empty, so add sample records that look like the ones you will delete, and check that your production count did not move before you trust the rehearsal.
4. Check what is left
The function returns deleted and remaining, and remaining should be 0. The SDK reference documents no has_more flag for deleteMany(), unlike updateMany(), whose "Results are batched in groups of up to 500", so the count afterwards is your proof that the job is done. If records still match, preview and run again, and investigate if the number does not fall.
5. Narrow the entity's Delete rule
The function above does not depend on the entity's rules, but every other delete in your app does. Keep the Delete permission as narrow as your users need. Base44's permissions guide shows the admin-only version: "You want only admins to be able to delete records." Set User property to User Role and Required value to Admin. That rule limits delete() and deleteMany() calls made from the browser.
When a per-record delete loop is still the right tool
Use deleteMany() unless you need one of these:
- On-delete automations for every record. They fire only for single-record deletes.
- Progress an admin can watch, or a delete you can stop between runs.
The function below deletes one record per call for up to four minutes, under the 5-minute limit, and reports how many records still match. Each pass re-reads the first page of matches, so a run that fails partway is safe to repeat.
// Backend function: deleteArchivedOrdersOneByOne
import { createClientFromRequest } from "npm:@base44/sdk";
const TIME_BUDGET_MS = 4 * 60 * 1000; // under the 5-minute function limit
const PAGE_SIZE = 100;
Deno.serve(async (req) => {
const base44 = createClientFromRequest(req);
const user = await base44.auth.me().catch(() => null);
if (user?.role !== "admin") return Response.json({ error: "Forbidden" }, { status: 403 });
const query = { status: "archived" };
const orders = base44.asServiceRole.entities.Order;
const started = Date.now();
let deleted = 0;
while (Date.now() - started < TIME_BUDGET_MS) {
// Deleted records stop matching, so the first page is always fresh work.
const page = await orders.filter(query, { limit: PAGE_SIZE });
if (page.items.length === 0) break;
for (const record of page.items) {
await orders.delete(record.id); // single-record delete: entity automations fire
deleted += 1;
}
}
const remaining = await orders.count(query);
return Response.json({ deleted, remaining });
});
import { base44 } from "@/api/base44Client";
export async function deleteArchivedWithProgress(onProgress) {
while (true) {
const { data } = await base44.functions.invoke("deleteArchivedOrdersOneByOne", {});
onProgress(data); // show data.deleted and data.remaining to the admin
// Stop when nothing is left, or when a run made no progress.
if (data.remaining === 0 || data.deleted === 0) return data;
}
}
Requests go one at a time, which keeps each burst small. If a run fails with a 429, wait and call it again; it picks up whatever still matches. PAGE_SIZE and the time budget are starting points: lower them if one page of deletes takes long enough to threaten the limit.
Bulk delete without code: the Data tab and AI chat
Admins who do not write code have three documented options in the dashboard (docs.base44.com):
- Select rows and move them to trash. In a table, you can "Select several to move them to trash together".
- Clear a whole table. On the table's card, More actions then Clear table records removes every record in it.
- Ask the AI chat, for example "Delete all records from the Orders entity." Base44 says the AI "prepares the delete action and asks for your approval before anything is removed."
Two limits apply. "The data table only shows up to 5,000 items, even if your collection contains more", so selecting rows only reaches what has loaded. And Clear table records has no filter: it empties the table. Records deleted from the Data tab stay in Recently deleted for 30 days.
If you deleted the wrong records
Work through these in order:
- Recently deleted. Open the table's More actions menu and choose Recently deleted. The data management guide says deleted records are kept there for 30 days. Given the SDK reference's "permanently" wording, do not count on SDK deletes appearing there, but look first.
- Backup & Restore, on Elite and Enterprise. "Backups are available on the Elite plan and on an Enterprise plan (Business or Enterprise). Elite keeps 7 days of backups, and Business and Enterprise keep 30 days." (docs.base44.com) You can restore one table or every table, only people with editor access can restore, and restores run on production data only.
- Your export. Import the CSV back into the table. Dashboard imports "add new rows only", so the restored rows are new records; repair anything that pointed at the old ones.
DIY vs hire decision
DIY is realistic when the delete touches one entity, the query is a plain field match you can preview with count(), and you have an export. The functions above are short. The care goes into the query, the role check and the recovery plan.
Hire help if any of these apply:
- The delete spans related entities that must stay consistent, such as orders, their line items and their refunds.
- An on-delete automation or webhook has to run for every record.
- Nobody on the team reads the Deno and TypeScript the function is written in, and a wrong query would be expensive.
Need this fix shipped this week?
Our fix sprint covers one high-impact issue: root cause, fix, regression test plan and written summary. For a bulk delete that means the dry-run function, the admin check, the Delete rules and a recovery plan. It starts within 48 hours of payment and access.
Book a fix sprint or order a $497 audit if you want to confirm scope first.
Related problems
- Base44 429 rate-limit errors — what to do when a per-record loop starts drawing 429 responses.
- Base44 function timeout errors — the 5-minute limit that caps each delete run.
- Base44 RLS drift after an AI edit — check that a Delete rule has not loosened before you rely on it.
- Base44 SDK reference —
bulkCreate(),updateMany(),bulkUpdate()and the rest of the entities module.