Security and permissions

Gate collections with security rules, gate function routes by sign-in, require a verified email or 2FA where it matters, and keep secrets and outbound calls scoped.

Docs menu

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.