React Forum
Blog & Guide Responses
Apr 11, 2023 · 04:00 PM

[Deep Dive #53] Security Scope Runebooks - discussion

Started by Jules Kwon
blogsecurityscopescapabilitiesledgersrouting

Deep Dive #53 treats security as a routing primitive: define a scope in the shell, let component routes request capabilities, and render the decision (allow/deny/fallback) as evidence.

The runebook idea is appealing because it's repeatable: requests, evaluation, recording, redirects, warnings. It's audit-friendly because it lives at the route boundary.

Read the post

Does your app have a single scope per shell, or do scopes stack per group/segment? How do you represent capabilities in a way that can be rendered and screenshot-debugged (strings, structured ids, versioned scopes)? Do you prefer deny-by-default with fallbacks, or allow-by-default with warnings recorded into the ledger?

Comments (10)

Back to latest
Dmitri Kline
Apr 11, 2023 · 04:06 PM

The best part is moving checks out of leaf components. Scattered checks are impossible to migrate.

Related: The Route-First Core Concept.

Maya Henson
Apr 11, 2023 · 04:16 PM

Rendering allow/deny is surprisingly calming. Users don't feel 'broken app', they feel 'policy'.

But you have to make fallback UX good or it just becomes an error boundary with extra steps.

Salma Qureshi
Apr 11, 2023 · 04:28 PM

We used a scope id + last decision marker and it made audit logs line up with UI instantly:

tsx
const ledger = useSecurityLedger('scope:admin');
return <div data-security-scope={ledger.read('scopeId')} data-security-decisions={String(ledger.read('decisions').length)} />;
Keira Santos
Apr 11, 2023 · 04:44 PM

Counterpoint: capability strings can sprawl ('cap:ops:override' becomes 'cap:ops:override:urgent').

I'd want a versioned scope contract so capabilities can be renamed without breaking the whole app.

Priya Liao
Apr 11, 2023 · 04:58 PM

This deep dive also feels like a continuation of client trust rails: the UI should never pretend it has authority it doesn't have.

Related: Client Trust Rails (Deep Dive #25).

Rowan Pierce
Apr 11, 2023 · 05:12 PM

We ended up with stacked scopes: shell scope for identity, group scope for product area, track scope for risky actions.

If everything lives in one scope, it becomes too broad to audit. If it's too granular, it becomes impossible to reason about.

Theo Bennett
Apr 11, 2023 · 05:28 PM

Capability representation we liked: structured ids + a printable label, so the UI can render it without string parsing.

tsx
type Capability = { id: string; label: string; version: string };

export function useCapabilityRequest(scopeId: string, cap: Capability) {
  const ledger = useSecurityLedger(scopeId);
  ledger.write("requested", [...ledger.read("requested"), cap]);
  return ledger.read("granted").some((g) => g.id === cap.id && g.version === cap.version);
}

Rendering label + version in the denial state made support tickets way easier.

Inez Park
Apr 11, 2023 · 05:46 PM

Deny-by-default + fallback has been better for us, but only if fallback is treated as a real route posture (not a generic error).

We render a distinct "policy fallback" screen with a clear recovery path.

Related: Security and Safety.

Lina Ortega
Apr 11, 2023 · 06:03 PM

Counterpoint: rendering allow/deny evidence can leak information. If an attacker can see "deny:cap:admin:export", you've told them what exists.

We ended up redacting capability ids in prod and only rendering a scope decision code.

Jules Kwon
Apr 11, 2023 · 06:09 PM

Good call. We landed on: render the decision + a short hash in prod, render full ids in dev/QA.

The key thing is the decision still exists as evidence; the detail level can be posture-dependent.

Maya Henson
Apr 11, 2023 · 06:24 PM

Also: this becomes *much* easier if you make capability requests part of the route story instead of ad hoc button handlers.

When it's tied to navigation intent, you can reason about it as a transition instead of a random check.