Security rules on collections
Declare a per-collection access level for each of read, write, create and delete: allow (public), auth (signed in), role:<name> (that role or one that inherits it), owner (the document's creator), or deny.
curl -s -X POST $SUBSTRIO_URL/v1/_rules -H "Authorization: Bearer $SUBSTRIO_KEY" \
-H 'content-type: application/json' \
-d '{"rules":[{"collection":"posts","read":"allow","write":"role:editor","delete":"role:admin"},
{"collection":"admin_logs","read":"role:admin","write":"deny","create":"deny","delete":"deny"}]}'
An operation a rule does not name is not public by default; only an explicit allow is. Set the narrowest rule each collection needs, and close every collection you have not declared a rule for with a rule on * (every collection without a rule of its own) set to deny. See permissions for the full layer order (roles, rules, record access).
Roles and declared permissions
Assign a built-in role (guest, viewer, editor, admin, owner) with role.assign, or declare your own named permissions (permissions.set, up to 64, each able to imply others) when a role is too coarse.
{"name": "substrio_role_assign", "arguments": {"id": 42, "role": "editor"}}
Check a specific permission from a function with db.requirePermission, or db.requirePersonOrPermission when a handler should also pass the row's own owner. Give a role or a permission only to the members who need it; a custom role's inherits should name the narrowest role that already covers what the new role needs.
Route gates: public and auth
Declare each function route's gate in its routes entry: "GET /orders" is public (the default); "GET /orders auth" refuses a caller with no session at once, with 401, before the function runs.
["GET /public-page", "POST /orders auth", "DELETE /orders/:id auth"]
Gate a route auth whenever its handler reads request.auth or acts on behalf of the caller; leave it public only for routes meant to answer with no session.
Verified email and 2FA
Call db.requireVerified(request.auth) in a handler whose need for a proven address depends on data it reads; it answers 401 for nobody signed in and 403 EMAIL_VERIFICATION_REQUIRED for a session that has not verified its email.
An account can require 2FA of its members; a member who has not enrolled enrols at their next sign-in (account.mfa.set). Require 2FA for any account whose keys reach billing or administrative operations.
Outbound fetch
A sandbox's outbound requests are fenced by default: fetch() from a sandbox is held in its outbox (sandbox.outbox) unless the host matches a pattern you added with sandbox.allow.add.
{"name": "substrio_sandbox_allow_add", "arguments": {"name": "dev", "pattern": "api.stripe.com"}}
Allow only the hosts a sandbox's functions actually call; a request to an unlisted host waits in the outbox and is never sent.
Secrets
Never put a credential in function source. Write it once with secret.write and read it back as env.<KEY>; a value is never read back by secret.list, only its name.
{"name": "substrio_secret_write", "arguments": {"key": "STRIPE_KEY", "value": "sk_live_..."}}
See best-practices/keys for the keys least-privilege applies to as well.