Building block · Functions

Serverless functions, no separate authorizer to build.

Write your own logic as a versioned JavaScript function. Call it as your own synchronous endpoint, or trigger it automatically on a data event: no server to run, no separate hosting to manage.

The alternative

What you'd otherwise build.

The function is the easy part. Its authorization story is what actually costs you time.

  • Wire up your own authorizer. An API Gateway custom authorizer, Cognito, Auth0, or an IAM policy, bolted on top of the function layer as a separate concern.
  • Manage its own credential. A service credential for what the function is allowed to touch, tracked and rotated apart from the policy governing everything else.
  • Keep the two in sync, forever. As your app grows, prove the function’s access still matches the policy every other caller follows.

What's actually different

Your function runs as a declared principal.

Runs as a declared principal

Your function runs as its own declared principal, a scoped identity with its own permissions: every call it makes to your data is enforced by the exact same access scopes the equivalent REST call would get, not a separately-managed service credential. Writes commit atomically: everything the script did lands, or none of it does.

blueprint.yaml: a trigger, scoped to one script
triggers:
  on-case-note-update:
    firingSource: { schemaName: case_note, event: UPDATE }
    scriptRef: { name: notify-on-status-change, version: latest }
    fields: [status]
    allowedActions: [records:r:case_note]
    dataScope: { 'scope:org': ['${{ input.scope.org }}'] }

Most serverless platforms bolt authorization onto the function layer as a separate concern from the policy governing the rest of your data. Here it's the same policy, not a second one.

Isolate your own clients, orgs, or anything else

Give a function access to exactly one client, one org, or any other axis your product needs. It can never read or write outside that boundary, no matter what the call itself says: leave the scope off and the request is rejected outright, not silently widened. Org and client are two reserved names, registered the same way as anything else you define.

TypeScript SDK: a client-scoped script execution
// clientId is the id of a client entity you already created.
const { token } = await client.auth.mintToken({
  scope: {
    allowedActions: ['scripts:x:notify-on-status-change'],
    dataScope: { 'scope:client': [clientId] },
  },
});
const scoped = new VectrosClient({ token, environment: 'production' });

// The script runs as this credential: every write it makes must name
// that same boundary, or it's rejected outright, just like a direct call.
await scoped.scripts.executeScript({
  scriptRef: { name: 'notify-on-status-change', version: 'latest' },
  input: { caseId: 'case_123' },
});

See the full access-control mechanics: roles, credentials, audit →

What this isn't. Not general-purpose compute where you bring your own container or language. It's a JavaScript sandbox with host functions for records, documents, and folders instead, and real execution-time and resource limits apply.

See it in a real app

A script, gated by the exact role that can run it.

The RAVV reference app's case-creation flow runs as a stored script, gated by scripts:x:create-case, a qualified action granted only on the two roles that actually create cases, never a bare scripts:x that would run anything. Its writes commit atomically: the case record and its folder land together, or neither does.

Read: the RAVV stack →

This is one of five building blocks Vectros is built from.