Skip to main content
A resource maps an external system to Mirage’s filesystem operations and shell commands. There are two paths:
  • Ship your own backend: a single TypeScript file in your own project or package, built on GenericResource. No Mirage fork, no edits to Mirage source.
  • Contribute a builtin: the four-layer layout inside the Mirage repo, mirrored in Python.

Ship Your Own Backend

Write the core functions over your data source, put them on a CommandIO table, and GenericResource wires the full generic command set (ls, cat, grep, find, head, wc, …) plus glob resolution and the VFS/FUSE ops:
The accessor is a type parameter, so the table is checked against the accessor your core functions actually take: wiring readdir where stat belongs, or an accessor from another backend, is a compile error rather than a runtime one. Only four table fields are required. The optional ones unlock more surface: write enables the byte-mutation family, find and du become native fast paths, and a command whose requirements the table cannot meet is never registered rather than registered and broken. The escape hatches are the ones the builtins use, because GenericResource assembles exactly what they assemble by hand:
  • overrides drops a generic command you replace, and commands supplies the replacement (or any bespoke verb) from command({...}).
  • ops layers an irregular VFS/FUSE handler over the derived set; one carrying no filetype shadows the derived op of the same name. autoOps: false opts out of deriving any.
  • sizesAlwaysKnown declares that stat sizes every file without fetching it, which is also what makes the mount legal on FSKit. supportsSnapshot declares that stat fills FileStat.fingerprint; setting it without that is not drift detection.
To make the backend constructible by name (workspace config, snapshots, the daemon), register a factory:
The registry takes a factory rather than a class because a browser backend is often reached through a dynamic import; buildResource('jira', config) then works exactly as it does for a builtin. Snapshots are the one place the registry does not reach: Workspace.load builds its mounts before any factory is consulted, so a saved custom mount has to be handed back explicitly, as Workspace.load(state, { resources: { '/jira/': new JiraResource(cfg) } }). GenericResource says so in its own state, which is what turns a forgotten override into a refusal to load rather than a mount that comes back empty. Workspace.copy needs nothing, since it passes the live resource through. See examples/typescript/other/custom_resource.ts for a complete runnable backend in one file, and examples/python/other/custom_resource.py for its Python twin. Both are asserted against the same truth file, so the two SDKs cannot drift.

Contribute a Builtin Resource

Builtins live inside the Mirage repo with the four-layer layout below. Keep the core I/O layer independent from command parsing, and check whether the same resource or behavior should also be added to Python. Pick the package by runtime, not by preference: packages/core for a backend that works in both the browser and Node, packages/node for one that needs Node APIs, packages/browser for one that needs a browser transport. Use a recent resource such as Qdrant or LanceDB as the structural reference. Paths are always PathSpec values inside the VFS; do not pass filesystem paths as raw strings.

File Structure

Tests are colocated: <name>.test.ts beside the source it covers.

1. Config, Accessor, and Registry

Define the config as an interface plus a resolve<Name>Config that fills every default, and keep secrets out of anything that gets serialized. Create an Accessor subclass that owns the client or transport. Add the resource name to ResourceName, and add a factory to the runtime package’s resource/registry.ts; that entry is what workspace config, snapshots, and the daemon construct through. Config keys arrive snake_case from YAML shared with Python and are mapped by normalizeFields, which already sends every unlisted key through snakeToCamel. Add a rename entry only for a key that mapping gets wrong. Keep every import at module scope. If that would create a cycle, change the dependency direction instead of adding a lazy import inside a function.

2. Core VFS Operations

Implement only the operations the backend supports. A read-only API resource usually starts with:
  • readdir(accessor, path, index?) returning child paths.
  • read(accessor, path, index?) returning bytes.
  • stat(accessor, path, index?) returning a FileStat.
FileStat.size must be the rendered content’s byte length or null, never a storage-side number: a confidently wrong size makes wc -c and ls -l lie over FUSE, while null rides the unknown-size machinery. Put the storage number in extra if it is worth reporting. Glob resolution is not a per-backend file: bind it from readdir with makeResolveGlob(readdir, cap), or let GenericResource derive it from the table.

3. Ops Layer

Ops are the workspace dispatcher’s typed adapters onto the core functions, and they are generated, not hand-written:
Write an op by hand only for an irregular handler, and pass its name through overrides so the derived set skips it. Mark every mutation write: true.

4. Commands

Build the standard command set with makeGenericCommands over the same table; the generic command owns flag interpretation, so a backend wrapper is wiring only. Export the result as <NAME>_COMMANDS from commands/builtin/<name>/index.ts. For a resource-specific command:
  • Use the shared command spec (specOf), or a new CommandSpec({...}) for a verb with its own grammar.
  • Mark every mutation write: true so MountMode.READ stays a real boundary.
  • Read a flag through new FlagView(flags, specOf('<name>')), never flags.get(...).
  • Add a provision estimator when a useful estimate is possible; otherwise the planner reports precision: unknown.

5. Resource Class

Import the command and op arrays at module scope and return them from commands() and ops():
Set cachesReads true only for stable, read-mostly content. Override getState() to carry the (redacted) config, and close() to release any client handles, calling super.close(), which closes the index store. A config-backed backend also sets needs_override: true in that state. TypeScript’s loader consults no registry, so a mount it was not handed comes back as an empty RAMResource; the flag makes it refuse instead. Python does not need it, which is why the flag is written on only a handful of resources there and read on none.

6. Snapshot Support

Leave supportsSnapshot unset unless the complete drift contract is implemented:
  1. stat() returns a stable FileStat.fingerprint.
  2. Every read record includes the fingerprint that produced those bytes.
  3. If the backend supports immutable revisions, reads consult the resolved revision and record it.
Setting the flag without recording fingerprints does not provide drift detection.

7. Verification

Add tests for config resolution, path layout, every VFS op, command behavior, read-only enforcement, state redaction, and cleanup. For major features, add or update integration coverage under integ/, and check Python/TypeScript parity before opening the PR.