Skip to content
LogoLogo

Capabilities & Layers

Make dependencies visible and provide them by scope

VibeLang models a dependency as a capability in a compiler-tracked requirement channel. A Context class defines and accesses a service; a Layer decides how to construct it.

Define a Capability

import { Context } from "vibelang/context"
 
abstract class Mailer extends Context {
  abstract send(message: Message): Result<Receipt, MailError>
}

The class is both the service contract and its nominal key. Two Context subclasses with identical methods remain distinct capabilities.

Use a Capability

function welcome(user: User): Result<Receipt, MailError> {
  const mailer = Mailer.context()
  const clock = Clock.context()
  const message = Message.welcome(user, clock.now())
  return mailer.send(message)
}

The inherited context() call returns the instance and adds the class to the enclosing function's inferred requirements. welcome requires Mailer | Clock.

Requirement and Result Composition

function register(
  input: Signup,
): Result<User, CreateUserError | MailError> {
  const users = Users.context()
  const user = users.create(input).unwrap()
  welcome(user).unwrap()
  return user
}

The compiler propagates Users, Mailer, and Clock through the ordinary calls. .unwrap() propagates the Error variants; these are separate from the requirement channel.

Build and Provide a Layer

import { Layer } from "vibelang/provider"
 
const Production = Layer.merge(
  Layer.succeed(Clock, SystemClock),
  Layer.succeed(Mailer, new SesMailer(credentials)),
  Layer.succeed(Users, new PostgresUsers(databaseUrl)),
)
 
Layer.provide(Production, async () => {
  const user = (await registerAsync(input)).unwrap()
  return user.id
})

A layer is conceptually Layer<Provides, InitError, Requires>. Acquisition may return a Result and require other capabilities. A missing dependency is a compile error when the closure is known.

Test With Layers

const Test = Layer.merge(
  Layer.succeed(Clock, TestClock.at("2026-08-20T12:00:00Z")),
  Layer.succeed(Mailer, RecordingMailer.make()),
  Layer.succeed(Users, InMemoryUsers.empty()),
)
 
Layer.provide(Test, () => {
  const user = register(fixture).unwrap()
  expect(RecordingMailer.sent).toHaveLength(1)
  return user
})

No module replacement or global clock patch is needed.

Platform Capabilities

function loadConfig(): Result<Config, FileError | InvalidConfig> {
  const fs = FileSystem.context()
  const text = fs.readText("app.json").unwrap()
  return Config.parse(text)
}

Node, Bun, Deno, native, WASI, and tests can provide different implementations of the same platform contracts.

Open Lifecycle Work

The layer API is accepted, while acquisition/disposal ordering, sharing, nested overrides, cycles, and finalizer failure policy remain under design. Source semantics do not depend on whether the backend uses hidden parameters or a scoped environment.