Skip to content

Contexts and tokens

Every File Service call acts in one context (a tenant and one of its applications) and carries a token for the Files API. This page explains both, and which combinations the service accepts. It is for backend developers and for anyone reviewing the security model.

Every route about files names its context in the path: /tenants/:tenant_id/applications/:application_id/…. This site writes that prefix as P, so P/uploads is /tenants/:tenant_id/applications/:application_id/uploads under https://files.centralcodestation.com/api/v1.

Everything lives inside one context: folders, files, settings, quotas, share links and the audit log. An id from another context answers 404, as if it did not exist. Two applications of the same tenant share nothing but the tenant’s quota.

A few routes are about the caller or the tenant instead:

Route Answers
GET /me The signed-in user, and whether the Identity Server reports them as a platform administrator
GET /me/tenants The user’s active tenants
GET /tenants/:tenant_id/applications The applications of a tenant the caller may use, each with files_enabled
GET P/me/capabilities The caller’s roles and expanded permission keys in the context
GET P/settings/public What an uploader needs: allowed types, sizes, the named upload policies
GET /health Liveness; no token
User token Machine token
Who A signed-in user, through your backend Your backend client itself
How you get it Token exchange of the user’s token for your API (User tokens) client_credentials with your client’s id and secret
Scope files:access files:manage, files:read or files:config.manage
What it may do What the user’s permissions allow, item by item (Permissions) Everything its scope grants in its context, whatever the entries say
Lifetime At most 10 minutes Until expires_in; the Node client renews it

The service tells them apart by the token itself: a token whose subject is the client (no user session) is a machine token. Both must have the audience files-api.

Scope Grants
files:manage Every action on every file and folder of its context, plus purging, owners and entries: uploads, metadata, versions, URLs, permissions, shares, trash, search
files:read view and download: reads, search and URL minting only
files:config.manage The application’s settings, taxonomies, usage and audit log; no content

A machine client never gets the role matrix: changing what roles may do is for people on the console. Prefer one client per job: your API holds files:manage, a deployment script files:config.manage.

Two values name the Files API, and they are easy to mix up.

Value Where it appears
https://files.centralcodestation.com/api The resource identifier: what you send as resource= when you ask the Identity Server for a token
files-api The audience: what the token’s aud claim says. Only the service reads it

Sending files-api as resource= fails at the Identity Server (invalid_target). A token for any other audience fails at the service with invalid_token.

  • A client of a tenant application works only in that tenant and application. Any other context answers client_context_mismatch. That is the normal case for your backend.
  • A client of a platform application (such as Chat’s) works in every tenant application that has Files switched on, and passes the context with every call. What Chat stores this way is on Media in Chat.
  • A user must be an active member of the tenant (not_tenant_member) with access to the application (no_application_access).
  • An application that switched Files off answers files_access_not_granted on every route.
  • A suspended tenant keeps reading but cannot upload: new tickets answer tenant_suspended.

A multi-tenant backend constructs one Node client without a default context and passes { tenantId, applicationId } as the last argument of every call. A per-call context wins over the client’s default; a call with neither rejects with FilesContextError before any request is made.

await files.folders.create({ parent_id: 'root', name: 'Media' }, { tenantId: 'fitness-co', applicationId: 'fitness-app' });
await files.files.update(fileId, { title: 'Beach' }, { ifMatch: 4 }, { tenantId, applicationId }); // options first, context last

A machine call is your backend’s own. Two fields say which user it was for:

  • owner_id on a ticket or a folder: the user the item belongs to. It must be an active member of the context. Without it the file belongs to acting_user_id, or else to no user.
  • acting_user_id on a write: the user recorded on the audit row. The service checks that the user is a member, nothing more: your backend’s own rule has already run.

To check what a user may do before you act for them, ask the service: see Permissions from code.