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 objects — item, 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
ctxargument: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.
ctxfetch, 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
}
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. res → ctx.res, get(...) → ctx.get(...), secrets → ctx.secrets. (Standard JavaScript globals like fetch, console, and JSON are the exception — always call them directly, never ctx.fetch.)
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
| Object | Description | Available In |
|---|---|---|
item | The current record being created, updated, or deleted | Trigger functions |
me | Current authenticated user | All functions (when authenticated) |
api | Information about the API | All functions |
secrets | Configured secrets (key/value pairs) | All functions |
req | Incoming HTTP request details | Trigger & HTTP functions |
res | Response object for custom responses | HTTP functions |
collectionName | Name of the collection being processed | Trigger functions |
validationErrors | Array 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:
| Property | Description |
|---|---|
id | User ID (GUID) |
email | User email |
name | User display name |
roleIds | Array 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:
| Property | Description |
|---|---|
id | API ID (GUID) |
name | API name |
tenantId | Tenant ID (GUID) |
log('Running in API:', api.name);
log('Tenant:', api.tenantId);
The req Object
Request details (trigger and HTTP functions):
| Property | Description |
|---|---|
method | HTTP method (GET, POST, PUT, PATCH, DELETE) |
headers | Request headers (filtered for security) |
query | Query parameters |
body | Raw body string |
bodyJson | Parsed 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:
| Property | Description |
|---|---|
status | HTTP status code (default: 200) |
headers | Response headers |
bodyJson | JSON response body |
body | Plain 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:
| Method | Description |
|---|---|
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:
| Family | Methods | Identity used |
|---|---|---|
| Built-in | get, post, put, patch, del, getFileBase64, postFileBase64 | The function's configured Token Options (None / Current User / Service Account) |
| User-context | user.get, user.post, user.put, user.patch, user.delete, user.getFileBase64, user.postFileBase64 | Always 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
Authorizationheader 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();
The fetch function for external HTTP requests is available on all tiers, including the free tier.
File Operations
| Method | Description |
|---|---|
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
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
- Create secrets in Settings → Secrets
- Select which secrets to inject when configuring your function
- Access them via the
secretsobject
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}`
}
});
Never log secrets or include them in error messages.
Logging
| Function | Severity |
|---|---|
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
| Type | Example | Returned to Client | Logged |
|---|---|---|---|
| Validation errors | validationErrors.push('...') | Yes (400 response) | Yes |
| Runtime errors | undefined.foo, failed fetch | No | Yes |
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,hostx-forwarded-*,x-azure-*,x-arr-*- Other infrastructure headers
Best Practices
- Use
itemnotbody— For trigger functions, the record is initem - Use built-in methods —
get(),post(), etc. handle auth automatically - Use
validationErrorsfor 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