An access control list is a list attached to a resource naming which users or roles may access it and what each is allowed to do. The attachment is the idea: where role systems hang permissions on people, an ACL hangs them on the object — every record its own guest list, every entry (an access control entry) a subject paired with its rights. It is one of computing’s oldest security constructs, and in application backends it is how “only Ada and the editors can touch this document” becomes a field instead of a feature.
Key takeaways
| Question | Answer |
|---|---|
| The structure | Object → list of entries · each entry = subject + permissions |
| The three meanings | Network traffic filters · filesystem permissions · per-record app ACLs |
| vs. RBAC | ACL answers “who can touch this object?” — RBAC “what can this role do?” |
| The scaling fix | Role entries + default ACLs + class-level rules for the common case |
| The iron rule | Evaluated server-side, on every request — never in the client |
One object’s guest list
Document "Q3 roadmap" — ACL
┌────────────────────┬───────┬───────┐
│ subject │ read │ write │
├────────────────────┼───────┼───────┤
│ user usr-8fk2 (Ada)│ yes │ yes │ ← owner
│ user usr-2mq7 (Bob)│ yes │ — │ ← individually granted
│ role editors │ yes │ yes │ ← a role as one entry
│ public (everyone) │ — │ — │ ← default: closed
└────────────────────┴───────┴───────┘
As data, on the record itself:
{ "title": "Q3 roadmap",
"ACL": { "usr-8fk2": { "read": true, "write": true },
"usr-2mq7": { "read": true },
"role:editors": { "read": true, "write": true } } }
Writing that guest list in application code:
// JavaScript / Node.js — Back4app JS SDK
// A per-object ACL: this document's own guest list
const doc = new Parse.Object('Document');
doc.set('title', 'Q3 roadmap');
const acl = new Parse.ACL(currentUser); // owner: read + write
acl.setReadAccess(reviewerId, true); // one user: read only
acl.setRoleWriteAccess('editors', true); // a role as an entry
acl.setPublicReadAccess(false); // everyone else: nothing
doc.setACL(acl);
await doc.save(); // enforced server-side on every future request // Flutter / Dart — Back4app Flutter SDK
// A per-object ACL: this document's own guest list
final doc = ParseObject('Document')..set('title', 'Q3 roadmap');
final acl = ParseACL(owner: currentUser); // owner: read + write
acl.setReadAccess(userId: reviewerId, allowed: true); // one user: read only
acl.setRoleWriteAccess('editors', true); // a role as an entry
acl.setPublicReadAccess(allowed: false); // everyone else: nothing
doc.setACL(acl);
await doc.save(); // enforced server-side on every future request // iOS / Swift — Back4app Swift SDK
// A per-object ACL: this document's own guest list
var doc = Document()
doc.title = "Q3 roadmap"
var acl = ParseACL()
acl.setReadAccess(user: currentUser, value: true) // owner: read…
acl.setWriteAccess(user: currentUser, value: true) // …and write
acl.setReadAccess(objectId: reviewerId, value: true) // one user: read only
acl.setWriteAccess(roleName: "editors", value: true) // a role as an entry
acl.publicRead = false // everyone else: nothing
doc.ACL = acl
try await doc.save() // enforced server-side on every future request // Android / Kotlin — Back4app Android SDK
// A per-object ACL: this document's own guest list
val doc = ParseObject("Document")
doc.put("title", "Q3 roadmap")
val acl = ParseACL(ParseUser.getCurrentUser()) // owner: read + write
acl.setReadAccess(reviewerId, true) // one user: read only
acl.setRoleWriteAccess("editors", true) // a role as an entry
acl.publicReadAccess = false // everyone else: nothing
doc.acl = acl
doc.save() // enforced server-side on every future request The three meanings of “ACL”
Most explanations pick one silently; the term genuinely names three mechanisms:
| Family | Attached to | An entry looks like | Evaluated by |
|---|---|---|---|
| Networking ACLs | Router/firewall interfaces | Allow/deny rule on IPs, ports, protocol — ordered, first match wins, implicit deny last | Network devices |
| Filesystem ACLs | Files and directories | user:ada:rw- — extending owner/group/other (POSIX acl(5)) | The operating system |
| Application ACLs | Rows, documents, objects | User/role → read/write flags on the record | Your backend, per request |
They share the shape — a list of subject-permission entries guarding a resource — and differ in everything else. This article’s home is the third meaning: the per-record permission lists of application backends, the least-covered and, for product developers, the most-used.
How a request is evaluated: the two-gate model
Layering is how mature systems reconcile coarse and fine control: class-level permissions state the policy for the whole category (“only authenticated users; only moderators delete”), and the per-object ACL decides the individual record. A request must pass both gates — which means a forgotten ACL can’t open what the class rule closed, and a generous class rule still can’t expose a locked object. The same layering logic appears one level down as row-level security when the database itself enforces the row predicate.
ACL vs. RBAC vs. ABAC
| ACL | RBAC | ABAC | |
|---|---|---|---|
| Permissions attach to | Each object | Roles assigned to users | Rules over attributes |
| Native question | Who can touch this object? | What can this role do? | Is this access allowed in context? |
| Granularity | Finest — per record, per user | Coarse — per function | Arbitrary — per condition |
| Admin cost | Grows with objects × subjects | Grows with roles | Grows with rule complexity |
| Audit “who sees X?” | Trivial — read X’s list | Indirect — expand roles | Hard — evaluate rules |
| Audit “what can Ada see?” | Hard — scan all objects | Trivial — read her roles | Hard |
| Weakness | Sprawl | Role explosion, no per-object nuance | Opaque policy debugging |
The honest answer is composition, not competition: roles handle access that follows job function; ACLs handle the per-object decisions roles can’t express (“this draft, these two reviewers”); attribute rules step in when context matters (time, tenant, state). The practical hinge between the first two is the role-entry ACE — an ACL line whose subject is a role — which keeps object-level control while delegating membership churn to the role system.
The scaling problem — and the mitigation ladder
Naive per-object ACLs grow as N objects × M subjects: a million documents each listing individual users means every hire, departure, and reorganization edits lists scattered across the dataset — the “hard to manage” every textbook mentions, made concrete. The mitigation ladder, in the order to climb it: role entries (one ACE covers a changing population; membership updates in one place); default ACLs (each new object born with owner-read/write and the right role entries — the application analog of POSIX default ACLs on directories); class-level rules for the common case, reserving per-object lists for exceptions; and, at relationship-heavy scale, graph-based authorization (ReBAC) that derives access from relationships instead of storing lists at all. Systems that skip the ladder don’t abandon ACLs — they drown in them.
Enforcement: server-side or not at all
An ACL enforced in the client is a suggestion. Hiding buttons, filtering lists in JavaScript, or trusting the app to send only permitted IDs all fail the same way: the attacker edits the request, not the UI — increment /documents/41 to /documents/42 and read someone else’s record. That failure class — broken object-level authorization, the top entry in OWASP’s API security list — is precisely what per-object ACLs exist to close, and the OWASP guidance is blunt: authorization checks run server-side, per request, per object; possessing access to a type of object never implies access to every object of that type. The evaluation belongs in the data layer, where no client path can route around it.
Common use cases
- User-generated content — each post, file, or note owned by its creator, shared record by record.
- Document collaboration — per-document viewer/editor lists; the share dialog is an ACL editor wearing UX.
- Multi-user records with exceptions — the HR case: the record’s subject reads it, their manager writes it, the auditors role reads everything.
- Tenant and team scoping — role entries per team on shared classes, with per-object grants for cross-team exceptions.
- Private-by-default apps — messaging, health, finance: every object closed at creation, opened only by explicit entries.
Should you use ACLs or roles? A decision matrix
| Situation | Reach for |
|---|---|
| Access follows job function across many records | Roles (RBAC) |
| Each record needs its own sharing decisions | ACLs |
| Both patterns at once (most real apps) | Class rules + role-entry ACLs |
| ”Everyone can read, owner can write” | ACL public-read flag + owner entry |
| Rules depend on context (time, state, tenant) | Attribute conditions above the ACL |
| Deep relationship logic (org charts, nested groups) | ReBAC-style systems |
Limitations and trade-offs
- Sprawl is the default trajectory. Without role entries and defaults, per-object lists become unauditable confetti; the mitigation ladder is not optional at scale.
- “What can this user access?” is the expensive query. ACLs optimize the per-object audit; the per-subject inventory requires scanning or secondary indexes.
- Wrong defaults are silent breaches. An object created public-readable stays public until noticed; default ACLs deserve the same review as code.
- Performance rides the check. Every read filters by ACL; the evaluation must be indexed and enforced in the data layer, not bolted on per endpoint.
- ACLs authorize; they don’t authenticate. The list is only as good as the identity presented to it — sessions and tokens are the upstream dependency.
ACLs on Back4app
Back4app is an open-source Backend-as-a-Service (BaaS) platform that combines a managed database, auto-generated REST and GraphQL APIs, authentication, file storage, and Cloud Code serverless functions. ACLs here are a first-class field: every object carries one, the code tabs above are the complete API — owner defaults, per-user grants, role entries, public flags — and enforcement happens in Back4app on every REST, GraphQL, and Live Query request, so real-time subscriptions respect the same guest lists as queries. The two-gate model ships intact: class-level permissions set the category policy in the dashboard, per-object ACLs refine it record by record, and a default-ACL setting makes new objects private-by-owner from birth. The scaling ladder is built in — roles are objects you manage like any other data — leaving the design decisions, not the enforcement machinery, as your share of the work.
Frequently asked questions
What is an ACL in simple terms?
A guest list attached to each resource: it names who may access that specific object and what each of them may do — Ada can read and write, Bob can only read, everyone else stays out. The list travels with the object, so every object can have different rules.
What is an example of an ACL?
A document record whose ACL reads: owner — read and write; the reviewers role — read; public — no access. In a database that is literally a field on the row or document; in a filesystem it is metadata on the file; on a router it is an ordered list of allow/deny traffic rules.
What is an access control entry (ACE)?
One line of the list: a subject (a user, role, or "everyone") paired with the permissions granted or denied to it. An ACL is simply an ordered collection of ACEs attached to one resource.
What are the types of ACLs?
Three families share the name: networking ACLs (ordered traffic filters on routers and firewalls), filesystem ACLs (per-file permission lists extending owner/group/other), and application or database ACLs (per-record permission lists in your data layer). In backend development, the third is usually the one meant.
What is the difference between ACL and RBAC?
Direction of attachment. An ACL hangs permissions on each resource, per subject — ideal when individual objects need individual decisions. RBAC hangs permissions on roles and assigns users to them — ideal when access follows job function across many resources. Real systems combine them: roles for the broad strokes, ACLs for per-object exceptions.
How do ACLs work in a database?
Each row or document carries (or references) its own permission list — typically an ACL field mapping user IDs and role names to read/write flags. The database or backend evaluates it on every operation, which pairs naturally with row-level security and per-table permission layers.
What is the difference between an ACL and a capability list?
Two views of the same access matrix: an ACL is a column — stored with the object, listing its subjects — while a capability list is a row — stored with the subject, listing its objects. ACLs make "who can touch this object?" instantly auditable; capabilities make "what can this user touch?" easy but revocation harder.
Why don't ACLs scale on their own?
Because the bookkeeping grows as objects × subjects: every hire, departure, and team change means editing lists scattered across millions of objects. The mitigations are role entries (one ACE covers a changing group), default ACLs applied at creation, and class-level rules handling the common case so per-object lists handle only exceptions.
What is the difference between per-object and per-class permissions?
Granularity. Per-class (or per-table) permissions gate a whole category — "only logged-in users may query Documents." Per-object ACLs gate one record — "only Ada may read this document." Layered systems check the class gate first, then the object's ACL; a request must pass both.