The acting node and elevation
Every invocation of a function acts at exactly one node of your product's organization tree. The records it reads and writes, the mail it sends, the events it publishes, what it is metered against and what its log says are all at that node. This page says which node that is, how a function moves below it, how it manages the tree, and how it may count records it cannot read.
Which node an invocation acts at
| the invocation | acts at |
|---|---|
a route inside [authenticate] (a session) |
the home of the end user's selected membership |
a public [[route]] in config/routes.toml |
the route's org_path |
an event trigger ([[triggers.events]] in function.toml) |
the trigger's org_path |
a cron trigger ([[triggers.cron]]) |
the trigger's org_path |
aircloud functions invoke |
your own node (the membership you act through), or with --at <node> a node at or below it |
A public route, an event trigger and a cron trigger have no end user, so they must declare their node:
[[route]]
prefix = "/api/hooks"
function = "accounts"
org_path = "/crm"
[[triggers.cron]]
schedule = "0 3 * * *"
org_path = "/crm/companies"
[[triggers.events]]
topic = "accounts.opened"
org_path = "/"
aircloud check refuses an entry without one: route.org_path_required, trigger.org_path_required. /, the root,
is a valid choice only when you write it — nothing defaults to it, so acting over every customer of the product is
always visible in your repository. The generators write org_path = "/" on every route and trigger they add; edit it
to act lower. A route to a container runs no function and declares none (route.org_path_not_invocation).
Every node you name is declared
A node exists only when something creates it, so every node your repository names — a public route's or a
trigger's org_path, an [mcp] facet's org_path, an [[authenticate.route]]'s at — must be one each
production activation creates, or one created at run time below a declared parent:
| you may name | because |
|---|---|
/ |
the tenant root always exists |
admin_org_path, default_org_path, the [authenticate.workspaces] parent, every [authenticate] nodes entry, and each of their ancestors |
each production activation creates them (Members and roles) |
any node strictly below the [authenticate.workspaces] parent |
workspace signup creates those at run time (Workspaces) |
aircloud check refuses any other node (org.path_undeclared), so a product never fails at run time on a node
nothing created. To act at a fixed node of your own, declare it:
[authenticate]
nodes = ["/crm", { path = "/crm/companies", display_name = "Empresas" }]
An entry is the node's path, or { path, display_name } to name it for people: each production release sets the
declared name on the node. A node below a fixed node is not declared by it: declare that node too. A node created by a function through
nodes.create outside the workspaces parent exists only once the function has run, so declare it in nodes if
your repository names it. Removing a node from nodes never deletes it: archiving and erasing a node are your
explicit acts (aircloud org nodes archive, aircloud org nodes delete).
A function with no end user is not owner-scoped: it reads every customer's owned records at or below its node, and what it writes is stored at its node (Owner and shared entities). An event trigger receives only the events published at or below its node.
Narrowing — only downward
A function may move to a node at or below the one it entered at, through the guest interface acting-node:
current() answers the node the next call acts at, and narrow(path) moves there. A node above or beside the entry
node is refused (capability.forbidden). Narrowing grants nothing; it only shrinks what the function already
reaches.
import { current, narrow } from "aircloud:functions/acting-node@1.0.0";
// A nightly cron declared at /crm/companies, one client at a time.
for (const company of companies) {
narrow(company.org_path); // every read and write that follows acts at that client's node
// …
}
Moving between nodes inside the entry node's subtree is not widening, so a sweep declared once at a parent narrows to each child in turn.
Managing the tree from a function
A function manages nodes, members, profiles, groups, denies and workspaces through the platform's tree capabilities, with the authority of its acting node, and only through the operations it declares:
[org]
operations = ["nodes.create", "members.invite"]
nodes.create takes {org_path, display_name?, seed?}: a display_name names the new node for people (1 to 100
characters, no control character; org_node_name_invalid otherwise), and nodes.list answers each node's
display_name, and the acting node's as at_display_name, when it has one.
Each name is an area and one of its operations — nodes.create, nodes.archive, members.invite,
members.update, workspaces.create and so on, one per operation the tree capabilities offer; aircloud check
refuses a name that is not one of them (org.operation_unknown), and an undeclared call is refused at run time
(capability.forbidden). A cron declared at / manages the whole tree; one declared at /acme manages
only below /acme. A session function manages members with the end user's own authority, never more.
Archived nodes
When a node is archived (aircloud org nodes archive), everything acting at it or below stops: sessions, public
routes, events, crons and direct invokes are refused with org_node_archived until you unarchive it.
Elevated aggregates
An owned entity keeps each customer's records to that customer. Some answers need a number across all of them —
"1 seat left", "you are 2nd on the waitlist" — without showing anyone else's record. A session function declares
exactly the aggregate it needs in function.toml:
[[elevate]]
entity = "booking"
ops = ["count", "sum", "max"]
filter = ["slot"]
group_by = ["status"]
fields = ["seats"]
scope = "subtree"
| key | what it says |
|---|---|
entity |
the owned entity whose owner scope the aggregate lifts |
ops |
the aggregate functions it may run: count, sum, avg, min, max |
filter |
the fields it may filter on |
group_by |
the fields it may group by |
fields |
the fields sum, avg, min and max may aggregate |
scope |
node: the records stored at the acting node; subtree: the acting node and below |
The function calls aggregate-elevated of the flex-query capability. That one aggregate runs over every owner's
records; every other read of the same invocation stays owner-scoped, and the tree, denies and field policy still
apply. No record and no owner can ever be named: the record id, the entity's owner field and its unique fields
are refused in every role (elevate.field_identifier), so the answer is a number over records nobody can single
out. A call outside the declaration is refused (capability.forbidden), each use is recorded, and it is metered.
aircloud check also refuses an unknown entity (elevate.entity_missing), operation (elevate.op_invalid),
field (elevate.field_unknown) or scope (elevate.scope_invalid).
A function with no end user is not owner-scoped to begin with, so an elevation declared for it lifts nothing.