Skip to content

Secrets

A Durable Object reads a secret with Config, in its Construction phase. The value is read while you deploy, and every node in the class’s pool gets it in its environment, so every instance reads it the same way.

src/room.ts
import * as Rivet from "@alchemy.run/rivet";
import * as Config from "effect/Config";
import * as Effect from "effect/Effect";
import * as Redacted from "effect/Redacted";
import { Default } from "./cluster.ts";
export default class Room extends Rivet.DurableObject<Room>()(
"Room",
{ main: import.meta.url },
Effect.gen(function* () {
const apiKey = yield* Config.Redacted("MODERATION_API_KEY");
return Effect.gen(function* () {
return {
moderate: (text: string) =>
Effect.succeed(Redacted.value(apiKey).length > 0 && text.length < 500),
};
});
}).pipe(Effect.provide(Default)),
) {}

Read it in the outer Effect, not inside a method: the outer Effect runs while you deploy as well as on each node, which is how the value reaches the pool. How Config travels from a deploy to a runtime is the same everywhere; see Secrets.

Every class on a pool shares its nodes, so each node’s environment holds the values of every class on it. Give a class with secrets the others shouldn’t see a pool of its own.

A control plane that keeps its data in a bucket outside its own cloud takes the bucket’s keys the same way. Three Hetzner engines sharing Hetzner Object Storage:

src/cluster.ts
import * as RivetHetzner from "@alchemy.run/rivet/Hetzner";
import * as Config from "effect/Config";
export const Cluster = RivetHetzner.ServerControlPlane("Rivet", {
instances: 3,
objectStorage: {
kind: "s3",
bucket: "my-rivet-data",
endpoint: "https://nbg1.your-objectstorage.com",
region: "nbg1",
accessKeyId: Config.String("HETZNER_S3_ACCESS_KEY_ID"),
secretAccessKey: Config.Redacted("HETZNER_S3_SECRET_ACCESS_KEY"),
},
});

Control planes on AWS and GCP need no keys: their engines reach S3 or Cloud Storage as their own instance role or service account.

.env is only the default source. A Stack can read each stage’s values from a secrets manager instead:

alchemy.run.ts
import * as Alchemy from "alchemy";
import * as Doppler from "@alchemy.run/doppler";
import * as Hetzner from "@alchemy.run/hetzner";
import * as Rivet from "@alchemy.run/rivet";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import Api from "./src/api.ts";
export default Alchemy.Stack(
"RivetRooms",
{
providers: Layer.mergeAll(Hetzner.providers(), Rivet.providers()),
secrets: ({ stage }) =>
stage === "prod"
? Doppler.Secrets({ project: "rivet-rooms", config: "prd" })
: Alchemy.Secrets.DotEnv(),
},
Effect.gen(function* () {
const api = yield* Api;
return { service: api.name };
}),
);

The Config.Redacted("MODERATION_API_KEY") in the room doesn’t change. See Secret providers.