Skip to content

Volumes

A Volume is a filesystem in an environment that Functions, Servers and Sandboxes mount at a path. Containers come and go; the Volume keeps their files, and a Worker can read and write them without any container at all.

home-samVolume/home/usersession 1Sandboxwrites notes.mdsession 2Sandboxreads notes.mdhome-samVolume/home/usersession 1Sandboxwrites notes.mdsession 2Sandboxreads notes.md
Session 1 writes a file to the Volume and stops. Session 2 mounts the same Volume later and finds it there.
src/Storage.ts
import * as Modal from "@alchemy.run/modal";
export const Models = Modal.Volume("Models", {});

Mount it into a Function, Server or program

Section titled “Mount it into a Function, Server or program”

A mounts entry is a volume and a path. Every container the program runs in sees the volume there.

src/Embed.ts
import * as Modal from "@alchemy.run/modal";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as HttpServerResponse from "effect/http/HttpServerResponse";
import { Backend, Models } from "./Storage.ts";
export default class Embed extends Modal.Function<Embed>()(
"Embed",
{ main: import.meta.url, app: Backend, mounts: [{ volume: Models, path: "/models", readOnly: true }] },
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
return {
fetch: Effect.gen(function* () {
return yield* HttpServerResponse.json(yield* fs.readDirectory("/models"));
}).pipe(Effect.orDie),
};
}),
) {}

Modal.MountVolume(Models, { path: "/models" }) does the same from the program itself, next to its other bindings.

In a Worker or Lambda, Modal.MountVolume returns the entry for sandboxes.create:

// construction phase
const models = yield* Modal.MountVolume(Models, { path: "/models", readOnly: true });
const sandboxes = yield* Modal.CreateSandbox(Coder);
// per request
const sandbox = yield* sandboxes.create({ mounts: [models] });

A volume created at runtime mounts the same way: { volume: home, path: "/home/user" }.

A bucket entry mounts an S3, R2 or GCS bucket. Pass a bucket name, or a bucket resource of another provider; a Cloudflare R2 bucket’s account endpoint is filled in. secret is a Modal.Secret holding the bucket’s keys.

src/MySandbox.ts
import * as Cloudflare from "@alchemy.run/cloudflare";
import * as Modal from "@alchemy.run/modal";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as HttpServerResponse from "effect/http/HttpServerResponse";
import { Coder, R2Keys } from "./Storage.ts";
export const Uploads = Cloudflare.R2.Bucket("Uploads", {});
export default class MySandbox extends Modal.Sandbox<MySandbox>()(
"MySandbox",
{ main: import.meta.url, app: Coder, mounts: [{ bucket: Uploads, path: "/uploads", secret: R2Keys }] },
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
return {
fetch: Effect.gen(function* () {
return yield* HttpServerResponse.json(yield* fs.readDirectory("/uploads"));
}).pipe(Effect.orDie),
};
}),
) {}

Modal.MountBucket(Uploads, { path: "/uploads", secret: R2Keys }) is the binding form.

Bind ReadVolume, WriteVolume or ReadWriteVolume:

src/Api.ts
import * as Cloudflare from "@alchemy.run/cloudflare";
import * as Modal from "@alchemy.run/modal";
import * as Effect from "effect/Effect";
import * as HttpServerResponse from "effect/http/HttpServerResponse";
import { Models } from "./Storage.ts";
export default class Api extends Cloudflare.Worker<Api>()(
"Api",
{ main: import.meta.url, compatibility: { flags: ["nodejs_compat"] } },
Effect.gen(function* () {
const models = yield* Modal.ReadWriteVolume(Models);
return {
fetch: Effect.gen(function* () {
yield* models.writeFile("/hello.txt", "hello");
const text = yield* models.readFileString("/hello.txt");
return HttpServerResponse.text(text);
}).pipe(Effect.orDie),
};
}).pipe(Effect.provide(Modal.ReadWriteVolumeHttp)),
) {}
const files = yield* models.listFiles("/", { recursive: true });
yield* models.copyFiles(["/hello.txt"], "/backup/hello.txt");
yield* models.removeFile("/backup", { recursive: true });
const volumes = yield* Modal.CreateVolume(MyEnv);
const home = yield* volumes.getOrCreate(`home-${user}`);
yield* home.writeFile("/notes.md", notes);

Objects created at run time are tagged with the stack, stage and binding (alchemy::stack, alchemy::stage, alchemy::id) and are not deleted when the stack is destroyed. A volume has only a name, so the tags become its name prefix. Find them by that prefix to clean them up:

const prefix = Modal.runtimePrefix({ stack: "my-app", stage: "prod", id: "Api" });
Terminal window
modal volume list --env "$ENV" | grep "$PREFIX"