Why this reference exists
Base44's documentation covers the SDK's happy paths but leaves out the failure modes, the undocumented behavior, and the design choices you must work around in production. This reference fills the gap. It is written for engineers who already know JavaScript and need to ship a real app, not for users learning the platform through the AI agent.
The opinions in this document come from auditing dozens of Base44 apps. Where the SDK is well-designed, we say so. Where its defaults are dangerous, we name the danger and the workaround. We will keep this updated; check the Last updated stamp at the top of the page.
SDK overview
The SDK is auto-injected in the Base44 IDE. In code, it is imported as:
import { base44 } from "@base44/sdk";
The base44 object is the root of the public API. Its main namespaces:
base44.entities.<EntityName>— CRUD for any entity defined in your schema.base44.authandbase44.User— authentication and current-user helpers.base44.functions.<functionName>— invocation of backend Deno functions.base44.integrations.<integrationName>— managed third-party calls (LLM, image gen, email).base44.files— upload and download.
We will walk each.
Entities
entities.<EntityName>.list(filter?, sort?, limit?)
Returns an array of records.
Critical default: with no filter argument, this returns every record in the entity, scoped only by the entity name. There is no implicit currentUser scoping.
// Returns ALL todos in the entire app, across every user.
const allTodos = await base44.entities.Todo.list();
// Returns only the current user's todos.
const myTodos = await base44.entities.Todo.list({
created_by: (await base44.User.me()).email,
});
This is the single most consequential design decision in the SDK and the source of most data-leak incidents we audit. Treat list() as fundamentally unsafe without a filter. Add ESLint rules in your repo to flag any call site that is missing one.
Filter syntax supports equality, $in, $ne, $gt, $lt, $gte, $lte, and limited string operators. Logical AND is implicit between fields; logical OR requires $or at the top level.
const recentHighValue = await base44.entities.Order.list({
$and: [
{ user_id: currentUserId },
{ total: { $gte: 100 } },
{ created_date: { $gte: "2026-04-01" } },
],
});
Sort: pass a string with a leading - for descending: "-created_date". Multi-field sort is not officially supported; some apps have made it work with comma-separated fields, but the behavior is undocumented.
Limit: caps at 5,000 per call as of November 2025. There is no offset parameter; for paginated lists, use a sortable field as a cursor.
entities.<EntityName>.filter(query) (deprecated alias)
Older code uses .filter(); new code should use .list(). They are aliases as of the current SDK, but the platform has signaled filter will be removed in a future release.
entities.<EntityName>.get(id)
Returns a single record by ID. Returns null if not found. Does not enforce ownership. A user who knows another user's record ID can fetch it. Wrap in a backend function if the data is sensitive.
entities.<EntityName>.create(data)
Creates a record. The created_by field is auto-populated with the current user's email. The created_date is set server-side.
const todo = await base44.entities.Todo.create({
title: "Ship the audit",
done: false,
});
// todo.id, todo.created_by, todo.created_date populated by server.
Trap: if your entity schema has a field the user should not control (e.g., role, subscription_tier, verified), the client can pass it and the server will accept it. The SDK does not strip unknown or dangerous fields. Strip them server-side in a backend function before allowing creates of sensitive entities.
entities.<EntityName>.update(id, data)
Updates a record by ID. Does not check ownership server-side by default. Any user with the SDK can update any record by ID. This is the second-most-dangerous default after list().
The pattern that works:
// Frontend - never use directly for sensitive entities.
// Instead, call a backend function:
await base44.functions.updateTodoSafely({ id: todoId, patch });
Inside updateTodoSafely, fetch the record, verify record.created_by === currentUser.email, then call update. Without this wrap, the entity is globally writable.
entities.<EntityName>.delete(id)
Deletes a record by ID. Same ownership default as update: not enforced. Same wrap pattern required.
entities.<EntityName>.bulkCreate(records)
Creates multiple records. Batch limit is 1,000 per call.
Bulk delete is missing
There is no bulkDelete method. This has been documented as a production blocker for any app that needs to clean up large volumes. Workaround: a backend function that loops delete() calls, paginated by ID. Each delete is a separate API call, so this is slow and credit-expensive at scale. We track this in our database best practices article.
User and auth
User.me()
Returns the current user object: { id, email, full_name, role, ... }. Returns null if not authenticated.
const me = await base44.User.me();
if (!me) {
// Redirect to login.
base44.auth.signInWithGoogle();
}
Trap: the role field is whatever you stored on the user entity. The SDK does not have a built-in role concept beyond Base44's coarse "admin/user" platform roles. Build your own role model on the user entity if you need granular permissions.
auth.signInWithGoogle() / signInWithEmail({ email, password })
Initiate sign-in flows. Both redirect to a hosted page; you cannot fully customize the UI without forking.
auth.signOut()
Signs the current user out. Clears the JWT from local storage.
User.update(patch)
Updates the current user's record. Note: this can update fields like role if the schema allows it. Strip dangerous fields in a backend function on critical paths.
Backend functions
functions.<functionName>(payload)
Invokes a backend Deno function. Returns the function's response.
const result = await base44.functions.processPayment({
amount: 4900,
currency: "USD",
customerId: "cus_abc123",
});
Behavior notes:
- Functions execute on Base44's Deno runtime.
- Cold-start latency is 200–800ms typical.
- The function inherits the calling user's identity via the JWT.
- Functions cannot share state without going through entities or external storage.
- The
ISOLATE_INTERNAL_FAILUREerror indicates Deno couldn't load the function; usually a syntax error or unsupported import. - Function URLs are routed via
/functions/<name>. There is a documented routing bug where POST requests can return 405 Method Not Allowed; see the function routing fix.
Defining a function
In the Base44 IDE, create a file under backend/functions/<name>.ts:
export default async function handler(req: Request): Promise<Response> {
if (req.method !== "POST") {
return new Response("Method Not Allowed", { status: 405 });
}
const body = await req.json();
// ... validate, do work, return ...
return new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { "content-type": "application/json" },
});
}
Imports: the Deno runtime supports a curated subset of npm and JSR. Some packages will fail with Unsupported dependency. Test imports in the IDE before assuming they work.
Environment variables: set in the IDE under the function settings. Read with Deno.env.get("KEY"). Do not put secrets in code; the AI agent has read access to your code.
Integrations
integrations.invokeLLM({ prompt, model? })
Calls a managed LLM. Costs platform credits. Latency 1–8 seconds depending on model and length.
const response = await base44.integrations.invokeLLM({
prompt: "Summarize this in three bullets: " + text,
model: "gpt-4o-mini",
});
Cost trap: every call burns credits regardless of cache hits. Add your own cache layer (entity-backed or external) for any prompt that repeats.
integrations.generateImage({ prompt, size? })
Generates an image via the platform's image model. High credit cost; cap usage per user.
integrations.sendEmail({ to, subject, body })
Sends transactional email through the platform's managed sender. Trap: deliverability depends on Base44's reputation, which is shared across all platform tenants. A spammy neighbor can land your transactional emails in spam. For anything mission-critical (password reset, payment receipt), use a dedicated provider (Resend, Postmark) via your own API key in a backend function.
Custom integrations are deprecated
Base44 has signaled that adding new custom integrations after March 1, 2026 is no longer supported. Existing custom integrations continue to work, but new ones must go through backend functions. Plan accordingly.
Files
files.upload(file, { entityId?, fieldName? })
Uploads a file to Base44's managed storage. Returns a file record with url, mime_type, size.
Limits:
- 50MB per file.
- MIME type is checked but not magic-byte verified; do not rely on it for security.
- Total storage limits are per-tier and not always published; you can hit an invisible cap.
For files that need access control, store them in your own bucket (S3 with signed URLs) via a backend function and store only the URL in the entity.
files.delete(fileId)
Deletes a file. Does not cascade-delete from entities — if an entity references the file by URL, the URL becomes a 404. Clean up references explicitly.
Error handling
The SDK throws on network failure and on 4xx/5xx responses. Errors include status, message, and sometimes details. Common error codes:
| Error | Meaning | Fix |
|---|---|---|
| 401 | Token expired or invalid | Re-auth |
| 403 | Permission denied | Check entity permissions and RLS |
| 404 | Record or function not found | Verify ID and function name |
| 405 | Method not allowed | Function routing bug; see fix article |
| 409 | Conflict (concurrent update) | Retry with backoff |
| 429 | Rate limited | Exponential backoff |
| 500 | Server error | Retry; if persistent, check status page |
Wrap every SDK call in a try/catch and surface user-friendly messages. The default error messages leak implementation details.
Common SDK mistakes
Calling Entity.list() without a filter. The default returns global data. Every production audit we run flags at least one of these.
Trusting the client to set ownership fields. Always re-validate created_by server-side in a backend function for sensitive entities.
Forgetting that backend functions inherit user identity. There is no service-account mode. If you need cross-user reads, you build the authorization logic yourself.
Using integrations.sendEmail for password resets. Shared deliverability reputation will burn you. Use a dedicated provider.
Ignoring 429 rate limits. The SDK does not retry by default. Add backoff or accept user-visible failures during traffic spikes.
Storing sensitive files in Base44 storage. No fine-grained access control. Use your own bucket for anything sensitive.
Assuming the SDK is portable. It binds to base44.com. Exported code still calls into the platform. Migration requires replacing every SDK call with your stack's equivalent.
Reference summary
- Use
Entity.list({ created_by: currentUser.email })always;list()with no filter is a bug. - Wrap
update/deletein backend functions for sensitive entities. - Build your own role model on the user entity.
- Cache LLM calls; they are slow and expensive.
- Use external email for anything mission-critical.
- Wrap the SDK in a thin project module for one-place swap during migration.
- Add ESLint rules to enforce the patterns above.
Want us to audit your SDK usage?
Our $497 production audit grep-walks every SDK call site in your repo, flags missing ownership filters, identifies privilege-escalation paths, and delivers a prioritized fix list. Most apps have 5–20 unsafe call sites. Order an audit or book a free 15-minute call.
Related reading
- Base44 Database Best Practices — data-modeling patterns that compose with safe SDK usage.
- Base44 Authentication Patterns — auth flows and the SDK's
Usermodule in production. - Base44 Vendor Lock-In Deep Dive — what the SDK binding means for portability.