Skip to main content

JavaScript Environment

Functions execute JavaScript in serverless containers. This page covers the execution environment, available objects, and best practices.

Two Function Forms

A function is written in one of two forms — and you must pick exactly one per file. They are not interchangeable, and you must never mix them. Both forms expose the same platform objectsitem, res, req, me, api, secrets, validationErrors, collectionName, the data-access helpers (get/post/put/patch/del, user.*), and the loggers (log/logInfo/logWarn/logError). The only difference is how you reach them:

  • Script form — those objects are bare globals. There is no export default.
  • Default-export form — those objects are members of the ctx argument: ctx.item, ctx.res, etc.

The two examples below are the same function written each way. The platform objects map 1:1: a bare global x in script form is ctx.x in the default-export form.

Standard JavaScript globals are not on ctx

fetch, console, JSON, Date, Math and other built-in JavaScript globals are available directly in both forms — they are not members of ctx. Call fetch(...), never ctx.fetch(...). Only the platform objects listed above move onto ctx in the default-export form.

Script form (bare globals)

// No export default. Every object is a bare global.
log('Request from:', me?.name ?? 'anonymous');

const existing = await get(`products?filter=sku eq "${req.query.sku}"`);

if (existing.data.length > 0) {
validationErrors.push('SKU already exists'); // non-empty array → 400
} else {
res = {
status: 201,
bodyJson: { ok: true, hasKey: Boolean(secrets?.UPSTREAM_KEY) },
};
}

Default-export form (everything through ctx)

import type { FunctionContext } from '../types';

// `../types` resolves to the generated types.d.ts at your project root.
// This is exactly what `rat` scaffolds for a new TypeScript function.
export default async function (ctx: FunctionContext) {
ctx.log('Request from:', ctx.me?.name ?? 'anonymous');

const existing = await ctx.get(`products?filter=sku eq "${ctx.req.query.sku}"`);

if (existing.data.length > 0) {
ctx.validationErrors.push('SKU already exists');
} else {
ctx.res = {
status: 201,
bodyJson: { ok: true, hasKey: Boolean(ctx.secrets?.UPSTREAM_KEY) },
};
}

return ctx.item; // return a value to replace the item; return nothing to leave it unchanged
}
Never mix the two forms

Pick one form per file and commit to it. Do not combine export default with bare-global assignments.

The moment a file has an export default, the bare globals are dead. Assigning a bare global res, item, validationErrors (or referencing bare me, secrets, req, get, …) — whether at the top level or inside the exported function — has no effect and is silently discarded. The single most common mistake is writing export default (ctx) => { ... } and then setting res = {...} instead of ctx.res = {...}: the response is lost and the function appears to do nothing.

For clarity the rest of this page documents each platform object in script form (bare global). In the default-export form, reach the identical object via ctx. — e.g. resctx.res, get(...)ctx.get(...), secretsctx.secrets. (Standard JavaScript globals like fetch, console, and JSON are the exception — always call them directly, never ctx.fetch.)

Type-checking catches the mistake before deploy

In the typed default-export form this slip is a compile error, not a silent failure: a bare res = {...} inside the function reports Cannot find name 'res'. Type-check your functions with rat lint before deploying and the mix can't get through — whereas at runtime the stray assignment is simply discarded with no error.

Available Objects

Core Objects

ObjectDescriptionAvailable In
itemThe current record being created, updated, or deletedTrigger functions
meCurrent authenticated userAll functions (when authenticated)
apiInformation about the APIAll functions
secretsConfigured secrets (key/value pairs)All functions
reqIncoming HTTP request detailsTrigger & HTTP functions
resResponse object for custom responsesHTTP functions
collectionNameName of the collection being processedTrigger functions
validationErrorsArray for validation errors (returns 400 if non-empty)All functions

The item Object

For trigger functions, item contains the record being processed:

// Access item properties
log('Processing:', item.name);
log('Item ID:', item.id);

// Modify the item (for validation/transformation)
item.slug = item.title.toLowerCase().replace(/\s+/g, '-');
item.updatedAt = new Date().toISOString();

The me Object

Information about the current user:

PropertyDescription
idUser ID (GUID)
emailUser email
nameUser display name
roleIdsArray of role IDs assigned to the user
log('Request by:', me.name);
log('User email:', me.email);
log('User roles:', me.roleIds);

The api Object

Information about the current API:

PropertyDescription
idAPI ID (GUID)
nameAPI name
tenantIdTenant ID (GUID)
log('Running in API:', api.name);
log('Tenant:', api.tenantId);

The req Object

Request details (trigger and HTTP functions):

PropertyDescription
methodHTTP method (GET, POST, PUT, PATCH, DELETE)
headersRequest headers (filtered for security)
queryQuery parameters
bodyRaw body string
bodyJsonParsed JSON body
// Access request data
const method = req.method;
const contentType = req.headers['content-type'];
const page = req.query.page || 1;
const payload = req.bodyJson;

The res Object (HTTP Only)

Set custom responses in HTTP functions:

PropertyDescription
statusHTTP status code (default: 200)
headersResponse headers
bodyJsonJSON response body
bodyPlain text response body
// Return custom response
res = {
status: 201,
headers: { 'X-Custom-Header': 'value' },
bodyJson: { success: true, id: newItem.id }
};

Real-Time Channels

Functions can manage private channels (channels with : in the name) using the built-in HTTP methods:

// Add client to a private channel with send permission
await post('/_rt/join', {
connectionId: req.bodyJson.connectionId,
channel: 'room:123',
readOnly: false // Client can send messages
});

// Add client as read-only listener
await post('/_rt/join', {
connectionId: req.bodyJson.connectionId,
channel: 'announcements:system',
readOnly: true // Client can only receive
});

// Send message to any channel
await post('/_rt/send', {
channel: 'announcements:system',
body: { type: 'notification', text: 'New update available' }
});

Private channels (with :) can only be joined/messaged by functions. The readOnly parameter controls whether the client can send messages after joining.

See Real Time for channel types and client-side usage.

Built-in HTTP Methods

Functions have built-in async methods for API calls:

MethodDescription
get(url)GET request to API endpoint
post(url, data)POST request
put(url, data)PUT request
patch(url, data)PATCH request
del(url)DELETE request
// Get all active products
const products = await get('products?filter=status eq "active"');

// Create a new order
const order = await post('orders', {
customerId: item.customerId,
total: item.total
});

// Update a record
await patch(`customers/${item.customerId}`, {
lastOrderDate: new Date().toISOString()
});

// Delete a record
await del(`temp-items/${item.id}`);

These methods automatically include authentication and use relative URLs within your API.

Two Authentication Contexts

Functions expose two parallel families of HTTP helpers, each backed by a different bearer token:

FamilyMethodsIdentity used
Built-inget, post, put, patch, del, getFileBase64, postFileBase64The function's configured Token Options (None / Current User / Service Account)
User-contextuser.get, user.post, user.put, user.patch, user.delete, user.getFileBase64, user.postFileBase64Always the original triggering user, regardless of Token Options

The two slots are populated independently. Pick the family that matches the identity you actually want the request made under:

// Runs as whatever Token Options is set to on this action.
const result = await get('orders');

// Runs as the user who triggered this function — independent of Token Options.
const userOrders = await user.get('orders');

When user.* has no token

The user.* family relies on a real triggering user being present. If a function runs without an authenticated caller — most commonly a timer function or any other server-initiated execution — the user-context slot is empty, and user.* requests go out with no Authorization header. The downstream API treats them as anonymous, which usually means a 401 against any non-public endpoint.

This is the most common cause of "my timer function gets 401 even though I configured a Service Account":

// Timer function configured with Token Options = Service Account.

// ❌ Wrong — `user.*` ignores Token Options. There is no triggering user
// for a timer, so this goes out unauthenticated and gets 401.
await user.get('orders');

// ✅ Right — the built-in family uses the configured Service Account.
await get('orders');

For trigger and HTTP functions invoked by a real user, user.* does carry that user's identity, and is useful when you want to enforce that user's access permissions inside your function logic.

Token Options and the built-in family

The built-in get/post/etc. use whichever identity was selected on the function action:

  • None — no Authorization header sent. Works only against endpoints that allow anonymous access.
  • Current User — the user who triggered the function. For server-initiated runs there is no triggering user, so this resolves to no token.
  • Service Account — a token minted for the configured service account. Works for both user-triggered and server-triggered runs.

The action's Skip Security Policy and Skip Roles flags, if set, are baked into the token used by both families — they make the configured identity bypass row-level access and role checks, but they do not change which identity is used.

See Trigger Functions and Timer Functions for where to configure these.

External HTTP Requests (fetch)

Use fetch for external API calls:

const response = await fetch('https://api.example.com/data', {
method: 'POST',
headers: {
'Authorization': `Bearer ${secrets.EXTERNAL_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ key: 'value' })
});

const data = await response.json();
Free Tier

The fetch function for external HTTP requests is available on all tiers, including the free tier.

File Operations

MethodDescription
getFileBase64(url)Get file as Base64 string
postFileBase64(url, fileName, content, mimeType)Upload Base64 file
// Download a file
const fileContent = await getFileBase64(`products/${item.id}/image`);

// Upload a file
await postFileBase64(
`products/${item.id}/thumbnail`,
'thumbnail.jpg',
resizedImageBase64,
'image/jpeg'
);

Sequences

Use getSequence(sequenceName) to generate atomic, auto-incrementing numbers. Each call returns the next value in the named sequence, guaranteed to be unique even under concurrent execution.

The sequenceName argument is required — it identifies which sequence to advance. Sequences with different names are independent, so each kind of number gets its own counter.

const orderNumber = await getSequence('orders');
item.orderNumber = orderNumber; // e.g., 1, 2, 3, ...

const invoiceNumber = await getSequence('invoices'); // independent counter

This is useful for:

  • Order numbers — Human-readable sequential IDs
  • Invoice numbers — Sequential numbering for accounting
  • Ticket numbers — Support ticket or queue numbers
  • Any sequential identifier — When UUIDs aren't appropriate
Atomic Guarantees

getSequence(sequenceName) uses database-level sequencing, ensuring no duplicates even when multiple function instances run concurrently.

Secrets

Secrets are securely stored credentials injected into your function.

Configuring Secrets

  1. Create secrets in SettingsSecrets
  2. Select which secrets to inject when configuring your function
  3. Access them via the secrets object

Using Secrets

// Access secrets
const apiKey = secrets.STRIPE_API_KEY;
const webhookSecret = secrets.WEBHOOK_SECRET;

// Use in external calls
const response = await fetch('https://api.stripe.com/v1/charges', {
headers: {
'Authorization': `Bearer ${secrets.STRIPE_SECRET_KEY}`
}
});
warning

Never log secrets or include them in error messages.

Logging

FunctionSeverity
log(...args)Info (multiple arguments supported)
logInfo(message)Info
logWarn(message)Warning
logError(message)Error
log('Processing started');
log('Item:', item.name, 'Status:', item.status); // Multiple args
logInfo('Single info message');
logWarn('Customer has pending balance');
logError('Failed to sync with external service');

Logs include timestamps and milliseconds from execution start.

Context by Function Type

Trigger Functions

// Available: item, me, api, secrets, req, collectionName, validationErrors
log('Processing item:', item.id);
log('Collection:', collectionName);
log('Triggered by:', me.name);
log('Method:', req.method);

// Validate
if (!item.title) {
validationErrors.push('Title is required');
}

// Modify item before save
item.updatedBy = me.id;

Timer Functions

// Available: api, me (service account), secrets
// No item or request context
log('Running scheduled task for API:', api.name);

const ordersRes = await get('orders?filter=status eq "pending"');
for (const order of ordersRes.data) {
// Process each order...
}

HTTP Functions

// Available: req, res, me, api, secrets
const input = req.bodyJson;
const result = processData(input);

res = {
status: 200,
bodyJson: { success: true, result }
};

Error Handling

Validation Errors

Use validationErrors to return user-facing errors. If this array has any items when the function completes, the operation returns 400 Bad Request with the errors in the response body.

if (!item.email) {
validationErrors.push('Email is required');
}

if (item.price < 0) {
validationErrors.push('Price cannot be negative');
}

// Multiple errors are returned together

Response when validation fails:

["Email is required", "Price cannot be negative"]

Runtime Errors vs Validation Errors

TypeExampleReturned to ClientLogged
Validation errorsvalidationErrors.push('...')Yes (400 response)Yes
Runtime errorsundefined.foo, failed fetchNoYes

Runtime errors (like undefined variables or failed external requests) are only logged server-side and not exposed to the client. Use validationErrors for errors you want users to see.

Try-Catch

Handle external failures gracefully:

try {
const external = await fetch(externalUrl);
if (!external.ok) {
logError('External API returned: ' + external.status);
validationErrors.push('Unable to verify with external service');
}
} catch (error) {
logError('External call failed: ' + error.message);
// Decide: add to validationErrors or let it proceed?
}

Filtered Headers

For security, these headers are removed from req.headers:

  • authorization, cookie, host
  • x-forwarded-*, x-azure-*, x-arr-*
  • Other infrastructure headers

Best Practices

  • Use item not body — For trigger functions, the record is in item
  • Use built-in methodsget(), post(), etc. handle auth automatically
  • Use validationErrors for user-facing errors — These are returned in the response
  • Handle external failures with try-catch — Decide whether to block or continue
  • Log important operations — Aids debugging and auditing
  • Keep secrets secure — Never expose in logs or responses