# Cordis Context Shadows and Extension Patterns: A Deep Dive into Isolated Service Architecture

> Explore Cordis context shadows and extension patterns for isolated service architecture. Learn how single proxy contexts create isolated plugin environments while maintaining shared service access.

- Repository: [Cordiverse/cordis](https://github.com/cordiverse/cordis)
- Tags: deep-dive
- Published: 2026-08-25

---

**Cordis implements modularity through a single proxy-based `Context` object that uses internal symbols to create isolated "shadow" contexts for every plugin while preserving access to shared services.**

The `cordiverse/cordis` framework achieves its plugin isolation and extensibility through a sophisticated context shadowing system. By intercepting property access and method calls through proxies, Cordis ensures that each service receives a shadow context pointing to its original caller, preventing state leakage while maintaining shared utility access. This article examines the implementation details found in the core source code and demonstrates the practical extension patterns enabled by this architecture.

## The Foundation: Context and Internal Symbols

The entire shadow mechanism rests on three internal symbols defined in [`packages/core/src/utils.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/utils.ts). These symbols control how the proxy layer creates and manages context isolation:

- **`symbols.shadow`** — Stores a reference to the caller’s original context when a service is instantiated, forming the basis of the shadowing mechanism.
- **`symbols.caller`** — Exposes the real caller to extensions that need to know their invocation source, particularly useful for callable services.
- **`symbols.tracker`** — Holds metadata (`associate`, `property`, `noShadow`) used by proxy machinery to determine shadow creation behavior.

## How Context Shadows Work in Cordis

### Proxy Interception in Context.ts

The `Context` class, defined in [`packages/core/src/context.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/context.ts), constructs a proxy using `ReflectService.handler` that intercepts all property access, method calls, and `new`-style extensions. The `extend()` method clones the current context while preserving the original shadow through prototype chain manipulation:

```typescript
// packages/core/src/context.ts (excerpt)
extend(meta = {}): this {
  const shadow = Reflect.getOwnPropertyDescriptor(this, symbols.shadow)?.value
  const self = Object.create(getTraceable(this, this))
  for (const prop of Reflect.ownKeys(meta)) {
    Object.defineProperty(self, prop, Reflect.getOwnPropertyDescriptor(meta, prop)!)
  }
  if (!shadow) return self
  return Object.assign(Object.create(self), { [symbols.shadow]: shadow })
}

```

The `isolate(name, label?)` method creates a *sub-shadow* that isolates a subset of symbols, allowing plugins to register isolated namespaces without contaminating the parent context.

### Shadow Method Creation in utils.ts

All property reads traverse `createTraceable` in [`packages/core/src/utils.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/utils.ts). When a property’s value is a function, the system generates a **shadow method** that swaps the `this` context:

```typescript
// packages/core/src/utils.ts (excerpt)
function createShadowMethod(ctx: Context, value: any, outer: any, shadow: {}) {
  return new Proxy(value, {
    apply: (target, thisArg, args) => {
      if (thisArg === outer) thisArg = shadow          // ← swap `this` to the shadow
      return getTraceable(ctx, Reflect.apply(target, thisArg, args))
    },
  })
}

```

When a service’s `Tracker` marks `noShadow: true`, the proxy skips shadow creation and exposes the raw caller via `symbols.caller` instead. This behavior is demonstrated in the `Probe` test within [`packages/core/tests/shadow.spec.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/tests/shadow.spec.ts).

## Cordis Extension Patterns

### Simple Service with Shadow Isolation

The most common pattern extends the base `Service` class, automatically receiving a shadow context that preserves isolation while granting access to shared utilities:

```typescript
import { Context, Service } from 'cordis'

export default class Greeter extends Service {
  constructor(ctx: Context) {
    super(ctx, 'greeter')
  }

  greet(name: string) {
    // `this.ctx` is a shadow pointing to the caller's context
    this.ctx.logger.info(`Hello, ${name}!`)
  }
}

```

When loaded via `root.plugin(Greeter)`, the `this.ctx` inside `greet` references a shadow of the caller’s context, maintaining isolation while allowing access to the shared `logger` service.

### Callable Services and Caller Exposure

Services can expose themselves as functions by implementing `[Service.invoke]`, allowing them to capture the caller context explicitly:

```typescript
class Counter extends Service {
  private n = 0
  protected [Service.invoke]() {
    // `symbols.caller` points to the context that invoked the service
    return (this as any)[symbols.caller].logger.info(`count = ${++this.n}`)
  }
}

```

Calling `await ctx['counter']()` triggers the shadow-aware proxy, returning the caller’s context via `symbols.caller` as shown in the "exposes the caller to callable services" test.

### No-Shadow Services for Raw Context Access

For services requiring direct access to the original context without shadow wrapping, set `noShadow: true` in the tracker metadata:

```typescript
class Probe {
  [Service.tracker] = { property: 'ctx', noShadow: true }
  constructor(public ctx: Context) {}
  inspect() {
    // `symbols.caller` is available, but no shadow is created
    return (this as any)[symbols.caller]
  }
}

```

The `noShadow` flag instructs the proxy to expose the raw caller context directly, bypassing the shadow creation mechanism entirely.

### Plugin Loading with Shadow Stripping

When services load additional plugins, Cordis prevents context leakage by stripping the current shadow before invocation. In the `Loader` implementation, the framework removes the active shadow before calling `ctx.plugin()`, ensuring that newly loaded plugins receive the root context rather than the loader’s internal shadow. This prevents accidental exposure of loader-specific state to child plugins, as verified by the "strips service shadow before creating plugins" test in [`shadow.spec.ts`](https://github.com/cordiverse/cordis/blob/main/shadow.spec.ts).

## Key Implementation Files

| File | Role |
|------|------|
| [`packages/core/src/context.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/context.ts) | Core `Context` class, `extend()`, `isolate()`, and symbol handling. |
| [`packages/core/src/utils.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/utils.ts) | Symbol definitions, shadow/tracker logic, proxy factories (`createTraceable`, `createShadowMethod`). |
| [`packages/core/src/service.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/service.ts) | Base `Service` class, integration of `Tracker`, callable transformation via `createCallable`. |
| [`packages/core/tests/shadow.spec.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/tests/shadow.spec.ts) | Test suite validating shadow behavior, no-shadow services, and loader shadow stripping. |
| [`packages/core/src/reflect.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/reflect.ts) | Provides `ReflectService.handler` powering the context proxy constructor. |

## Summary

- **Cordis context shadows** are created through a proxy system in [`packages/core/src/context.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/context.ts) that intercepts property access and method calls.
- Three internal symbols—`symbols.shadow`, `symbols.caller`, and `symbols.tracker`—manage context isolation and caller exposure.
- The `extend()` method clones contexts while preserving shadow references through prototype chain manipulation.
- **Extension patterns** range from simple shadow-isolated services to callable services exposing `symbols.caller`, and no-shadow services using raw context access.
- The `Loader` strips shadows before creating new plugins to prevent context leakage from parent services to child plugins.

## Frequently Asked Questions

### How does Cordis prevent plugins from accessing each other's internal state?

Cordis creates a **shadow context** for every plugin instantiation, stored via `symbols.shadow`. When a service method is invoked, the proxy layer (implemented in [`packages/core/src/utils.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/utils.ts)) swaps the `this` argument to the shadow context through `createShadowMethod`. This ensures each plugin operates within its isolated context bubble while still accessing shared framework services through the prototype chain.

### What is the difference between `symbols.shadow` and `symbols.caller` in Cordis?

`symbols.shadow` holds a reference to the **original context** at service instantiation time, creating an isolated layer for method execution. `symbols.caller` exposes the **actual context that invoked a callable service**, used primarily when a service defines `[Service.invoke]` and needs to know which context called it as a function. While shadows maintain isolation, `symbols.caller` provides explicit access to the invocation source.

### When should I use `noShadow: true` in a Cordis service?

Set `noShadow: true` in the `[Service.tracker]` metadata when your service requires **direct access to the original context** without proxy wrapping. This is necessary for services that inspect the raw caller context or operate as thin wrappers around existing objects. According to [`packages/core/tests/shadow.spec.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/tests/shadow.spec.ts), this pattern is used by diagnostic tools like `Probe` that need to verify the exact context reference rather than a shadow proxy.

### How does the `extend()` method preserve context shadows?

The `extend()` method in [`packages/core/src/context.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/context.ts) retrieves the existing shadow via `Reflect.getOwnPropertyDescriptor(this, symbols.shadow)`, creates a new object inheriting from the traceable context, and conditionally assigns the shadow to the new instance. If no shadow exists, it returns the bare extended object; otherwise, it creates a new object with `symbols.shadow` explicitly set to the original shadow reference, ensuring the chain of isolation persists through context extensions.