Implementing Document-Level Access Control in Appwrite
Learn how to implement Document-Level Access Control Lists (ACLs) in Appwrite to prevent IDOR vulnerabilities and ensure strict data isolation between users.
05 Dec 2025, 11:25 UTC

The Problem: Preventing Cross-User Data Leaks
When building multi-tenant applications, a common failure point is the "Insecure Direct Object Reference" (IDOR). This occurs when a system relies on the client to provide a document ID without verifying if the requesting user actually owns or has permission to access that specific record. Relying solely on collection-level permissions often forces developers to create separate collections for every user or implement complex server-side filtering logic to prevent User A from reading User B's data.
The solution is Document-Level Access Control Lists (ACLs). By shifting permissions from the collection to the individual document, Appwrite enforces data boundaries at the API gateway, ensuring that the database only returns records the requester is explicitly authorized to see.
The Smallest Suitable Design
To secure a resource where users can only manage their own data, the most efficient architecture is a single collection configured for document-level permissions. This avoids the overhead of managing hundreds of collections while maintaining strict isolation.
Configuration Requirements
- Collection Setting: Permissions must be set to "Document Level". In this mode, the collection-level rules act as a fallback or a baseline, but the specific permissions attached to each document take precedence.
- Permission Strings: Appwrite uses a specific string format for roles:
user:[USER_ID],team:[TEAM_ID], orany. - Action Types: Permissions are granularly defined as
read,create,update, anddelete.
Trust and Data Boundaries
The trust boundary exists between the Client SDK and the Appwrite API Gateway. The client provides a session cookie or JWT, which the gateway validates against the Authentication service.
When a request for a document arrives, the system does not simply execute the query. It intercepts the request and injects a permission filter based on the authenticated identity. If the user's ID is not present in the document's permission array for the requested action, the gateway rejects the request before it reaches the database engine, returning a 403 Forbidden error.
Implementation Example
Consider a "Private Notes" application. The goal is to allow the creator to edit the note and a specific "Manager" team to read it.
Document Creation Configuration
When creating the document via the SDK, you must define the permissions array. Run this from your client-side application using a logged-in user session:
// Example using Appwrite Web SDK
const promise = databases.createDocument(
'[DATABASE_ID]',
'[COLLECTION_ID]',
'unique()',
{
content: 'This is a private note',
category: 'work'
},
[
Permission.read(Role.team('[MANAGER_TEAM_ID]')),
Permission.readWrite(Role.user('[CURRENT_USER_ID]'))
]
);
Operational Checks
To verify the boundary is working, perform these three checks:
- Authorized Access: Log in as
[CURRENT_USER_ID]and attempt to update the document. Expected:200 OK. - Unauthorized Access: Log in as a different user not in the Manager team. Attempt to read the document. Expected:
403 Forbidden. - Role Access: Log in as a member of
[MANAGER_TEAM_ID]. Attempt to read the document. Expected:200 OK. Attempt to update the document. Expected:403 Forbidden.
Failure Modes and Limitations
Permission Resolution Overhead
As the number of teams assigned to a single document increases, the time required to resolve permissions during a listDocuments call can increase. If a document is shared with hundreds of individual users rather than a single team, query performance may degrade.
The 'Any' Role Risk
Adding Permission.read(Role.any()) to a document effectively makes it public. This bypasses all authentication checks for that specific document. This should be used exclusively for public profiles or shared assets.
Orphaned Documents
If a user is deleted from the system, documents where they were the sole permission holder remain in the database but become inaccessible to everyone (including the original owner's ghost record). A cleanup strategy using a Server SDK (with API Key bypass) is required to reassign or delete these documents.
Design Evolution: When to Change
This document-level design is optimal for most user-centric apps. However, you should move to a different architecture if:
- Global Read Requirements: If 90% of your documents are public and only 10% are private, it is more performant to use Collection-level permissions set to "Public Read" and override specific documents with restrictive permissions.
- Complex Hierarchy: If permissions depend on a complex organizational tree (e.g., Regional Manager > District Manager > Store Manager), you may need to implement a custom permission mapping table and use a Server-side Function to validate access.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.