Skip to content

Part 3: Persist Data with a Volume

Your Service from Part 2 is stateless. Writes to the container’s root disk die with the container. In this part you mount a Volume so files survive deploys.

A Volume is a standalone resource on the Project. Add it next to the Project:

src/project.ts
import * as Railway from "alchemy/Railway";
export const Site = Railway.Project("Site");
export const Data = Railway.Volume("Data", {
project: Site,
mountPath: "/data",
});

The volume is disconnected until a Service mounts it. mountPath is the path in the container.

Inside the Service’s init, Railway.MountVolume(Data, { path }) attaches that Volume at deploy time and hands you the path at runtime:

src/api.ts
import * as Railway from "alchemy/Railway";
import * as Effect from "effect/Effect";
import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest";
import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
import { Site } from "./project.ts";
import { Data, Site } from "./project.ts";
export default class Api extends Railway.Service<Api>()(
"Api",
{
project: Site,
main: import.meta.url,
region: "us-west2",
port: 3000,
},
Effect.gen(function* () {
const mount = yield* Railway.MountVolume(Data, { path: "/data" });
return {
fetch: /* ... */,
};
}),
) {}

MountVolume is a binding: at deploy time it tells the Service “attach this Volume at /data”, and at runtime it hands you the resolved mount.path.

Bindings declare a capability; layers implement it. Provide MountVolumeLive on the Service’s init Effect:

Effect.gen(function* () {
const mount = yield* Railway.MountVolume(Data, { path: "/data" });
return {
fetch: /* ... */,
};
}),
}).pipe(Effect.provide(Railway.MountVolumeLive)),
) {}

Yield FileSystem in the outer Effect. Close over it in fetch. Do not yield it per request.

src/api.ts
import * as FileSystem from "effect/FileSystem";
// ...
Effect.gen(function* () {
const mount = yield* Railway.MountVolume(Data, { path: "/data" });
const fs = yield* FileSystem.FileSystem;
return {
fetch: /* ... */,
};
}).pipe(Effect.provide(Railway.MountVolumeLive)),

The Volume is a directory. Use fs from init:

fetch: Effect.gen(function* () {
const request = yield* HttpServerRequest;
const url = new URL(request.url, "http://service");
if (url.pathname === "/health") {
return HttpServerResponse.json({ ok: true });
}
const file = `${mount.path}${url.pathname}`;
if (request.method === "PUT") {
const body = yield* request.text;
yield* fs.writeFileString(file, body).pipe(Effect.orDie);
return HttpServerResponse.empty({ status: 204 });
}
return HttpServerResponse.text("Hello from Railway!");
}),

Add the read branch, turning a missing file into a 404:

if (request.method === "PUT") {
const body = yield* request.text;
yield* fs.writeFileString(file, body).pipe(Effect.orDie);
return HttpServerResponse.empty({ status: 204 });
}
if (request.method === "GET") {
const text = yield* fs.readFileString(file).pipe(
Effect.catchAll(() => Effect.succeed(undefined)),
);
if (text === undefined) {
return HttpServerResponse.text("Not found", { status: 404 });
}
return HttpServerResponse.text(text);
}
return HttpServerResponse.text("Hello from Railway!");
alchemy.run.ts
import { Site } from "./src/project.ts";
import { Data, Site } from "./src/project.ts";
Effect.gen(function* () {
const site = yield* Site;
yield* Data;
const api = yield* Api;
Terminal window
bun alchemy deploy
Plan: 1 to create, 1 to update

+ Data (Railway.Volume)
~ Api (Railway.Service)

Proceed?
◉ Yes ○ No
 Data (Railway.Volume) created
 Api (Railway.Service) updated

During the Service’s deploy, Alchemy attaches the Volume at /data.

Terminal window
# Store a file on the Volume
curl -X PUT https://myapp-api-dev-a1b2c3d4.up.railway.app/hello.txt -d 'Hello, Volume!'
# Read it back
curl https://myapp-api-dev-a1b2c3d4.up.railway.app/hello.txt
# → Hello, Volume!

Ship another code change and deploy — the file is still there.

You now have:

  • A Volume in the Project mounted at /data on the API Service
  • A Service reading and writing it through Effect’s FileSystem — no SDK, just files
  • Data that outlives deploys

In Part 4, you’ll store a Variable on the Project and read it from the Service, then tear the stack down.