Scope tiers (global / service / user / device)

How scripts, block rules, and breakpoint patterns fold across four scopes — global, per-service-group, per-user, and per-device — server-side in effective_settings_for_device (device overrides user overrides service overrides global, additive union), authored via the dashboard scope selector or the MCP service- and user-tier writers.

Scripts, block rules, mocks, and breakpoint patterns all answer the same question — which config applies to a given device? — and they all answer it the same way: with four scopes that fold together server-side. A rule can live at the Global tier (the whole fleet), the Service tier (every device that has applied a service group), the User tier (every device one person owns), or the Device tier (one device only), and the backend merges all four into the single effective set a capture engine actually runs.

This page covers the model once, so the per-feature pages (Scripts, Block rules, Breakpoints) don't each re-explain it.

The four scopes

ScopeStored inApplies toAuthor it on
Globalsettings_global.dataEvery device in the fleetSettings → ‹Scripts / Blocks / Breakpoints›
Serviceservice_groups.data (per group)Every device that has applied that service group (via applied_service_groups)Settings → Services → ‹group›, or set_*_service over MCP
Usersettings_user.data (keyed by the owner's account id)Every device a single user ownsthe same page, with the scope selector set to a user
Devicesettings_device.dataOne device onlyDevices → ‹device› → ‹Scripts / Blocks / Breakpoints›

Global is the fleet-wide baseline. Service is a shared layer keyed to a service group — it applies to every device that has applied that group, so a fix authored once for "the DoorDash group" reaches every device working that service. User is the per-person layer that follows you across all your own devices without touching anyone else's. Device is the most specific — a single machine's own overrides.

Precedence: device > user > service > global

The four lists are an additive union, not a replacement: the effective set for a device is global ∪ service ∪ user ∪ device, with entries de-duped by id. When two scopes carry an entry with the same id, the more specific scope wins — device overrides user overrides service overrides global. Entries with distinct ids all coexist; nothing at a lower tier is dropped just because a higher tier exists.

Multiple service groups on one device — the tiebreak. A device can have applied more than one service group. When that happens the service-tier lists are folded in a stable, deterministic order: by the group's name (alphabetical). So if two groups carry an entry with the same id, the one whose group name sorts later wins within the service tier — and the device / user tiers still override the merged service result. The order is fixed by name (not by apply time or id) so the resolved set is reproducible.

globalserviceuserdeviceeffective setfor this devicefleet-widedevices on this groupall your devicesthis device onlyunion, de-duped by idmore specific wins: device › user › service › global

So a device with no overrides of its own still runs every global script, every script attached to a service group it has applied, and every script its owner set at the user tier. Add a device-tier entry that reuses a global entry's id and that one device runs your variant while the rest of the fleet keeps the global one.

This is the same fold the connection-type chain uses (device → user → global → vpn) — the difference is that connection type is a single most-specific-wins value, whereas scripts/blocks/breakpoints are lists that union.

Authoring each tier

From the dashboard

On the Scripts, Blocks, and Breakpoints pages a scope selector picks which tier you're editing:

  • Global — the page's default. Edits apply to the whole fleet.
  • Service — under Settings → Services → ‹group›, author the shared layer for every device that has applied that service group.
  • User — pick a user, and you're editing the per-person tier for every device they own.
  • Device — open the tab under Devices → ‹device› to edit that one machine.

The per-device tab annotates how many global, service, and user entries are also in effect, so you always see the full effective picture, not just the tier you're editing. Edits at any tier fan out over Realtime — no reconnect.

From MCP / BusyBro

Every tier has a read and a writer. The reads return the lists plus the resolved union; the writers each replace one tier's list (full-list replace, additive against the others):

ToolTierWhat it doesGating
get_scriptsGlobal + (with a device) the owner user's + that device's scripts + the effective unionscripts:view
set_scripts_globalGlobalReplace the global scriptsscripts:edit · admin · confirm
set_scripts_serviceServiceReplace a service group's scriptsscripts:edit · admin · confirm
set_scripts_userUserReplace a user's scriptsscripts:edit · admin · confirm
set_scripts_deviceDeviceReplace one device's scriptsscripts:edit · admin · confirm
get_block_rulesGlobal + the owner user's + the device's rules + the effective unionglobal:view
set_block_rules_globalGlobalReplace the global block rulesglobal:edit
set_block_rules_serviceServiceReplace a service group's block rulesservices:edit · confirm
set_block_rules_userUserReplace a user's block rulesusers:edit · confirm
set_block_rules_deviceDeviceReplace one device's block rulesdevices:edit
set_breakpoint_patterns_globalGlobalSet the global breakpoint patternsglobal:edit
set_breakpoint_patterns_serviceServiceSet a service group's breakpoint patternsservices:edit · confirm
set_breakpoint_patterns_userUserSet a user's breakpoint patternsusers:edit · confirm
set_breakpoint_patterns_deviceDeviceSet one device's breakpoint patternsdevices:edit

These flow to BusyBro too — you can ask it in plain language to add a service- or user-tier rule — at exactly the same gates (the confirm prompt surfaces the full payload first).

RBAC per tier

Neither the service tier nor the user tier relaxes the bar — each feature keeps its own gating at every scope:

  • Scripts — scripts:edit + admin + confirm, at every tier. A script is arbitrary JavaScript that runs inside the capture path, so authoring is treated like the admin-only remote-browser tools no matter which scope it lands in. The service and user tiers are held to the identical bar as global and device — neither lowers it (services:edit alone is not enough for a service-tier script; the database write-gate enforces the full scripts:edit + admin + confirm). This is enforced at the database layer by a write-gate on each settings store (settings_{global,user,device}_scripts_gate plus the service_groups scripts gate), so no surface — dashboard, MCP, or a raw device token — can smuggle a script in. In particular, a {type:"script"} action embedded in a block-rule write at any tier stays privilege-gated to scripts:edit + admin + confirm, not the laxer block-rule bar.
  • Block rules — the global tier is global:edit, the device tier is devices:edit, the service tier is services:edit (confirm-gated), and the user tier is users:edit (confirm-gated). The inline-script exception above still applies.
  • Breakpoint patterns — global is global:edit, device is devices:edit, the service tier is services:edit (confirm-gated), and the user tier is users:edit (confirm-gated). Low-risk, no code execution.

See Roles & permissions for the capability grid.

How it applies — server-side, in real time

There is no client-side merge. Every capture engine — the proxy-server, the iOS VPN tunnel, and the cdp-connector — receives one already-folded effective settings blob. The database function effective_settings_for_device does the four-way union (de-duped by id, device wins then user then service then global; multiple applied service groups fold in stable name order) and hands back the resolved list; the client just runs it.

That means the service tier reaches every engine with zero new plumbing. When a service-tier list changes, the backend fans a settings-updated event to the device:<uuid> channel of every device that has applied that group, and a user-tier change fans to every device that user owns — the channels those always-on subscribers already listen on — and they refetch the folded blob. The operator dashboard also gets the coarse settings_user:all (and the existing service_groups:all) fan-out so its review of the service- and user-tier lists stays live. No reconnect, no per-client merge logic to keep in sync across three languages — the fold lives in one place.

globalserviceuserdeviceeffective_settings_for_devicefolds + de-dupsproxy-serveriOS tunnelcdp-connectorsettings_* + service groupsalready merged · over Realtime

Source

The user tier is migration supabase/migrations/20260617160000_settings_user_three_tier.sql — the settings_user table, the union in effective_settings_for_device, the settings_user_scripts_gate write-gate, and the settings_user:all + per-device settings-updated broadcast. The service tier is migration supabase/migrations/20260618120000_settings_service_group_four_tier.sql — it adds service_groups.data (+ breakpoint_patterns) rather than a new table, extends effective_settings_for_device to fold the union of every applied service group (the net-new per-group aggregation, ordered by service_groups.name for the multi-group tiebreak), and reuses the service_groups_scripts_gate trigger so a service-tier script still demands scripts:edit + admin + confirm even though the base-table write is services:edit. The MCP writers live in supabase/functions/_shared/mcpRegistry.ts (with the inline-script privilege re-check in supabase/functions/mcp/index.ts). The cross-component sync of this fold is contract #5 (schema → typed clients) and #10 (the script config mirror).

Next