Nikhil
03/04/2026, 12:05 AMparent relation for permission inheritance.
## The problem
I need to enforce data access at the Postgres query level. These checks happen on every DB read, so they need to be cheap. I'm considering two approaches and would love feedback on whether there's a better pattern.
### Approach 1: LookupResources
Ask SpiceDB for the set of entity IDs a user can access, then pass those into a WHERE id IN (...) clause.
Concern: For tables with large row counts, the returned ID set can be massive — potentially too large for a WHERE IN clause and too slow for SpiceDB to compute. At our current scale the set is small, but I don't want to commit to a pattern that breaks as we grow.
### Approach 2: Resolve accessible anchors, filter via joins
Ask SpiceDB for the anchor resources (locations, companies, enterprises) a user can access — a much smaller set — then join from the target entity up to the anchor and filter there.
Example: to fetch messages, resolve the user's accessible companies, then query messages → threads → locations → companies WHERE company_id IN (...).
Concerns:
- Join complexity: every entity needs a join path back to an anchor, and that join runs on every query.
- Ambiguous join paths: some entities can reach an anchor through multiple paths (e.g. a thread relates to a location, a candidate, and an application). Which path do I join through?
### What I'm leaning toward
Approach 1 feels much easier to implement, but the where in limit is a serious concernNikhil
03/04/2026, 12:10 AMdefinition user {}
definition resource_group {
relation parent: resource_group
relation rel_enterprise_write: user
relation rel_enterprise_read: user
relation rel_company_write: user
relation rel_company_read: user
relation rel_location_write: user
relation rel_location_read: user
permission enterprise_read = rel_enterprise_read + parent->enterprise_read
permission enterprise_write = rel_enterprise_write + parent->enterprise_write
permission company_read = rel_company_read + parent->company_read
permission company_write = rel_company_write + parent->company_write
permission location_read = rel_location_read + parent->location_read
permission location_write = rel_location_write + parent->location_write
}
definition enterprise {
relation resource_group: resource_group
permission write = resource_group->enterprise_write
permission read = resource_group->enterprise_read
}
definition company {
relation resource_group: resource_group
relation parent: enterprise
permission write = parent->write + resource_group->enterprise_write + resource_group->company_write
permission read = parent->read + resource_group->enterprise_read + resource_group->company_read
}
definition location {
relation resource_group: resource_group
relation parent: company
permission write = parent->write + resource_group->enterprise_write + resource_group->company_write + resource_group->location_write
permission read = parent->read + resource_group->enterprise_read + resource_group->company_read + resource_group->location_read
}Nikhil
03/04/2026, 12:10 AMdefinition department {
relation parent: company
permission write = parent->write
permission read = parent->read
}
definition job {
relation parent: location | company | department
permission write = parent->write
permission read = parent->read
}
definition application {
relation parent: company | candidate | job
permission write = parent->write
permission read = parent->read
}
definition candidate {
relation parent: enterprise
permission write = parent->write
permission read = parent->read
}
definition thread {
relation parent: location | candidate | application
permission write = parent->write
permission read = parent->read
}
definition message {
relation parent: thread
permission write = parent->write
permission read = parent->read
}yetitwo
03/04/2026, 8:57 PMyetitwo
03/04/2026, 9:00 PM