---
title: "Scoped Context"
description: "A scoped context is a namespaced view of the context: it auto-prefixes every RPC id, shared-state key, and streaming channel with your tool's id, and adds a typed, persisted settings store, used from a single tool's code."
canonical_url: "https://devfra.me/guide/scoped-context"
---
# Scoped Context

> A scoped context is a namespaced view of the context: it auto-prefixes every RPC id, shared-state key, and streaming channel with your tool's id, and adds a typed, persisted settings store, used from a single tool's code.

A scoped context is a namespaced view of the context: it auto-prefixes every RPC id, shared-state key, and streaming channel with your tool's id, and adds a typed, persisted `settings` store, used from a single tool's code.

## Node side

`setup(ctx)` receives the full `DevframeNodeContext`; scope it with `ctx.scope(id)`, conventionally your devframe `id`:

```ts
import { defineDevframe, defineRpcFunction } from 'devframe'

export default defineDevframe({
  id: 'my-plugin',
  name: 'My Plugin',
  setup(ctx) {
    const my = ctx.scope('my-plugin')

    my.rpc.register(defineRpcFunction({
      name: 'get-modules', // bare name: stored as `my-plugin:get-modules`
      type: 'query',
      handler: () => loadModules(),
    }))
  },
})

declare function loadModules(): { id: string }[]
```

`ctx.scope(id)` is stable per id, re-exposing unscoped APIs (`views`, `diagnostics`, `agent`, `host`, `cwd`, `mode`), swapping in the auto-namespaced `rpc`, and keeping the original as `my.base`.

## Browser side

`(await connectDevframe()).scope(id)` gives the matching view: `my.rpc` carries `call` / `callEvent` / `callOptional`, `register`, `sharedState`, and `streaming`, plus `my.settings`.

## Auto-namespacing

Bare names are prefixed `<namespace>:` (`call('get-modules')` → `my-plugin:get-modules`); a name with `:` passes through unchanged to another tool (`call('other-plugin:status')`).

`register` accepts only bare names; an already-namespaced one throws [`DF0034`](https://devfra.me/errors/DF0034); use `ctx.base.rpc.register`.

Bare names stay typed: `call('get-modules')` resolves to your [RPC registry](https://devfra.me/guide/rpc#type-safe-rpc-client-registry) entry, `sharedState('selection')` to the matching [`DevframeRpcSharedStates`](https://devfra.me/guide/shared-state#type-safe-keys) key.

## Settings

`my.settings` is a persisted key-value store (alongside `my.rpc`), with two scopes:

- **`project`**: per-checkout values, under the `project` storage dir.
- **`global`**: per-user values, under the `global` storage dir.

Both are file-backed and synced to the browser over the shared-state protocol; a `set` propagates to peers, surviving restarts.

```ts
const { settings } = my

await settings.project.set('theme', 'dark')
await settings.project.get('theme') // => 'dark'
await settings.project.all() // => theme is 'dark'
await settings.project.delete('theme')

const off = await settings.global.onChange((value) => {
  console.log('global settings changed', value)
})
```

Every method is async; the store resolves on first access.

### Typed settings

Augment `DevframeSettingsRegistry` to type a namespace's settings once; the scope types `settings.global` and `settings.project`:

```ts
declare module 'devframe' {
  interface DevframeSettingsRegistry {
    'my-plugin': {
      theme: 'light' | 'dark'
      recentFiles: string[]
    }
  }
}
```

```ts
const my = ctx.scope('my-plugin')
await my.settings.project.set('theme', 'dark') // ✓ typed
await my.settings.project.set('theme', 'blue') // ✗ not assignable
```

Unaugmented namespaces fall back to an open record.

## What's next

- [RPC](https://devfra.me/guide/rpc)
- [Shared State](https://devfra.me/guide/shared-state)
- [Client](https://devfra.me/guide/client)


## Sitemap

See the full [sitemap](https://devfra.me/sitemap.md) for all pages.
