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
| Scope | Stored in | Applies to | Author it on |
|---|---|---|---|
| Global | settings_global.data | Every device in the fleet | Settings → ‹Scripts / Blocks / Breakpoints› |
| Service | service_groups.data (per group) | Every device that has applied that service group (via applied_service_groups) | Settings → Services → ‹group›, or set_*_service over MCP |
| User | settings_user.data (keyed by the owner's account id) | Every device a single user owns | the same page, with the scope selector set to a user |
| Device | settings_device.data | One device only | Devices → ‹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.
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):
| Tool | Tier | What it does | Gating |
|---|---|---|---|
get_scripts | — | Global + (with a device) the owner user's + that device's scripts + the effective union | scripts:view |
set_scripts_global | Global | Replace the global scripts | scripts:edit · admin · confirm |
set_scripts_service | Service | Replace a service group's scripts | scripts:edit · admin · confirm |
set_scripts_user | User | Replace a user's scripts | scripts:edit · admin · confirm |
set_scripts_device | Device | Replace one device's scripts | scripts:edit · admin · confirm |
get_block_rules | — | Global + the owner user's + the device's rules + the effective union | global:view |
set_block_rules_global | Global | Replace the global block rules | global:edit |
set_block_rules_service | Service | Replace a service group's block rules | services:edit · confirm |
set_block_rules_user | User | Replace a user's block rules | users:edit · confirm |
set_block_rules_device | Device | Replace one device's block rules | devices:edit |
set_breakpoint_patterns_global | Global | Set the global breakpoint patterns | global:edit |
set_breakpoint_patterns_service | Service | Set a service group's breakpoint patterns | services:edit · confirm |
set_breakpoint_patterns_user | User | Set a user's breakpoint patterns | users:edit · confirm |
set_breakpoint_patterns_device | Device | Set one device's breakpoint patterns | devices: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:editalone is not enough for a service-tier script; the database write-gate enforces the fullscripts:edit+ admin + confirm). This is enforced at the database layer by a write-gate on each settings store (settings_{global,user,device}_scripts_gateplus theservice_groupsscripts 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 toscripts:edit+ admin + confirm, not the laxer block-rule bar. - Block rules — the global tier is
global:edit, the device tier isdevices:edit, the service tier isservices:edit(confirm-gated), and the user tier isusers:edit(confirm-gated). The inline-script exception above still applies. - Breakpoint patterns — global is
global:edit, device isdevices:edit, the service tier isservices:edit(confirm-gated), and the user tier isusers: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.
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
- Scripts — the
onRequest/onResponsehook engine these scopes carry. - Block rules — auto-block / mock matching requests across the same four tiers.
- Breakpoints & resend — pause and edit matching traffic, scoped the same way.
- Specialist agents & service groups — what a service group is and how a device applies one (the unit the service tier keys on).
- Remote settings & env — the global / per-device settings model and the connection-type chain this fold mirrors.
- Roles & permissions — the capabilities that gate each tier.