Lesson 7 · Authorization — what may you do?
Locations & access_paths
Permissions aren't just "what" — they're "where." A tree of locations, encoded so one grant covers a whole branch.
Your win: explain how the location hierarchy is modeled, what an
access_path is, and how a single permission grant at one location automatically covers
its entire subtree — with one indexed string comparison.
Locations form a tree
An organization is a hierarchy of places: the org at the root, then brands, regions, centers,
classrooms. The locations table models it with a self-referencing parent pointer:
locations( location_id, name, parent_location_id, access_path, location_type, resource_path )
(There's a parallel tree, location_types, describing the kinds of place —
org, brand, center — but the access model runs on locations.)
The trick: access_path is a materialized path
Notice the access_path column above. Rather than store only the parent pointer, each
location also stores its full ancestry as a string — the location_ids
from the root down to itself, joined by /. This is the materialized path
model of hierarchical data.1 It's computed by a recursive
walk:
WITH RECURSIVE with_locations AS (
SELECT location_id, location_id::TEXT AS access_path -- root: path = own id
FROM locations WHERE parent_location_id IS NULL
UNION ALL
SELECT lo.location_id, (wl.access_path || '/' || lo.location_id) -- child: parent path + "/" + own id
FROM with_locations wl JOIN locations lo ON lo.parent_location_id = wl.location_id
) UPDATE locations SET access_path = with_locations.access_path FROM with_locations WHERE …
Why materialized path: the subtree is one prefix match
Here's the payoff, and it's the single most important idea in this lesson. To find every
location in the subtree under some location L, you don't recurse — you ask for
every location whose access_path starts with L's path:
l1.access_path ~~ (l.access_path || '%') -- ~~ is Postgres LIKE
-- "org/A%" matches org/A, org/A/A1, org/A/A2 — the whole Brand-A branch, in one indexed scan
L automatically covers L and everything
beneath it, because "the subtree of L" is exactly "rows whose access_path
begins with L's." One grant at "Brand A" reaches every center in Brand A — no per-center rows, no
recursive query, just a prefix LIKE (indexed with text_pattern_ops).
Grant high, inherit down.
Which locations is a user attached to?
Two tables connect users to the tree, for two different purposes:
| Table | Meaning |
|---|---|
granted_role_access_path | which location a role grant applies to (Lesson 6) — where a permission is effective |
user_access_paths | which locations a user belongs to (their home places) — migrations/bob/1147 |
Both carry an access_path, so both participate in the same prefix-match subtree
logic. In Lesson 8 we combine them: "does this user hold permission P on a location that covers the
data they're asking for?"
Storing hierarchical data — the materialized path
The tree model this uses, and why prefix-matched paths beat recursion for subtree reads.
→ SitePoint — storing hierarchical data
→ Materialized path, explained ·
in-repo migrations/bob/1124,1144,1157
Check yourself (from memory)
Q1. What does a location's access_path store?
/,
e.g. org/A/A1. Parent pointer alone would need recursion.
Q2. How do you find every location in the subtree under location L?
LIKE (~~), no recursion.
Q3. A permission is granted at "Brand A". Which locations does it cover?
access_path + subtree scoping.parent_location_id (migrations/bob/1124,1144); location_types
is a parallel tree of place-kinds. access_path = the
materialized path: each location stores its full ancestry as a /-joined
string of ids (org/A/A1), backfilled by a recursive CTE (1157).
Subtree = prefix match: the subtree of L is every location where
access_path ~~ (L.access_path || '%') (Postgres LIKE, indexed
text_pattern_ops) — one grant at "Brand A" covers the whole branch, no recursion.
Beats adjacency list (needs recursion) / closure table (extra rows). Two user↔location tables:
granted_role_access_path (where a grant is effective) and user_access_paths
(where a user belongs).LIKE instead of a recursive query. Grant
at the brand, inherit at every center."
1. Hierarchical data models, materialized path. In-repo: migrations/bob/1124,1144,1147,1157.