ByeBuy.ai
BUILD YOUR ESCAPE ROUTE · ✦ CURSOR · HOST IT · ◫ SUPABASE · CONNECT IT · ↯ RELAY · BUILD YOUR ESCAPE ROUTE · ✦ CURSOR · HOST IT · ◫ SUPABASE · CONNECT IT · ↯ RELAY ·
CURRICULUM
← BYEBUY NOTES

September 12, 2026

ROLES, OWNERSHIP, AND SAFE ACCOUNT FEATURES

Roles, Ownership, and Safe Account Features

You can now tell identity from permission and specify a sign-in flow without hand-rolling cryptography. The remaining failure is subtler: collapsing every access question into one boolean. Real apps juggle four different kinds of "who may do this" at once — and an AI left unsupervised will flatten all four into isAdmin.

Four access concepts, not one flag

  • System roles describe a person's broad job: reader, organizer, admin. Coarse and few. Useful for gating *kinds* of action ("only admins open the ops console").
  • Record ownership describes a fact about one row: watchlist_item.owner_id = usr_41. Fine-grained and per-record. Decides *which* things a person may touch.
  • Team membership describes shared access: Maya and Daniel co-organize a festival series, so both may edit the series page without either being a system admin. Membership can change without changing anyone's role.
  • Service / agent credentials describe non-human actors: the nightly refresh job, a deployment script, an AI worker. They authenticate too — with narrow keys or workload identities — and their permissions should be the smallest that complete their job.

The anti-pattern: a single isAdmin boolean consulted everywhere. It starts innocently ("admins can do anything, it is just v1") and ends with an admin SDK key in the browser, a refresh job running as a superuser, and no way to answer "which humans saw whose data?" When you see an AI propose if (user.isAdmin) return allow, stop the line. Ask: *which* of the four concepts is this decision actually about?

Research Desk rules, stated exactly

Apply all four to the running application:

1. Anyone can read public demo briefs. No session required. This is intentionally public data — the enforcement point is simply "this route serves only the public demo corpus, never private rows." 2. Only the account owner can read and write their own watchlist. watchlist.owner_id must equal the session identity, checked on every route *and* in the database policy. Maya's session never implies access to Daniel's rows. 3. A narrow server-side refresh job can write source snapshots. The scheduled worker uses its own service credential, and its policy permits exactly one action — writing fresh provider snapshots to the snapshot table. It cannot read watchlists, cannot impersonate users, cannot open admin tooling. 4. An admin has audited access to operational tooling, not universal access through the browser. Admins can view queue health or re-run a failed refresh from an ops surface that writes an audit entry (who, what, when, why). They do not get a master key that bypasses ownership from the normal UI.

Notice how each rule names *who*, *what*, *where enforced*, and *what is recorded*. That is the authorization matrix from Lesson 47.1 grown up. If a rule cannot name its enforcement point, it is not a rule yet.

These boundaries matter beyond tidiness. Authentication and session weaknesses — overly broad tokens, missing ownership checks, service keys with human privileges — recur throughout OWASP Authentication Failures. Read that page as the "why" behind every narrow scope in this lesson.

Enforcement belongs near the data

A frontend filter is never a privacy boundary. myWatchlist.filter(item => item.owner_id === me) still downloads everyone's rows to the browser — View Source is not a vault, as Class 45 warned.

Ownership must be enforced where bypass is impossible: in the API route *and* in the data layer. Conceptually, this is row-level policy: the database itself refuses to return or modify rows the caller does not own, regardless of which route, script, or AI-generated query asks.

In practical terms, for a managed Postgres/auth stack this means:

  • The API route verifies the session and passes the caller identity to the data layer — never a caller-supplied owner_id.
  • The table policy states the rule once, near the data: "a caller may select/insert/update/delete watchlist rows where owner_id equals their identity; the refresh service identity may write snapshot rows and nothing else."
  • A direct database query with a forged identity fails exactly like a forged API request fails. There is no back door through "just query Supabase from the browser with the anon key and filter client-side."

Tell your AI this explicitly: "Enforce ownership in the route handler and in the database policy. No client-side filtering as an access control. Prove both layers deny." If the platform supports it, the database policy is the backstop that survives the next three AI refactors of the route code.

Deletion and export, in product terms

Accounts accumulate data, and users will eventually ask two questions: "give me my data" and "delete my data." Answer both before you need to, in plain product language:

  • Know what you hold: sign-in identity, watchlist rows, saved briefs, logs referencing the account, snapshots the refresh job wrote (which are *source* data, not the user's private profile — do not confuse the two).
  • Export: what the user receives (their watchlist, their profile fields), in what format, and how long it takes. Export contains *their* rows only — never another user's, never internal audit trails.
  • Deletion: what is deleted (profile, watchlist rows), what is retained for a stated operational reason (e.g. anonymized audit entries, billing records where applicable), and for how long. Say the retention reason out loud; "we keep everything just in case" is not a policy.
  • Background data is not the user: deleting Maya's account removes her watchlist, not the public company snapshots the refresh job maintains for everyone. Deleting Daniel's organizer account does not delete events attendees already hold tickets for — those need their own stated rule.

Put this in your spec now, even for v1. Retrofitting deletion onto a schema with no ownership columns and logs full of personal data is the kind of rewrite that Lesson 53 exists to prevent.

The AI review prompt that earns its keep

Paste this after any AI change that touches user-owned data. Require file-and-line citations, not reassurance:

Then require the four policy tests — the minimum proof set for every owned-record route:

1. Owner allowed: Maya deletes her own watchlist item → success (200/204), row gone, audit/log event written. 2. Different user denied: Daniel, signed in, deletes Maya's item ID → 403, row untouched. This must be a real second identity, not a mocked allow = true helper. 3. Anonymous denied: no session at all → 401/redirect, row untouched. Catches routes where the session check was "temporarily" skipped. 4. Authorized service allowed only on its narrow job: the refresh job writes a snapshot → success; the same credential reading a watchlist or deleting a user row → denied.

Beware the test that mocks the authorization helper to always return true — Lesson 50.4's cautionary tale. All checks pass; any user can edit anyone's watchlist. Integration tests with two real identities plus a database policy that independently denies are the fix. One passing test with a mocked gatekeeper proves nothing.

Exercise and finish line

Extend your AUTHORIZATION-MATRIX.md with a column (or companion note) for *service actors*: the refresh job, any AI worker, deployment scripts. Each gets the same five columns — actor, resource, action, enforcement, test — with deliberately tiny permissions.

Your finish line: four passing policy tests per owned-record route (owner / other / anonymous / service-narrow), plus a review note from the prompt above citing exact files and lines. If any test forges identity by passing owner_id in the body rather than establishing it from the session, rewrite the test — it is testing the lock by handing over the key.

Check your understanding

1. Why is a single isAdmin flag dangerous when a refresh job and human admins both exist? 2. A route returns only the current user's rows because the frontend filters the full list. What is wrong, and where should the rule live instead? 3. Which of the four policy tests catches a mocked authorization helper, and why?

You now hold the full Class 47 outcome: identity separated from permission, a specified sign-in flow with managed auth, and ownership enforced near the data with proof. Class 48 builds directly on this foundation — turning these boundaries into a threat model, validation rules, and rate limits that survive contact with real users.

ARTICLE DISCUSSION

JOIN THE
CONVERSATION.

0 COMMENTS

BYEBUY ACCOUNT ACCESS

Sign in

Use your account to save routes and make the catalogue yours.

Enter your email and we’ll send a secure sign-in link and code.

NEW ROUTES ADDED WEEKLY · 9,235 CATALOGUE ENTRIES · BUILD · DEPLOY · QUERY · STACK · SAY BYE TO BUY · NEW ROUTES ADDED WEEKLY · 9,235 CATALOGUE ENTRIES · BUILD · DEPLOY · QUERY · STACK · SAY BYE TO BUY ·