Learn the stack

Start with inspectFile, a capability21: one named action with a contract and a server-side handler. The contract21 defines its name, input, output, failure, and metadata. The handler implements the action.

A service is a named interface for one job. A machine defines states and the events that permit movement between them. A projection18 builds a callable interface from capabilities.

Follow these parts, then try the hosted content tools.

Read Effect basics17 before this trace if the computation model is new. For implementation tasks, use services16, Layer composition13, and testing22.

The pieces

One action, five interfaces

                     ┌─ CLI
                     ├─ HTTP
One Capability ──────┼─ MCP
                     ├─ code mode
                     └─ RPC

A projection18 exposes the same action through another interface. RPC serves browser clients. Code mode21 lets agents compose calls in one program.

All five interfaces use one handler.

Trace one action

The CLI uses inspectFile. The hosted site uses a separate content registry.

1. Define the contract

The contract21 names the action and its input, output, and failure schemas.

packages/core/src/contracts.ts · 5c0f78e0
export const inspectFileContract = defineContract("inspectFile", {
  annotations: { idempotent: true, readOnly: true },
  description: "Count bytes, characters, words, and lines in a file",
  failure: FileStatsError,
  input: Schema.Struct({
    path: Schema.String.annotate({ description: "File to inspect" }),
  }),
  output: FileStatsSchema,
});

FileStatsError belongs to the interface. The annotations mark a repeatable, read-only action.

2. Bind one handler

implement binds the contract to runInspectMachine. The registry includes the resulting capability21.

packages/core/src/inspect-file.ts · 5c0f78e0
export const inspectFile = implement(inspectFileContract, ({ path }) =>
  runInspectMachine(path)
);

export const capabilities = [inspectFile, joinInterest] as const;

The handler starts a machine to read the file.

3. Put file work in a service

FileInspector.make captures the file system. Its inspect method reads bytes and maps file errors to FileStatsError.

packages/core/src/file-inspector.ts · 5c0f78e0
  make: Effect.gen(function* makeFileInspector() {
    const fileSystem = yield* FileSystem.FileSystem;

    const inspect = Effect.fn("FileInspector.inspect")(function* inspect(
      path: string
    ) {
      const content = yield* fileSystem
        .readFile(path)
        .pipe(
          Effect.mapError(
            (error) => new FileStatsError({ path, reason: error.message })
          )
        );

      return summarizeBytes(path, content);
    });

The service exposes its Layer12 beside the implementation. This follows hexagonal architecture19: composition supplies the file-system dependency.

packages/core/src/file-inspector.ts · 5c0f78e0
    return { inspect } as const;
  }),
}) {
  static readonly layer = Layer.effect(this, this.make);
}

Callers ask the service to inspect a path. Composition chooses the file-system implementation.

4. Give the lifecycle named states

A declared fromEffect actor calls FileInspector.

packages/core/src/inspect-machine.ts · 5c0f78e0
const readStats = fromEffect({
  effect: ({ input }) =>
    FileInspector.use((inspector) => inspector.inspect(input.path)),
  schemas: { input: Schema.Struct({ path: Schema.String }) },
});

The machine starts in reading. It finishes in inspected or unreadable.

packages/core/src/inspect-machine.ts · 5c0f78e0
  initial: "reading",
  output: ({ context }) => context.outcome,
  states: {
    inspected: { type: "final" },
    reading: {
      invoke: {
        input: ({ context }) => ({ path: context.path }),
        onDone: {
          context: ({ event }) => ({
            outcome: inspectOutcome.Inspected({ stats: event.output }),
          }),
          target: "inspected",
        },
        onError: {
          context: ({ event }) => ({
            outcome: inspectOutcome.Unreadable({ error: event.error }),
          }),
          target: "unreadable",
        },
        src: "readStats",
      },
    },
    unreadable: { type: "final" },
  },
});

5. Project the registered actions

The command-line interface (CLI) projects the same capabilities21 list for each server interface.

apps/cli/src/surfaces.ts · 5c0f78e0
export const http = toHttpApi("RatStack", capabilities);

export const tools = toToolkit(capabilities);

export const codeMode = toCodeMode(capabilities);

export const routes = Layer.merge(
  HttpApiBuilder.layer(http.api, { openapiPath: "/openapi.json" }).pipe(
    Layer.provide(http.layer)
  ),
  HttpApiScalar.layer(http.api, { path: "/docs" })
);

The RPC projection18 derives contracts from its supplied capabilities.

packages/capability/src/to-rpc.ts · 5c0f78e0
export const toRpc = <const Caps extends readonly AnyCapability[]>(
  capabilities: Caps
): RpcProjection<Caps> => {
  const contracts = capabilities.map((capability) => capability.contract);

A projection takes a registry and reuses its domain handlers.

6. Connect the CLI

toCommand derives each command. inspectFile also gets the stats alias.

apps/cli/src/command.ts · 5c0f78e0
const capabilityCommands = capabilities.map((capability) => {
  const command =
    capability === inspectFile
      ? toCommand(capability, {
          positional: ["path"],
          render: formatFileStats,
        }).pipe(Command.withAlias("stats"))
      : toCommand(capability);

  return command;
});

The entry point supplies the service Layer12 and Node services. It denies approval21 by default.

apps/cli/src/cli.ts · 5c0f78e0
  Effect.provide(
    Layer.mergeAll(
      Layer.provideMerge(FileInspector.layer, NodeServices.layer),
      remoteJoinInterestLayer,
      Approval.denyAll
    )
  )

The command keeps the shared handler.

7. Compose the cloud

An Alchemy Stack groups one cloud infrastructure program and its outputs. Alchemy declares the Stack, Cloudflare providers, and state. The Stack includes Mischief and Website.

apps/infra/alchemy.run.ts · 5c0f78e0
export default Alchemy.Stack(
  "RatStack",
  {
    providers: Cloudflare.providers(),
    state: Cloudflare.state(),
  },
  Effect.gen(function* stack() {
    const mischief = yield* Mischief;
    const website = yield* Website;

    return { mischiefUrl: mischief.url, websiteUrl: website.url };
  })
);

The Stack deploys the cloud applications. The Node file-inspection CLI runs separately.

Try hosted search and read

Use POST /api/execute for code mode21. The agent guide documents this search → read program:

const found = await tools.search({ query: "cartridges", limit: 1 });
const page = await tools.read({ id: found.matches[0].id });
return { title: page.title, id: page.id };

Send it as a JSON code string:

curl --request POST 'https://ratstack.sh/api/execute' \
  --header 'content-type: application/json' \
  --data '{"code":"const found = await tools.search({ query: \"cartridges\", limit: 1 }); const page = await tools.read({ id: found.matches[0].id }); return { title: page.title, id: page.id };"}'

read takes the exact ID from search. This hosted program reads site content.

Read before changing a piece

  1. Read AGENTS.md for pins, boundaries, and the fence39.
  2. Read VISION.md for intent.
  3. Read the package you plan to change.
  4. Before Effect or XState work, read node_modules/effect/AGENTS.md and the pinned sources listed in repo law.

In a copied product repo, replace the template’s product notes and rules. Keep the stack lessons that still serve the product.

Follow repo law and pinned code when an example is stale.

Pick the next skill

Finish

Run the full gate:

pnpm turbo run check test build

Fix failures. Keep the fence39 intact.

Check deployment separately from the gate.


Last changed 2026-10-07 in f1a00f2. Source on GitHub. Change log.

Unlinked mentions (14)

Content updated .