BASE44DEVS

ARTICLE · 19 MIN READ

Base44 SDK Reference: Entities, Auth, Functions, and Integrations

The Base44 SDK is the JavaScript client your app uses for data, auth, files, and backend functions. This reference covers the documented surface — the entities module, the auth module, file uploads through Core integrations, and functions.invoke() — plus the defaults that catch teams off guard. It is opinionated about what to use, what to avoid, and which permission rules you must set before SDK calls are safe in production.

Last verified
2026-10-17
Published
2026-05-01
Read time
19 min
Words
3,741
  • SDK
  • REFERENCE
  • ENTITIES
  • AUTH
  • FUNCTIONS

The Base44 SDK exposes the entities module (list, filter, count, get, create, update, delete, upsert, bulkCreate, updateMany, deleteMany), the auth module (me, updateMe, loginViaEmailPassword, loginWithProvider, logout), backend function calls through functions.invoke(), and Core integrations (InvokeLLM, GenerateImage, UploadFile, SendEmail). Its single biggest design trap is that the SDK adds no ownership filter of its own: list() returns every record the entity's Read rule allows, and a Read rule left on All Users is a common source of the data leaks we find in audits. Production apps need row-level security rules on every entity, backend functions that check the caller before any service-role write, custom User fields locked down before access rules depend on them, cached LLM calls, and a dedicated email provider for anything mission-critical.

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, maintained by the lead engineer at Base44Devs, 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. If that is not you, hire a Base44 SDK expert instead.

The opinions in this document come from our audits of production Base44 apps. Where the SDK is well-designed, we say so. Where its defaults are dangerous, we name the danger and the workaround. Every method name below was checked against Base44's SDK reference on 23 September 2026. We will keep this updated; check the Last updated stamp at the top of the page.

SDK overview

Inside a Base44-generated app, the client is created for you. Base44's docs import it like this:

import { base44 } from "@/api/base44Client";

Outside Base44 (your own frontend or backend using Base44 as the backend), install @base44/sdk and create the client with your app ID, which appears in the Base44 editor URL:

import { createClient } from "@base44/sdk";

const base44 = createClient({ appId: "your-app-id" });

The base44 client is the root of the public API. Its main modules:

  • base44.entities.EntityName — CRUD, bulk operations and realtime subscriptions for every entity in your schema, including the built-in User entity.
  • base44.auth — the current user, login, registration, password reset and logout.
  • base44.functions — invoke() and fetch() for your backend Deno functions.
  • base44.integrations — Core built-ins (LLM, image generation, file upload, email) and custom workspace integrations.
  • base44.asServiceRole — the same modules with entity access rules bypassed, available only inside Base44-hosted backend functions.

The docs also list agents, analytics, app-logs, connectors and sso modules; this reference covers the five above, which every production app touches. There is no files module and no User module: uploads go through integrations.Core, and user data lives on the User entity and the auth module.

Method names that do not exist

Base44's own write-up on its agent Skills lists names that AI coding agents hallucinate, among them signInWithGoogle(), signInWithEmailAndPassword(), auth.currentUser, entities.Task.find() and functions.call() (base44.com/blog/base44-skills). The other rows below are names an earlier revision of this reference used. The right-hand column is the documented method.

Name that does not existDocumented method
base44.auth.signInWithGoogle()base44.auth.loginWithProvider('google')
base44.auth.signInWithEmailAndPassword(email, password), base44.auth.signInWithEmail({ email, password })base44.auth.loginViaEmailPassword(email, password)
base44.auth.currentUser, base44.User.me()await base44.auth.me()
base44.User.update(patch)base44.auth.updateMe(data)
base44.auth.signOut()base44.auth.logout()
base44.entities.Task.find()base44.entities.Task.filter(query)
base44.functions.call(), base44.functions.myFunction(payload)base44.functions.invoke('myFunction', data)
base44.integrations.invokeLLM()base44.integrations.Core.InvokeLLM()
base44.files.upload(file)base44.integrations.Core.UploadFile({ file })

Entities

entities.EntityName.list()

list() has three forms in Base44's SDK reference (read 17 October 2026). The cursor form, list({ sort, limit, cursor, fields }), returns one page as { items, next_cursor, has_more }: limit defaults to 100 and tops out at 5,000, and you pass next_cursor back as cursor to keep paging. The positional form, list(sort?, limit?, skip?, fields?), returns a plain array, defaults limit to 5,000 and is "kept for existing code"; Base44 says to prefer a cursor for pagination. A third form returns one field's distinct values. sort defaults to '-created_date' in every form. list() takes no query; to narrow results, use filter().

Critical default: the SDK adds no ownership filter of its own. list() returns every record the entity's Read rule lets the caller see. The editor's All Users rule means "Anyone can perform this action, even without signing in" (docs.base44.com), so on an entity whose Read permission is All Users, list() hands every record in the app to anyone who can reach it, one page at a time.

// Positional form: up to 5,000 Todos the caller's Read rule allows, newest first.
// On an All Users entity, paging reaches every Todo in the app.
const allTodos = await base44.entities.Todo.list();

// Cursor form: one page at a time (100 by default), unaffected by new records.
let page = await base44.entities.Todo.list({ sort: "-created_date", limit: 1000 });
while (page.has_more) {
  page = await base44.entities.Todo.list({ cursor: page.next_cursor, limit: 1000 });
}

// Narrows the result to the current user's todos.
// A UI convenience, not a security boundary.
const me = await base44.auth.me();
const myTodos = await base44.entities.Todo.filter({ created_by: me.email });

This is the single most consequential default in the SDK and a common source of the data leaks we find in audits. A filter() in your frontend does not fix it: anyone can open the browser console and call list() on the same entity. The fix lives in the entity's row-level security rule, {"created_by": "{{user.email}}"} on Read, which the editor's permissions page calls Creator Only.

entities.EntityName.filter(query)

The query method, in the same forms as list(): filter(query, { sort, limit, cursor, fields }) returns a cursor page, and the positional filter(query, sort?, limit?, skip?, fields?) returns an array with limit defaulting to 5,000. It is not deprecated: Base44's Skills write-up lists entities.Task.filter() as the correct name.

Query syntax: field-value pairs match exactly and must all match (AND is implicit), an array matches any of its values, null matches null, and MongoDB query operators handle the rest: $in, $ne, $gt, $gte, $lt, $lte and $or among them. Field names are case-sensitive. Base44 describes the database behind entities as NoSQL and MongoDB-compatible (docs.base44.com).

const recentHighValue = await base44.entities.Order.filter(
  {
    user_id: currentUserId,
    total: { $gte: 100 },
    created_date: { $gte: "2026-04-01" },
  },
  "-created_date",
  100,
);

These multi-condition filters are the workhorse behind most aggregations; if you are rolling up records into charts or summaries, see how to build a reporting dashboard in Base44 on top of these queries.

Sort: one field name, with a - prefix for descending ("-created_date") and no prefix or + for ascending. The documented SortField type takes a single field; multi-field sort is not part of it.

Limit and paging: the cursor forms default to 100 records per page and the positional forms to 5,000; 5,000 is the maximum either way. The positional form pages with skip (list("-created_date", 10, 20) skips 20 records and returns the next 10), and records added or deleted between calls shift that window. The cursor forms do not: Base44 says "Records added or deleted between pages never shift the boundary." For totals and "page N of M", count(query?) returns the number of records the caller can read "without fetching them".

entities.EntityName.get(id)

Returns a single record by ID. It is checked against the same Read rule as list(), so on an All Users entity anyone who has a record's ID can fetch it. IDs travel in URLs, logs and shared links; treat them as public and let the Read rule do the protecting.

entities.EntityName.create(data)

Creates a record and returns it with server fields such as id and created_date filled in. Every record also carries created_by (the creator's email) and created_by_id, which are what Creator Only rules compare against.

const todo = await base44.entities.Todo.create({
  title: "Ship the audit",
  done: false,
});
// todo.id and todo.created_date are set by the server.

Trap: if your schema has a field the user should not control (role, subscription_tier, verified), the client can set it on create unless something stops it. Base44 supports field-level security in the entity schema (a write rule on the field), but the editor's permissions page works per table, and its FAQ says you cannot restrict individual fields there. Either add a field-level rule through the entity schema or route creates of sensitive entities through a backend function that sets those fields server-side.

entities.EntityName.update(id, data)

Updates only the fields you pass. It is checked against the entity's Update rule, and if that rule is All Users, any caller can update any record by ID. This is the second-most-dangerous default after an open Read rule.

The standard fix is Creator Only on Update and Delete ({"created_by": "{{user.email}}"}). When an update needs logic a rule cannot express, such as which fields may change or which status transitions are legal, send it through a backend function:

// Frontend: send the change to a backend function, not straight to the entity.
await base44.functions.invoke("updateTodoSafely", { id: todoId, patch });

Inside updateTodoSafely, get the record, verify record.created_by === user.email, drop any fields the caller may not change, then call update. If the function writes through base44.asServiceRole, that check is the only thing between the caller and every record, because service role skips all entity access rules.

entities.EntityName.delete(id)

Deletes a record by ID and returns { success }. It is checked against the Delete rule; the fix is the same as for update.

Bulk operations

  • bulkCreate(records) creates many records in one request.
  • bulkUpdate(records) updates up to 500 specific records per request, each with its own data; every object needs an id.
  • updateMany(query, data) applies one MongoDB update ($set, $inc and the rest) to every matching record in batches of up to 500. Call it again while the result's has_more is true.
  • deleteMany(query) permanently removes every record that matches the query.
  • upsert(records, options) creates or updates records by a key you name instead of by id (for example an ID from another system), up to 500 records per request.

Trap: updateMany and deleteMany act on whatever the query matches within the caller's permissions. Run the same query through filter() and check the count before you point either at production data, and never build the query from unvalidated client input. We cover cleanup patterns in our database best practices article.

Auth and the User entity

auth.me()

Returns the signed-in user: id, email, full_name, role, is_verified, disabled, app_id, plus any custom fields on your User schema. To gate a page, check isAuthenticated() first:

const signedIn = await base44.auth.isAuthenticated();
if (!signedIn) {
  // Sends the user to the app's login page, then back here.
  base44.auth.redirectToLogin(window.location.href);
} else {
  const me = await base44.auth.me();
  console.log(me.email, me.role);
}

Trap: the built-in role is either admin or user. For anything finer (manager, viewer, per-company access), you add custom fields to the User entity and reference them in rules as {{user.data.<field>}}. That makes those fields security-critical; see updateMe() below.

The User entity itself (base44.entities.User) has fixed rules: regular users can only read and update their own record, and you create users through the auth module (register(), inviteUser()), not entities.User.create().

auth.loginWithProvider(provider, fromUrl?) and auth.loginViaEmailPassword(email, password)

loginWithProvider() redirects to the provider's login page. 'google' is enabled by default; 'microsoft', 'facebook', 'apple' and 'sso' must be switched on in your app's authentication settings first. loginViaEmailPassword() does not redirect: it returns { access_token, user } and sets the token for later requests, so you can build your own login form. New accounts go through register(), which emails a one-time code, then verifyOtp(). Password resets use resetPasswordRequest() and resetPassword().

auth.logout(redirectUrl?)

Removes the token from local storage and the request headers, then redirects to redirectUrl or reloads the page. It runs in the browser only.

auth.updateMe(data)

Updates the signed-in user's own record. It can change any custom field on your User schema. id, email, full_name, created_date, updated_date, created_by and collaborator_role are off-limits, and changing role requires editor access on the app (docs.base44.com).

Trap: because updateMe() can write any custom User field, a rule keyed on one, such as {"data.company": "{{user.data.company}}"}, is only as strong as that field. Test whether a normal user can change it with updateMe(). If they can, they can move themselves into another company's records. Put a field-level write rule on it, or set it only from a backend function.

Backend functions

functions.invoke(functionName, data?)

Sends a POST request to a backend Deno function and resolves to a response whose data property holds what the function returned. Passing a File as any parameter in data switches the request to multipart/form-data.

const result = await base44.functions.invoke("processPayment", {
  amount: 4900,
  currency: "USD",
  customerId: "cus_abc123",
});
console.log(result.data);

For streaming responses (SSE, NDJSON), PUT, PATCH or DELETE requests, or raw status codes and headers, use base44.functions.fetch(path, init), which returns a native Response.

Behavior notes:

  • Functions execute on Base44's Deno runtime.
  • The first call after an idle period can be slower while the function cold-starts.
  • Called through the SDK, a function receives the signed-in user's credentials. Called by direct HTTP (a webhook, cURL), it has no user context, and Base44's docs say to use asServiceRole for all operations in that case.
  • Maximum execution time is 5 minutes; longer requests are terminated.
  • Functions cannot share state without going through entities or external storage.
  • The ISOLATE_INTERNAL_FAILURE error indicates Deno couldn't load the function; usually a syntax error or unsupported import.
  • Each function has an HTTP endpoint at https://<your-app-domain>/functions/<function-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 a CLI project, each function lives in its own folder (base44/functions/<name>/entry.ts by default) and default-exports a handler. Create the SDK client from the incoming request:

import { createClientFromRequest } from "npm:@base44/sdk";

export default async function (req: Request): Promise<Response> {
  if (req.method !== "POST") {
    return new Response("Method Not Allowed", { status: 405 });
  }

  const base44 = createClientFromRequest(req);
  const user = await base44.auth.me();
  if (!user) {
    return Response.json({ error: "Unauthorized" }, { status: 401 });
  }

  const body = await req.json();
  // ... validate, do work, return ...

  return Response.json({ ok: true });
}

The SDK's client guide shows the same handler wrapped in Deno.serve(async (req) => { ... }); both forms take a standard Request and return a Response.

Imports: load packages with npm: or jsr: specifiers. Some packages will still fail with Unsupported dependency. Test imports before assuming they work.

Secrets: read them with secrets.get("KEY") from the built-in base44:runtime module, inside the handler; a read at module load, before any request runs, returns undefined. Set them with base44 secrets set from the CLI. Do not put secrets in code; the AI agent has read access to your code.

Integrations

Built-in integrations live under base44.integrations.Core and cost integration credits. Base44's credits page says the credits for an action "are calculated after the action runs", so its per-call figures are estimates (docs.base44.com).

integrations.Core.InvokeLLM({ prompt, model?, response_json_schema?, add_context_from_internet?, file_urls? })

Calls a managed LLM and returns a string, or an object when you pass response_json_schema. The credits page lists about 1 integration credit per call on the Automatic model, about 5 on Gemini 3 Flash and about 15 on GPT-5.4.

const summary = await base44.integrations.Core.InvokeLLM({
  prompt: "Summarize this in three bullets: " + text,
  model: "gpt_5_mini",
});

Cost trap: every call burns credits, including a prompt you sent a minute ago. Add your own cache layer (entity-backed or external) for any prompt that repeats.

integrations.Core.GenerateImage({ prompt })

Returns { url } for a PNG about 1,024px on its shorter side, at about 1 integration credit per image. Cap usage per user anyway: an unmetered button spends your credits as fast as anyone can click it.

integrations.Core.SendEmail({ to, subject, body, from_name? })

Sends a plain-text email. Base44's reference describes it as a way to "Send emails to registered users of your app", and its integrations guide says it does not support external mailing lists or attachments. It costs about 1 integration credit per email, or about 2 from a custom email domain. Trap: mail goes out through the platform's managed sender, so 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 workspace integrations

base44.integrations.custom.call(slug, operationId, params) calls an external API that a workspace admin has set up by importing an OpenAPI specification. Calls are proxied through Base44's backend, so credentials are never exposed to the frontend. operationId takes a method:path form such as "get:/contacts". When an API has no OpenAPI spec, or you need full control over the request, a backend function that reads the key with secrets.get() does the same job.

Files

There is no files module. Uploads go through Core integrations.

integrations.Core.UploadFile({ file })

Uploads to public storage and returns { file_url }. Anyone with the URL can open the file. It costs about 1 integration credit per upload.

Limits:

  • Size caps vary by file type. For uploads through UploadFile in a live app, Base44's uploading files guide lists 50MB for images, documents, data files and audio, and 100MB per video. Validate size in the client before you upload.
  • The same guide says "There is no hard storage limit per app."
  • Do not rely on a file's reported MIME type for security; check the contents in a backend function when it matters.

integrations.Core.UploadPrivateFile({ file }) and CreateFileSignedUrl({ file_uri, expires_in })

Private storage. UploadPrivateFile returns a file_uri, and CreateFileSignedUrl turns it into a temporary signed_url that expires after expires_in seconds. Use this pair for anything a stranger should not be able to open: contracts, ID documents, medical files.

const { file_uri } = await base44.integrations.Core.UploadPrivateFile({ file });
// Store file_uri on the record, not a public URL.

const { signed_url } = await base44.integrations.Core.CreateFileSignedUrl({
  file_uri,
  expires_in: 3600, // one hour
});

Assume anyone who can read the record holding a file_uri can request a signed URL for it, and protect that record with a Read rule.

Deleting files

The Core integrations reference lists no method for deleting an uploaded file. Plan as if a public upload stays reachable at its URL after you delete the entity that pointed to it, and put sensitive files in private storage from the start.

Error handling

The SDK throws on network failure and on 4xx/5xx responses. SDK errors are instances of Base44Error, which carries status, message and code:

import { Base44Error } from "@base44/sdk";

try {
  await base44.entities.Task.list();
} catch (error) {
  if (error instanceof Base44Error) {
    console.error(error.status, error.code, error.message);
  } else {
    throw error;
  }
}

Common status codes:

ErrorMeaningFix
401Token expired or invalidRe-auth
403Permission deniedCheck the entity's permission rules (RLS)
404Record or function not foundVerify ID and function name
405Method not allowedFunction routing bug; see fix article
409Conflict (concurrent update)Retry with backoff
429Rate limitedExponential backoff
500Server errorRetry; 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

Leaving an entity's Read rule on All Users. The SDK adds no ownership filter, so list() returns whatever the rule allows.

Treating a frontend filter() as security. Anyone can call list() from the browser console. Row-level security rules are the boundary.

Trusting the client to set sensitive fields. Tier, verification and ownership fields need a field-level write rule or a backend function that sets them server-side.

Reaching for asServiceRole without checking the caller. Service role skips every entity access rule. In a backend function, confirm who is calling with auth.me() and check their access before any service-role read or write.

Keying access rules on a custom User field anyone can rewrite. updateMe() writes custom User fields. Lock the field down before a rule depends on it.

Using integrations.Core.SendEmail for password resets. For built-in email and password login, auth.resetPasswordRequest() already sends the reset email; for anything custom, 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.

Uploading sensitive files with UploadFile. It returns a public URL. Use UploadPrivateFile and short-lived signed URLs.

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. Pointing an AI coding agent at the code does not change that; what Claude Code can and cannot do with a Base44 codebase walks through what eject and GitHub sync hand an agent, and what they leave on the platform.

Reference summary

  • Set a row-level security rule on every entity before launch; an All Users Read rule is a data leak.
  • Use filter() for queries and list() for unfiltered pages; neither one is a security boundary.
  • Put Creator Only (or stricter) rules on Update and Delete; route logic-heavy writes through functions.invoke().
  • Check the caller before every asServiceRole call in a backend function.
  • Lock down custom User fields that access rules depend on.
  • Cache LLM calls; every call costs credits.
  • Upload sensitive files with UploadPrivateFile.
  • Use external email for anything mission-critical.
  • Wrap the SDK in a thin project module for one-place swap during migration.

Want us to audit your SDK usage?

Our $497 production audit grep-walks every SDK call site in your repo, flags open permission rules and unguarded service-role calls, identifies privilege-escalation paths, and delivers a prioritized fix list. Order an audit or book a free 15-minute call.

QUERIES

Frequently asked questions

Q.01What does the Base44 SDK actually do, and why can't I just call REST endpoints?
A.01

The SDK is the JavaScript client for your app's Base44 backend. Its auth module stores the user's token and adds the authorization header to every request, so entity calls run with the signed-in user's permissions and your permission rules apply. Base44 also documents a REST Apps API whose entity endpoints can read, add, change or remove an entity's records, but inside app code the SDK is the path Base44's own docs and generated apps are built on. Use it for anything that needs to keep working through platform updates.

Q.02Where is the SDK installed by default in a Base44 app?
A.02

Inside a Base44-generated app the client is already created and configured; Base44's docs import it from @/api/base44Client, so you install nothing. Outside Base44 you run npm install @base44/sdk and create the client with createClient and your app ID. Either way the client talks to your app's Base44 backend, which is the core lock-in concern documented in our vendor lock-in deep dive.

Q.03Can I use the SDK to call entities from a backend function?
A.03

Yes. In a Base44-hosted backend function you create the client with createClientFromRequest(req). When your frontend calls the function through functions.invoke(), the client acts as the signed-in user, with that user's permissions. For work that needs more, base44.asServiceRole skips every entity access rule, row-level and field-level, so your function must run its own authorization checks first. Service role exists only in Base44-hosted functions, and a function called by direct HTTP, such as a webhook, has no user context at all.

Q.04Why does Entity.list() return data for users who shouldn't see it?
A.04

Because the SDK adds no ownership filter of its own. list() returns every record the entity's Read rule allows, and a Read rule of All Users, or one loosened by a later AI edit, lets any caller read every record. Adding a created_by filter in your frontend does not fix it, because anyone can call list() from the browser console. The fix is a row-level security rule, such as Creator Only on Read, Update and Delete. We cover this in detail in our security hardening checklist.

Q.05What is the rate limit on SDK calls?
A.05

Base44 enforces a soft rate limit that triggers 429 responses on burst traffic but does not publish exact thresholds, and AI integration endpoints tend to throttle sooner than entity endpoints. The platform does not surface limits in headers. Production apps should implement client-side throttling and exponential backoff on 429s, plus an external monitor that catches sustained throttling before users notice.

Q.06Can I extend the SDK with custom methods?
A.06

Not through a plugin system. The SDK's source is public on GitHub (base44/javascript-sdk), but it does not expose a plugin API. The pattern that works is to write a thin wrapper module in your project that adds your conventions (ownership checks, error handling, telemetry) and use the wrapper everywhere instead of the raw SDK. This also gives you a single seam to swap during migration.

NEXT STEP

Order a $497 base44 audit.

Delivered one business day after access (two for apps with 15+ tables or several integrations): 12 components, 110 pass/fail checks and a fix, rebuild or migrate verdict. The $497 fee is credited in full against any fix, build, migration or written-recommendation engagement booked within 30 days. If you ordered the audit for a specific bug and we cannot reproduce it, we refund the fee; otherwise it is non-refundable.