Calls and reads between apps

Call a function or read a query that another app offers, with no HTTP and no shared code.

Docs menu

An app calls a function another app offers (substrio.apps.<app>.<entry>(...args)), or reads a query another app offers (substrio.apps.<app>.query.<name>(...args)). Both sides must say so: the owner offers, the user declares the use. Each app sets only its own word.

The word

In substrio.toml, sent by substrio deploy (operation apps.calls.set, PUT /v1/_app_calls; read back with apps.calls.get, GET /v1/_app_calls):

# The owner (app "people")
[apps.offer]
peopleByEmail = ["shop", "@account"]        # the function, offered to app "shop" and every app of this account

[apps.query.byEmail]
collection = "users"
where = ["email"]                           # equality on each field, bound to argument i; each declared indexed
select = ["name", "email"]                  # the only fields a row answers
to = ["shop"]
limit = 100                                 # optional: at most this many rows (absent: every match)

# The user (app "shop")
[apps.use]
people = ["peopleByEmail"]

[apps.use_query]
people = ["byEmail"]                        # or { byEmail = ["name"] } to read only some selected fields

"@account" offers to every app of the owner's own account. The document is validated whole: a bad one is refused with the path of the bad member and the word already in force stays. A word is stored before it is answered, so a crash after the answer keeps it.

Naming an app that does not exist yet is allowed (deploy order is free): the answer carries "warnings": [{"code": "APP_USE_UNKNOWN_APP", "app": "<slug>"}].

Using it

// Held once at the top of the file: deploys and runs as fast as a call made inside the handler.
const byEmail = substrio.apps.people.query.byEmail;

module.exports = async function (request) {
  const rows = byEmail('[email protected]');                        // [{ name: 'Ann', email: '[email protected]' }]
  const person = substrio.apps.people.peopleByEmail('[email protected]');
  return Response.json({ rows, person });
};
  • A read runs straight on the owner's store as an index seek, with no code of the owner run. Its rows are plain objects in the reader's own heap, holding only the selected fields.
  • A call runs the owner's function in the owner's own isolate, with the owner's limits. Arguments and the answer must be plain data.
  • An app that names no other app has no substrio.apps at all (typeof substrio.apps === 'undefined').
  • A sandbox reaches the other app's sandbox of the same name, never its live data. A test app reaches only test apps.
  • A change to either side's word, or a deploy of either app, applies to the next call or read, including one held in a variable.
  • Every call and read is counted on both apps (APP_CALLS).

Refusals

A refused call or read throws an Error whose name and code are the code below, with a message naming the app.

code when
app_call_unknown_app / app_query_unknown_app no app has that slug (never made, or deleted)
app_call_unavailable / app_query_unavailable the app exists but is suspended, or its database could not be opened
app_call_not_offered / app_query_not_offered the owner offers no such entry or query
app_call_not_granted / app_query_not_granted offered, but not to this app, or this app no longer declares it
app_query_bad_argument not one argument per where field, or an argument that is not a string, number or boolean
app_query_not_servable the query's collection is gone, or a where field is no longer declared indexed
app_query_field_not_exposed the reader names a field the owner's offer does not select
app_call_value_not_copyable an argument or the answer is not plain data (a function, promise, class instance, cycle)
app_call_not_local the owner runs where this call cannot reach it
cpu_timeout / memory_exceeded the owner's own CPU or heap limit ended the call (the caller goes on)

Refusals at apps.calls.set (each answered with its error-table status):

code when
INVALID_ARGUMENT the document is malformed (the detail names the path)
COLLECTION_NOT_FOUND an offered query names a collection that does not exist
APP_QUERY_NOT_INDEXED a where field (other than _id) is not declared indexed or unique
APP_QUERY_FIELD_NOT_EXPOSED use_query names a field the owner's granted offer does not select