> ## Documentation Index
> Fetch the complete documentation index at: https://docs.base44.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Base44 backend functions: Backend functions (Vite SPA)

> source commit is in /SOURCE_COMMIT at the repository root. Edits here are overwritten by

> [!WARNING]
>
> This page is part of an AI coding agent skill and is written for agents, not humans. For the human-readable Base44 docs, see the [developer documentation](https://docs.base44.com/developers).

# Base44 backend functions: Backend functions (Vite SPA)

<overview>
  Backend functions: HTTP handlers in `base44/functions/{functionName}/entry.ts`, for external APIs with no built-in integration. Always prefer built-in integrations when they cover the need. Logic needed by more than one function must live in a `base44/shared/` module they both import — extract it, never copy it between functions.
</overview>

The Core-integration routing rule is an exception: use a backend function for Core methods except `UploadPublicFile`, `UploadPrivateFile` and `CreateFileSignedUrl` (`UploadFile` is the retired spelling of `UploadPublicFile`).

<integration_priority>
  For external-service integrations, in order:

  1. App connectors — when a connector supports the service, use it exclusively (e.g. Google Calendar via its connector).

  2. Backend functions with secrets — when connectors don't cover the service, API keys/secrets.
</integration_priority>

<guest_checkout>
  * Narrow exception to the authentication/service-role rules: public checkout initiation may run without login, including a server-side payment-provider call and creation of a new pending order. Do not report missing authentication solely for that flow when the server selects the payable products, prices and currency from server configuration, provider-owned prices, or catalog records buyers cannot create, update or delete, validates quantities, returns only the new checkout's details, and neither exposes nor changes existing customer/order/entitlement records chosen by the caller. Reading the public catalog through service role does not invalidate this exception. This does not exempt other anonymous writes, messages, paid AI calls or privileged payment operations.
  * Report concrete payment trust-boundary failures: caller-controlled amounts/prices/currency or payment status, access to another customer's records, or fulfillment/pass/access granted before server-side payment-provider verification. A success redirect, a client-supplied session/order id, or the presence of a signature header alone is not proof of payment.
  * When fixing payment findings, preserve intended guest checkout and its successful response shape. Fix pricing, record scoping and payment verification at the relevant boundary; do not require login or a browser-embedded shared secret merely to start checkout.
  * Origin/Referer/Host headers are not caller authentication or proof of payment. Do not add an Origin-equals-Host restriction as a security fix: legitimate custom-domain, platform-domain and preview callers may use different API hosts. Preserve platform app/Host binding and legitimate redirect/CORS allowlists; check both custom-domain and platform-domain guest checkout after changes.
</guest_checkout>

<secrets>
  Before writing a function that needs a secret/API key, check it's set — using an unset secret fails. If missing, ask the user to add it on the app's Dashboard → Secrets page (shown once backend functions are on) and wait for their confirmation before writing code that reads it.
  Secrets: `import { secrets } from "base44:runtime"` → `secrets.get("NAME")`; pre-populated: BASE44\_APP\_ID. Users manage secrets on the Dashboard "Secrets" page (its own sidebar item, shown once backend functions are on), not in Settings.
  Never ask for BASE44\_SERVICE\_TOKEN, BASE44\_SERVICE\_ROLE\_KEY, or similar — those secrets don't exist: auth comes from `createClientFromRequest(req)`, service-role from `base44.asServiceRole`.
</secrets>

<structure_and_validation>
  Code passes lint validation before deployment.
  Rejected: syntax errors, missing imports, unmatched brackets/braces, wrong function structure or return types.
  Accepted (to prevent deployment loops): unused variables, `let` vs `const`, unused catch params.

  Requirements:

  * Handler: `export default async function(req: Request): Promise<Response> { ... }` — the only accepted pattern.
  * Return Response objects, not strings/plain objects.
  * All initialization inside the handler — module-top-level `throw`/`await` = boot error instead of a 500: no try/catch reached, no app logs.
  * Caller errors return 4xx before the work, never `throw` into the catch: 400 bad/missing input (an unparseable body too: `await req.json().catch(() => ({}))`), 401/403 auth, 404 missing record. SDK calls reject instead of returning null (`auth.me()` signed out: 401, `entities.X.get(id)` on a missing id: 404): before the `if (!x)` guard, turn only that status into null (`.catch((e) => (e.status === 404 ? null : Promise.reject(e)))`), or the guard never runs and the catch returns 500; a 429 or outage still rejects. try/catch the rest; the catch's 500 means the function crashed.
  * `await` every Base44 SDK call — `base44.entities.X.list()`, `base44.auth.me()`, `base44.functions.invoke(...)` all return Promises.
  * Validate user auth with `base44.auth.me()` when using the SDK.
  * Exception: use `base44.asServiceRole.integrations.Core` for Core-integration functions. Keep the function narrowly scoped: validate and bound its input, return only the needed result, and do not use the service role for entities or other app data.
  * Declare variables before use; don't redeclare in-scope names (`user`, `req`); matched brackets; syntactically valid regex.
  * Never duplicate logic across functions — extract it to a `base44/shared/` module and import it (`import { calc } from "../../shared/calc.ts"`); own-dir helpers import as `./util.ts`.
  * `base44/` is server-side and never in the client bundle: never import it from a frontend file (the page calls `base44.functions.invoke(...)` instead), and never import `src/` or `@/` from a function.
</structure_and_validation>

<coding_guidelines>
  For external webhooks, browser GET links and OAuth callbacks, use the server-set `X-Base44-App-Url` origin plus `/functions/<functionName>`. Never expose deployment URLs. Preserve callback query parameters and validate OAuth state in the handler.

  * Web-standard APIs over external deps (fetch not Axios, WebSockets API not node-ws).
  * External deps: import with a versioned `npm:` specifier (Base44 SDK: `npm:@base44/sdk@0.8.54`); rewrite `esm.sh` / `unpkg.com` URLs to `npm:`.
  * JavaScript, not TypeScript. Names camelCase only: no spaces, `/`, hyphens.
  * Use `base44:runtime` for secrets and post-response work (see below); no `eval()`/`new Function()`; no WebAssembly from runtime-built bytes.
  * Paginate entity queries, stream large responses — don't load full datasets into memory.
  * Requests are cut at 5min and a Worker holds 6 connections at once: run independent calls in `Promise.allSettled` batches of \~6, never one await per item; one `filter({ id: { $in: ids } })` instead of a get per id; every outbound `fetch` takes `signal: AbortSignal.timeout(ms)` sized to the API (\~10s, more for LLM/media); on 429 back off or fail, never a tight retry loop.
</coding_guidelines>

<post_response_work>
  For work to complete after sending a response (analytics pings, non-critical logging): import `waitUntil` from `base44:runtime` and call `waitUntil(myAsyncOp())`. The function returns the response immediately; the Worker stays alive until the promise settles.

  ```js theme={null}
  import { waitUntil } from "base44:runtime";
  export default async function(req) {
    waitUntil(fetch("https://hooks.example.com/ping", { method: "POST" }));
    return Response.json({ ok: true });
  }
  ```
</post_response_work>

<base44_sdk_usage>
  ```js theme={null}
  import { createClientFromRequest } from 'npm:@base44/sdk@0.8.54';

  export default async function(req) {
    try {
      const base44 = createClientFromRequest(req);
      const user = await base44.auth.me().catch((e) => (e.status === 401 ? null : Promise.reject(e)));
      if (!user) return Response.json({ error: 'Unauthorized' }, { status: 401 });
      const { id } = await req.json().catch(() => ({}));
      if (!id) return Response.json({ error: 'id is required' }, { status: 400 });
      const record = await base44.entities.SampleData.get(id).catch((e) => (e.status === 404 ? null : Promise.reject(e)));
      if (!record) return Response.json({ error: 'Not found' }, { status: 404 });
      const { items } = await base44.entities.SampleData.filter({ status: 'open' }, { sort: '-created_date', limit: 50 });  // user-scoped (app-user token), one page
      const total = await base44.asServiceRole.entities.SampleData.count();  // service-scoped (admin), a number: no records read
      const res = await base44.functions.invoke('myFunction', { foo: 'bar' }); // another backend function
      return Response.json({ record, items, total });
    } catch (error) {
      return Response.json({ error: error.message }, { status: 500 });
    }
  }
  ```

  Prefer user-scoped calls (`base44.entities...`) for frontend/user actions. Use `base44.asServiceRole.integrations.Core` for Core-integration functions. For entities, functions, and non-Core integrations, use `base44.asServiceRole` only when admin-level operations are necessary, after verifying `base44.auth.isAuthenticated()` is true and the business logic allows the elevation. One exception: a deliberately public lookup, such as a visitor checking their own submission, may skip the sign-in check when a long random code (`crypto.randomUUID()`) saved at submission is the check; reject a missing or blank code, match it exactly, and return only the fields needed.
</base44_sdk_usage>

<webhooks>
  Endpoints called without user auth (webhooks): use the service role; validate request authenticity first — anyone can reach the endpoint. Known providers: official signature validation/npm package; otherwise a shared secret as a query param, verified each call.
  Stripe: do the base44 auth — after setting the token from request.headers — before any Stripe signature validation. Web Crypto is async (SubtleCrypto): `await stripe.webhooks.constructEventAsync(body, signature, secret)`; synchronous `constructEvent()` throws "SubtleCryptoProvider cannot be used in a synchronous context".
</webhooks>

<admin_only_functions>
  Admin-only functions (scheduled tasks, functions a workflow or automation calls, admin dashboard ops, maintenance) MUST authenticate via `base44.auth.me()`, verify `user.role === 'admin'`, and return 403 Forbidden otherwise — this stops non-admins from invoking the endpoint directly.
</admin_only_functions>

<frontend_usage>
  Platform V3+: pages/components/layouts use functions only through the SDK:

  ```js theme={null}
  import { base44 } from "@/api/base44Client";
  const response = await base44.functions.invoke('someFunction', {someParam: "someValue"})
  ```

  Response: Axios response object `{data, status, headers}` — your function's data is in `response.data`.

  Parameters only through the payload — URL parameters won't work. Single path only (`/`, no multiple paths).
</frontend_usage>

<dashboard_access>
  Function details, input/output testing, API/webhook usage, endpoint URL: dashboard -> code -> functions -> function\_name.
</dashboard_access>

<testing_and_debugging>
  A function deploys automatically once its file is written. Test it by invoking it — `base44.functions.invoke('myFunction', {param: "value"})` from the app, or its endpoint URL from dashboard → Code → Functions — and read its logs there (the `base44-troubleshooter` skill covers log access); logs catch logical errors even on success. Write (auto-deploys) → invoke → check response/logs → fix (auto-redeploys) → re-test.
</testing_and_debugging>

<example>
  <user_ask>
    Integrate OpenAI's gpt-4o-mini via a backend function: tell the user a joke based on their name.
  </user_ask>

  <assistant>
    I need the OPENAI\_API\_KEY secret first — please add it on the app's Dashboard → Secrets page.
  </assistant>

  <user_ask>
    I set the OPENAI\_API\_KEY secret. Please write the function.
  </user_ask>

  <assistant>
    ```
    // base44/functions/openai/entry.ts
    import { createClientFromRequest } from 'npm:@base44/sdk@0.8.54';
    import OpenAI from 'npm:openai';
    import { secrets } from 'base44:runtime';

    export default async function(req) {
      try {
        const base44 = createClientFromRequest(req);
        const user = await base44.auth.me().catch((e) => (e.status === 401 ? null : Promise.reject(e)));
        if (!user) return Response.json({ error: 'Unauthorized' }, { status: 401 });
        // SDK clients constructed inside the handler — module-level init throws on missing secrets and crashes boot.
        const openai = new OpenAI({ apiKey: secrets.get("OPENAI_API_KEY") });
        const response = await openai.chat.completions.create({
          model: "gpt-4o-mini",
          messages: [{ role: "user", content: 'write a clean, clever joke about the user named ' + user.full_name }]
        });
        return Response.json({ joke: response.choices[0].message.content });
      } catch (error) {
        return Response.json({ error: error.message }, { status: 500 });
      }
    }

    ```

    Created a backend function that generates personalized jokes.
  </assistant>
</example>

<example_2>
  <user_ask>
    Add the ability to export all tasks to a PDF file.
  </user_ask>

  <assistant>
    ```
    // base44/functions/exportTasks/entry.ts
    import { createClientFromRequest } from 'npm:@base44/sdk@0.8.54';
    import { jsPDF } from 'npm:jspdf@4.0.0';

    export default async function(req) {
      try {
        // ...createClientFromRequest(req) + auth.me() + 401 guard, as in base44_sdk_usage...
        const tasks = await base44.entities.Task.filter({});
        const doc = new jsPDF();
        // ...doc.text title/headers + one row per task; doc.addPage() when y > 270...
        const pdfBytes = doc.output('arraybuffer');
        return new Response(pdfBytes, {
          status: 200,
          headers: { 'Content-Type': 'application/pdf', 'Content-Disposition': 'attachment; filename=tasks.pdf' }
        });
      } catch (error) {
        return Response.json({ error: error.message }, { status: 500 });
      }
    }
    ```

    Then in `src/pages/Tasks.jsx`: import `base44` from `@/api/base44Client` and the `Download` icon, and add an "Export PDF" button whose `handleExport` calls `base44.functions.invoke('exportTasks')` and downloads the response blob.

    Added a PDF export button.
  </assistant>
</example_2>

Model values for base44.integrations.Core.InvokeLLM in the generated app:

The SDK reference includes compatibility aliases. Use this catalog for current model IDs and capability constraints.

Omit model (or use "automatic") unless the app needs an explicit model. Automatic selection supports text, uploaded files, and web search.

| model | file\_urls | add\_context\_from\_internet |
| - | - | - |
| gemini\_3\_8\_flash | yes | yes |
| gpt\_6\_luna | yes | no |
| claude-sonnet-5 | yes | no |
| claude\_opus\_5\_5 | yes | no |
| gpt\_6\_sol | yes | no |
| gpt\_6\_astra | yes | no |
| claude\_fable\_5\_1 | yes | no |
| glm\_5\_2 | no | no |

An incompatible explicit model and capability combination raises an error. Non-default models use more integration credits; tell the user when choosing one.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.