Praxsuite

Files

Vincent Depassier · August 30, 2026

Files over the Gateway

Your workspace stores files, and your app needs to put them there and get them back. The gateway exposes that under one path:

https://gateway.praxsuite.com/{workspaceId}/files

Calls authenticate like everything else on the gateway: an API key, or an end user's JWT.

Authenticating is not the same as being allowed to open one particular file. That second question has its own answer, and it is the part most apps get wrong — see Who can reach a file.


The operations

Operation

Call

Upload

POST /files/upload — multipart/form-data with a single file field

List

GET /files — id, name, extension, size and creation date. For an end user it returns only the files that user may read

Download

GET /files/{blobId} — streams the content with its real filename

Share link

GET /files/{blobId}/url — a link that opens with no auth header

Delete

DELETE /files/{blobId} — removes the file and its record

Upload returns the blob id, and that id is the whole point: it is what you put in a File-type column when you write a row.

{
  "refs": { "Documents": "…" },
  "mutation": {
    "type": "insert",
    "table": "Documents",
    "values": [{ "Title": "Contract", "Attachment": "<blob-id>" }]
  }
}

Who can reach a file

A file is almost never an island. It is the value of a File column on some row, and that row already has permissions: the caller's role, their column permissions, their row filter. So the file inherits them, and you do not configure anything twice.

Two kinds of caller are judged differently:

Caller

What it may reach

API key — your server, an automation, an AI connector

Every file in the workspace. It is your app's own identity, not a person's.

End user (JWT)

Decided per file, in the order below.

For an end user, in order:

  1. An access list declared on the file settles it. If your app declared grants on resource type file for that blob, they decide — in both directions. Someone outside the list is refused even if they could read a row the file sits on.

  2. The uploader keeps their own file, and a published file is readable by anyone. Uploading and attaching are two calls, and between them the file belongs to no row.

  3. Otherwise the rows that reference it decide. The check runs the very query that person could have sent themselves, so their row filter and column permissions apply unchanged. Only File-type columns create that link: a blob id kept in a text or JSON column references nothing the gateway can see.

The trap: where nothing declares anything about a file, reading it by id is allowed. Inheritance can only answer where there is something to inherit from, and two shapes leave nothing to inherit: no row references the file at all, or the caller holds no scope of any kind on the table that does — which is what an app whose users read through endpoints looks like, because there the app's own credential does the querying and the end users never get table scopes. In both, GET /files/{blobId} and GET /files/{blobId}/url serve the file to any end user signed in to that workspace. The reasoning is that the caller already holds the id, and who handed it to them — a row they read through your app, a link someone pasted — is the one part the gateway cannot see. "We know nothing about this file" is not the same claim as "this file is private".

What stays closed in exactly those cases:

  • The file does not appear in `GET /files`. That listing hands out ids nobody gave the caller, so a file with no declared permission stays out of it.

  • Writing — and therefore deleting — still needs a grant or a writable row. A file no row references can be deleted only by whoever uploaded it.

So if a file must be refused by id, say so: declare a grant on it, or put its id in a File column of a table the caller is scoped to. Refusals then read as 404, not 403 — someone who may not see a file should not be able to confirm it exists by trying ids.

Deleting asks for more than reading. An end user may delete a file only with a manage grant on it, or by being able to write the row that references it.

Everyone sees it, one person edits it

The common shape — a coach uploads exercise gifs, their students watch them, only the coach can replace them:

  1. The coach's app uploads the gif and writes the id into a File column on a row that names its owner.

  2. Give the coach's role write access to that table with a row filter of their own rows; give the students' role read access with a row filter that reaches their coach's rows.

  3. Nothing else. The gif follows the row: students can open it, cannot delete it, and never see another coach's files.

Step 2 is what makes the file private, and it is the first thing to check when a file turns out to be readable by somebody you expected to be refused: with no scope on the table, there is no row permission for the file to inherit.

Use grants on resource type file when the sharing does not follow a row — one file handed to one person, a file shared outside the table that holds it, or a file that must be refused outright to everyone not on the list.

Files that are not rows at all (attachments and generated artefacts, reached by path rather than by blob id) have nothing to inherit from, so an end user needs a signed link for them. That is what the product hands out for those files anyway.


Two things the gateway refuses

Files that belong to another workspace. Every operation resolves the blob within the workspace in the route, so a valid id from somewhere else answers 404. There is no cross-workspace file access to misconfigure.

Raw storage URLs. Every file URL Praxsuite hands out is a Praxsuite URL, and the content is streamed through it. Your callers never receive an address that points at the underlying storage, so access stays revocable and the storage layer is not part of your public contract.

Uploads are also checked against your plan's size limit and an allowed-extension list, and both rejections name the actual limit rather than failing vaguely.


How file URLs come back in query results

When a query returns a File column, the shape of the URL depends on the calling credential's FileUrlMode:

Mode

What you receive

Proxy (default)

A URL that requires the gateway's authentication — and therefore the same per-file check as everything above.

SasUrl

A signed link that opens with no auth header and expires.

PublicUrl

A permanent anonymous URL — only for files you explicitly published. A file that is not published falls back to Proxy, so switching modes cannot publish anything by itself.

FileSasExpiryMinutes sets how long a signed link lasts: 60 minutes by default, 5 minimum, 7 days maximum. Keep it short. Once a signed link leaves your page, its expiry is the only thing still limiting it — the link is a permission, and anyone holding it can open the file without signing in.

`FileUrlMode` belongs to an API key credential, and an end user's JWT does not have one. A query authenticated with a JWT therefore always receives the Proxy shape, whatever the credentials in that workspace are set to. This is why an <img src> pointed straight at a URL that came back in an end user's query result shows nothing: the tag cannot send an authentication header. For an end user, either call GET /files/{blobId}/url, which returns a signed link that opens without one, or publish the file outright when it is something like a logo that everybody is meant to see.

GET /files/{blobId}/url mints that link on demand, capped at 24 hours, and only for a caller who may already read the file. It works for an API key and an end user's JWT alike.

This is set per credential, so a public read-only key can serve images directly while your backend key keeps everything proxied.


Choosing

Would you mind if this URL were forwarded to somebody who is not signed in? If yes, stay on Proxy.

Product photos, avatars and public marketing assets are fine on SasUrl or published outright. Anything a person uploaded about themselves is not.


Next

  • Resource Grants — declaring who may reach one specific file.

  • Gateway Roles — the table and row permissions a file inherits.

  • Credentials and Principals — where FileUrlMode is configured.

  • Mutations — writing a blob id into a row.