Skip to content

Permissions

storage permissions access control Hasura authorization roles security

File permissions are managed through Hasura’s permission system on the storage.files table. The same role-based, row-level permissions used for your application data control who can upload, download, and delete files.

Permissions follow a Zero Trust model — by default no role has access. Access must be explicitly granted.

OperationHasura PermissionRequired Columns
Uploadinsert on storage.filesid must be granted. Optionally: bucket_id, name, size, mime_type
Downloadselect on storage.filesAll columns must be granted
Deletedelete on storage.files

When a user makes a request to the Storage API:

  1. The Storage service extracts the JWT from the Authorization header
  2. The JWT contains the user’s ID, role, and any custom claims
  3. Storage forwards these as Hasura session variables (X-Hasura-User-Id, X-Hasura-Role, etc.)
  4. Hasura evaluates the permission rules for the storage.files table
  5. If the permission check passes, the operation proceeds

This means the same permission variables available in your GraphQL permissions also work for storage:

  • X-Hasura-User-Id — the authenticated user’s ID
  • X-Hasura-Role — the user’s role
  • Custom claims like X-Hasura-Org-Id (see Permission Variables)

Private Files — Users Access Only Their Own Files

Section titled “Private Files — Users Access Only Their Own Files”

The most common pattern. Users can upload, download, and delete only files they own, in a specific bucket.

Row check: bucket_id equals personal

Column presets: uploaded_by_user_id set to X-Hasura-User-Id

SettingValue
Roleuser
Columnsid, bucket_id, name, size, mime_type
Row check{"bucket_id": {"_eq": "personal"}}
Column presetuploaded_by_user_id = X-Hasura-User-Id

The column preset automatically sets uploaded_by_user_id from the session, ensuring every file is tagged with its uploader.

Files in a public bucket can be downloaded by anyone (including public role), but only authenticated users can upload.

SettingValue
Roleuser
Columnsid, bucket_id, name, size, mime_type
Row check{"bucket_id": {"_eq": "public"}}
Column presetuploaded_by_user_id = X-Hasura-User-Id

Multi-Tenant — Organization-Scoped Files

Section titled “Multi-Tenant — Organization-Scoped Files”

For multi-tenant applications where files belong to an organization. This uses a custom claim X-Hasura-Org-Id (see Permission Variables).

This example assumes you have a relationship from storage.files to your organizations table through a custom org_id column in your file metadata or through a linking table.

A simpler approach uses the file’s custom metadata JSONB column to store the organization ID, and a Hasura computed field or relationship to evaluate it.

Alternatively, you can create a dedicated organization_files table that references storage.files:

CREATE TABLE public.organization_files (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
file_id uuid NOT NULL REFERENCES storage.files(id) ON DELETE CASCADE,
org_id uuid NOT NULL REFERENCES public.organizations(id)
);

Then set permissions on this linking table using X-Hasura-Org-Id, and configure a relationship from storage.files to organization_files. For the storage permissions themselves:

SettingValue
Roleuser
Columnsid, bucket_id, name, size, mime_type
Row check{"bucket_id": {"_eq": "org-files"}}
Column presetuploaded_by_user_id = X-Hasura-User-Id

Different roles get different levels of access. For example, editor can upload and delete, while viewer can only download.

Insert:

SettingValue
Roleeditor
Columnsid, bucket_id, name, size, mime_type
Column presetuploaded_by_user_id = X-Hasura-User-Id

Select: All columns, no row restriction.

Delete: {"uploaded_by_user_id": {"_eq": "X-Hasura-User-Id"}} (editors can only delete their own uploads).

  • Always use column presets for uploaded_by_user_id on insert permissions. This prevents users from impersonating others.
  • Restrict bucket_id in row checks to control which buckets a role can access.
  • Use the public role for files that should be accessible without authentication.
  • Combine bucket restrictions with ownership checks for fine-grained control.
  • Test permissions by signing in as different users and verifying access is correctly scoped.