Skip to content

CONCEPT Cited by 1 source

Object-capability RPC

What it is

Object-capability RPC is an RPC model in which references to remote objects and functions are first-class values on the wire. Holding a reference (a "stub") is the authority to invoke the underlying object — there is no out-of-band ACL check on every call, no global function namespace anyone can name, and (critically) no way for a peer to forge a reference it wasn't granted. Method invocations on a stub are proxied back to the location where the object lives.

The security model comes straight from Dennis & Van Horn's 1966 capability-based access control: what you can do is determined by what references you possess, and references are unforgeable by construction. Applied to RPC, this means:

  • Functions pass by reference. Pass a function over RPC and the recipient gets a stub; invoking the stub calls the function back at its origin.
  • Objects pass by reference. An object extending a marker type (e.g. RpcTarget in Cap'n Web) is transmitted by reference; method calls route back to the origin.
  • Stubs are unforgeable. The only way to obtain one is to be handed one by a peer that already has it. This lets API authors encode authorization in the type system rather than re-checking on every method.

The "Cap'n" in Cap'n Proto and Cap'n Web is short for "capabilities and …" — the naming flags this as the load-bearing design decision.

Why it matters

  • Authorization-by-possession, not authorization-by-identity. A caller that holds a stub for an AuthenticatedSession can invoke protected methods without re-presenting credentials — the session object is the credential. See patterns/capability-returning-authenticate.
  • Session state without special cases. Most RPC systems handle auth via a connection-state flag mutated by a "login" message; object-capability RPC replaces that with a typed stub return value, so auth fits the normal API abstraction. Particularly valuable over WebSocket where headers can't be added after the initial handshake.
  • Least privilege falls out for free. Hand out a narrow stub (e.g. ReadOnlyBucket) and the recipient structurally cannot call the wider API — there's no stub to invoke. Contrast a REST API where all URLs are guessable and the authorization check lives in the handler.
  • Bidirectional / symmetric calling. Passing a callback by reference is just another instance of the same mechanism, so bidirectional RPC comes free with no special protocol for it.

Trade-offs vs traditional RPC

  • Lifetime / GC is harder. A stub held by a remote peer pins the underlying object. Implementations need an export/import table, reference counting (or equivalent), and a disposal discipline. Cap'n Web uses signed integer IDs never reused over a connection and implicit / explicit release.
  • Debuggability shifts. Instead of "look at the URL and the bearer token," you look at the export table: who is holding which stubs? Traces need to correlate stub IDs.
  • Transport must be bidirectional. Object-capability RPC needs callbacks to work, which rules out naïve request/reply transports unless stubs are limited to the request lifetime (Cap'n Web's HTTP batch mode does exactly this — references become invalid once the batch completes).
  • Not naturally stateless. Standard REST / gRPC services scale horizontally by being stateless; object-capability RPC is connection-oriented (each connection has its own export table), which affects sharding / load-balancing design.

Capability-as-authorization: the canonical pattern

The idiomatic idiom in object-capability RPC is:

// Top-level interface (anyone can call):
class ApiServer extends RpcTarget {
  authenticate(apiKey: string): Session {
    if (!checkApiKey(apiKey)) throw new Error("bad key")
    return new AuthenticatedSession(this.lookupUser(apiKey))
  }
}

// Returned by authenticate(); only callers who successfully
// authenticated hold references:
class AuthenticatedSession extends RpcTarget {
  whoami(): string { return this.username }
  // ... other protected methods
}

Clients do const session = await api.authenticate(key); const who = await session.whoami(). The server side cannot be called by a client that never obtained a session stub. There is no if (req.user) check on every method — the type system enforces it. See patterns/capability-returning-authenticate.

Seen in

Last updated · 200 distilled / 1,178 read