Skip to content

Permissions and Relationships

Set up file permissions and GraphQL relationships for personal and community-shared files

storage permissions relationships Hasura GraphQL communities multi-tenant guide

This guide walks through two common file-access patterns: personal files (users access only their own) and community files (shared across a group via a junction table and membership). Both patterns build on Hasura permissions applied to the storage.files table.

The simplest pattern — users can upload, view, and delete only their own files in a dedicated bucket.

Go to the Storage page in the dashboard and click New Bucket in the sidebar. Name it personal.

Create bucket

Open Storage → Permissions in the dashboard to reach the permission grid. For each action below, click the cell for the user role and configure it in the rule editor. The JSON equivalent is shown below each rule — you can also paste it directly into the rule editor’s JSON view instead of building the rule visually.

  1. Select With custom check and add a rule: bucket_id equals personal.
  2. Under Uploader identity, enable the Prefill uploaded_by_user_id with X-Hasura-User-Id toggle — this automatically tags every upload with uploaded_by_user_id = X-Hasura-User-Id.

upload files permissions

Equivalent JSON:

{"bucket_id": {"_eq": "personal"}}
// Upload a file to the personal bucket
const { body } = await nhost.storage.uploadFiles({
'bucket-id': 'personal',
'file[]': [file],
})
const uploadedFile = body.processedFiles?.[0]

Because metadata lives in Hasura, you can query files via GraphQL — permissions are applied automatically:

const response = await nhost.graphql.request<{
files: Array<{ id: string; name: string; size: number; mimeType: string; createdAt: string }>
}>({
query: `query MyFiles {
files(where: { bucketId: { _eq: "personal" } }, order_by: { createdAt: desc }) {
id
name
size
mimeType
createdAt
}
}`,
})
// Only returns files uploaded by the current user (enforced by permissions)
const files = response.body.data?.files ?? []

A more advanced pattern where files are shared with groups. Users join communities, upload files to a shared communities storage bucket, and associate files with communities through a junction table. Access is controlled by membership — only community members can see the community’s files.

erDiagram
communities ||--o{ community_members : has
communities ||--o{ community_files : has
community_files }o--|| storage_files : references
community_members }o--|| auth_users : belongs_to
storage_files }o--|| auth_users : uploaded_by
communities {
uuid id PK
text name
text description
}
community_members {
uuid id PK
uuid user_id FK
uuid community_id FK
timestamptz joined_at
}
community_files {
uuid id PK
uuid file_id FK
uuid community_id FK
}
storage_files {
uuid id PK
text name
uuid bucket_id FK
uuid uploaded_by_user_id FK
}
auth_users {
uuid id PK
text display_name
text email
}

Go to the Storage page in the dashboard and click New Bucket in the sidebar. Name it communities.

Then create the required tables (via the Dashboard or a migration):

-- Communities
CREATE TABLE public.communities (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL UNIQUE,
description TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Membership junction
CREATE TABLE public.community_members (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
community_id UUID NOT NULL REFERENCES public.communities(id) ON DELETE CASCADE,
joined_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(user_id, community_id)
);
-- File ↔ community junction
CREATE TABLE public.community_files (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
file_id UUID NOT NULL REFERENCES storage.files(id) ON DELETE CASCADE,
community_id UUID NOT NULL REFERENCES public.communities(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

On the Database page sidebar, click the menu on each table and choose Edit Relationships. Under Suggested Relationships, every foreign key is listed — click Add and then Create Relationship. Do this for each table below so the relationships appear in the GraphQL API.

Suggested Relationships

FromNameTypeTo
communitiesmembersArraycommunity_members
communitiescommunity_filesArraycommunity_files
community_memberscommunityObjectcommunities
community_membersuserObjectauth.users
community_filescommunityObjectcommunities
community_filesfileObjectstorage.files
storage.filescommunity_filesArraycommunity_files

One more relationship links each file to the user who uploaded it. In the Database sidebar, switch to the storage schema, open the files table’s Edit Relationships, and click Relationship +:

  • Relationship Name: uploadedByUser
  • From Source: default / storage / files
  • To Reference: default / auth / users
  • Relationship Type: Object Relationship
  • Source ColumnReference Column: uploaded_by_user_idid

Click Create Relationship.

uploadedByUser relationship form

On the Database page, open the communities table and click Permissions. For the user role, configure Select: choose Without any checks and select all columns. All communities are visible so anyone can browse and join.

communities Select permission

On the Database page, open community_membersPermissions for the user role and configure each operation below.

Choose Without any checks and select all columns. Anyone can see who’s a member of any community.

community_members Select permission

On the Database page, open community_filesPermissions for the user role and configure each operation below.

Only members of the file’s community can see it:

  1. Select With custom check.
  2. Pick relationship community, then pick relationship members, then add condition: user_id equals X-Hasura-User-Id.
  3. Allow all columns.

community_files Select permission

Equivalent JSON:

{"community": {"members": {"user_id": {"_eq": "X-Hasura-User-Id"}}}}

Configure the Storage permissions for the user role: Upload, Download, and Delete each get their own rule.

Click the Upload cell for the user role:

  1. Select With custom check and add a rule: bucket_id is in default, personal, communities.
  2. Under Uploader identity, enable the Prefill uploaded_by_user_id with X-Hasura-User-Id toggle.

storage.files Upload permission

Equivalent JSON:

{"bucket_id": {"_in": ["default", "personal", "communities"]}}
// Join a community
await nhost.graphql.request({
query: `mutation JoinCommunity($communityId: uuid!) {
insert_community_members_one(object: { community_id: $communityId }) {
id
}
}`,
variables: { communityId },
})
// Leave a community
await nhost.graphql.request({
query: `mutation LeaveCommunity($communityId: uuid!, $userId: uuid!) {
delete_community_members(where: {
community_id: { _eq: $communityId },
user_id: { _eq: $userId }
}) {
affected_rows
}
}`,
variables: { communityId, userId },
})

The insert permission’s column preset automatically sets user_id to the authenticated user, and the check constraint prevents users from adding memberships for other users.

// 1. Upload the file to the communities bucket
const uploadResponse = await nhost.storage.uploadFiles({
'bucket-id': 'communities',
'file[]': [file],
})
const uploadedFile = uploadResponse.body.processedFiles?.[0]
if (!uploadedFile?.id) throw new Error('Upload failed')
// 2. Associate the file with a community
await nhost.graphql.request({
query: `mutation AddCommunityFile($fileId: uuid!, $communityId: uuid!) {
insert_community_files_one(object: {
file_id: $fileId,
community_id: $communityId
}) {
id
}
}`,
variables: { fileId: uploadedFile.id, communityId },
})
const response = await nhost.graphql.request<{
community_files: Array<{
id: string
file: { id: string; name: string; size: number; mimeType: string; uploadedByUser: { displayName: string } | null }
}>
}>({
query: `query GetCommunityFiles($communityId: uuid!) {
community_files(where: { community_id: { _eq: $communityId } }) {
id
file {
id
name
size
mimeType
uploadedByUser {
displayName
}
}
}
}`,
variables: { communityId },
})
// Permissions automatically filter to communities the user is a member of
const communityFiles = response.body.data?.community_files ?? []
// Remove the junction record
await nhost.graphql.request({
query: `mutation RemoveCommunityFile($id: uuid!) {
delete_community_files_by_pk(id: $id) {
id
}
}`,
variables: { id: communityFileId },
})
// Optionally delete the file from storage entirely
await nhost.storage.deleteFile(fileId)

The ON DELETE CASCADE on community_files.file_id automatically removes all community associations when the storage file is deleted.