Skip to content

Build a Git Repo API

So far the Cloudflare track has added stateful primitives one at a time. This tutorial pulls two of them together into something recognizable: a tiny GitHub. Cloudflare Artifacts stores the actual Git history (clone, push, pull all work against it), and a Durable Object per repo holds the metadata you don’t want inside the repo itself — description, topics, star count.

We’ll front the whole thing with Effect’s HttpApi so every route is schema-validated end-to-end and the integration test can call the worker through the same typed client a real consumer would use.

By the end you’ll have a Worker that lets a client create a repo, git clone against it, read combined info, star it, and update its description.

A Cloudflare Artifacts namespace is the top-level container for Git-compatible repos. Namespaces are implicit — there’s nothing to provision at deploy time, so the resource is a thin binding marker. Repos themselves are created at runtime through the binding.

Create src/Repos.ts:

src/Repos.ts
import * as
import Cloudflare
Cloudflare
from "alchemy/Cloudflare";
export const
const Repos: Effect<Cloudflare.Artifacts, never, Stack | Stage>
Repos
=
import Cloudflare
Cloudflare
.
const Artifacts: (name: string, props?: Cloudflare.ArtifactsProps) => Effect<Cloudflare.Artifacts, never, Stack | Stage>

Marker for a Cloudflare Artifacts namespace binding.

Artifacts namespaces are implicit (created on first repo write) and require no deploy-time provisioning, so this is a pure binding marker rather than a full Resource. The Worker provider sees this object in bindings: { ... } and emits the corresponding { type: "artifacts", name, namespace } binding to the script.

A Cloudflare Artifacts namespace — the top-level container for Git-compatible versioned repositories. See the

https://blog.cloudflare.com/artifacts-git-for-agents-beta/ Artifacts launch post

and

https://developers.cloudflare.com/artifacts/concepts/namespaces/ Namespaces docs

.

Namespaces on Cloudflare are implicit: there is no POST /namespaces endpoint. The namespace is conjured the first time a repo is created against it (either via the REST API or the Worker binding). Because of that, the Alchemy "resource" is a thin binding marker — there is nothing to provision at deploy time. Repos themselves are typically created at runtime through the bound Artifacts API.

@binding

@productArtifacts

@categoryDeveloper Platform

@sectionDeclaring a Namespace

@example

Default namespace (a unique physical name is generated)

const Repos = Cloudflare.Artifacts("Repos");

@example

Override the namespace name (must be lowercase, 3–63 chars)

const Repos = Cloudflare.Artifacts("Repos", { namespace: "starter-repos" });

@sectionBinding to a Worker

@example

Wiring it into a Worker

export const Worker = Cloudflare.Worker("Worker", {
main: "./src/worker.ts",
bindings: { Repos },
});
export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;
// { Repos: Artifacts }

@example

Async-style worker

export default {
async fetch(request: Request, env: WorkerEnv) {
const repo = await env.Repos.create("starter-repo");
return Response.json({ remote: repo.remote, token: repo.token });
},
};

@example

Effect-style worker

const artifacts = yield* Cloudflare.Artifacts.bind(Repos);
const repo = yield* artifacts.create("starter-repo", {
setDefaultBranch: "main",
});

Artifacts
("Repos");

That’s the whole declaration. The Worker provider will see this the moment we .bind(...) it.

The schema and endpoint declarations live outside the Worker so the same file can be imported by clients and tests without pulling in any runtime code. Start with one endpoint — POST /repos:

src/Api.ts
import * as
import Schema
Schema
from "effect/Schema";
import * as
import HttpApi
HttpApi
from "effect/unstable/httpapi/HttpApi";
import * as
import HttpApiEndpoint
HttpApiEndpoint
from "effect/unstable/httpapi/HttpApiEndpoint";
import * as
import HttpApiGroup
HttpApiGroup
from "effect/unstable/httpapi/HttpApiGroup";
export class
class CreateRepoResponse
CreateRepoResponse
extends
import Schema
Schema
.
const Class: <CreateRepoResponse, {}>(identifier: string) => {
<Fields>(fields: Fields, annotations?: Schema.Annotations.Declaration<CreateRepoResponse, readonly [Schema.Struct<Fields>]> | undefined): Schema.Class<CreateRepoResponse, Schema.Struct<Fields>, {}>;
<S>(schema: S, annotations?: Schema.Annotations.Declaration<CreateRepoResponse, readonly [S]> | undefined): Schema.Class<CreateRepoResponse, S, {}>;
}

Creates a schema-backed class whose constructor validates input against a

Struct

schema. Construction throws a

SchemaError

on invalid input.

When to use

Use when you need a schema-backed data class with validated construction, schema-derived decoding/encoding, and class-style methods or inheritance.

Details

Pass the desired class type as the first type parameter. The second optional type parameter can be used to add nominal brands.

Gotchas

Passing disableChecks in the options skips constructor validation.

Example (Basic class)

import { Schema } from "effect"
class Person extends Schema.Class<Person>("Person")({
name: Schema.String,
age: Schema.Number
}) {}
const alice = new Person({ name: "Alice", age: 30 })
console.log(alice.name) // "Alice"
console.log(`${alice}`) // "Person({ name: Alice, age: 30 })"

Example (Extending a class)

import { Schema } from "effect"
class Animal extends Schema.Class<Animal>("Animal")({
name: Schema.String
}) {}
class Dog extends Animal.extend<Dog>("Dog")({
breed: Schema.String
}) {}
const dog = new Dog({ name: "Rex", breed: "Labrador" })
console.log(dog.name) // "Rex"
console.log(dog.breed) // "Labrador"

@seeTaggedClass for adding a _tag literal field to the class schema

@seeErrorClass for defining schema-backed error classes

@seeTaggedErrorClass for defining tagged schema-backed error classes

@categoryconstructors

@since3.10.0

Class
<
class CreateRepoResponse
CreateRepoResponse
>(
"CreateRepoResponse",
)({
name: Schema.String
name
:
import Schema
Schema
.
const String: Schema.String

Type-level representation of

String

.

Schema for string values. Validates that the input is typeof "string".

@categorymodels

@since4.0.0

@categoryschemas

@since4.0.0

String
,
remote: Schema.String
remote
:
import Schema
Schema
.
const String: Schema.String

Type-level representation of

String

.

Schema for string values. Validates that the input is typeof "string".

@categorymodels

@since4.0.0

@categoryschemas

@since4.0.0

String
,
token: Schema.String
token
:
import Schema
Schema
.
const String: Schema.String

Type-level representation of

String

.

Schema for string values. Validates that the input is typeof "string".

@categorymodels

@since4.0.0

@categoryschemas

@since4.0.0

String
,
defaultBranch: Schema.String
defaultBranch
:
import Schema
Schema
.
const String: Schema.String

Type-level representation of

String

.

Schema for string values. Validates that the input is typeof "string".

@categorymodels

@since4.0.0

@categoryschemas

@since4.0.0

String
,
}) {}
export class
class RepoConflict
RepoConflict
extends
import Schema
Schema
.
const TaggedErrorClass: <RepoConflict, {}>(identifier?: string) => {
<Tag, Fields>(tag: Tag, fields: Fields, annotations?: Schema.Annotations.Declaration<RepoConflict, readonly [Schema.TaggedStruct<Tag, Fields>]> | undefined): Schema.Class<RepoConflict, Schema.TaggedStruct<Tag, Fields>, YieldableError>;
<Tag, S>(tag: Tag, schema: S, annotations?: Schema.Annotations.Declaration<RepoConflict, readonly [Schema.Struct<{ [K in keyof ({
readonly _tag: Schema.tag<Tag>;
} & S["fields"])]: ({
readonly _tag: Schema.tag<Tag>;
} & S["fields"])[K]; }>]> | undefined): Schema.Class<...>;
}

Defines a schema-backed yieldable error class with an automatically populated _tag field.

When to use

Use to define typed errors that are schema validated, yielded in Effect.gen, and matched as tagged union members.

Example (Tagged error class)

import { Effect, Schema } from "effect"
class NotFound extends Schema.TaggedErrorClass<NotFound>()("NotFound", {
id: Schema.Number
}) {}
const program = Effect.gen(function*() {
yield* new NotFound({ id: 42 })
})

@categoryconstructors

@since3.10.0

TaggedErrorClass
<
class RepoConflict
RepoConflict
>()(
"RepoConflict",
{
message: Schema.String
message
:
import Schema
Schema
.
const String: Schema.String

Type-level representation of

String

.

Schema for string values. Validates that the input is typeof "string".

@categorymodels

@since4.0.0

@categoryschemas

@since4.0.0

String
},
) {}
export const
const createRepo: HttpApiEndpoint.HttpApiEndpoint<"createRepo", "POST", "/repos", HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.Json<Schema.Struct<{
readonly name: Schema.String;
readonly description: Schema.optional<Schema.String>;
}>>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.Json<typeof CreateRepoResponse>, HttpApiEndpoint.Json<typeof RepoConflict>, never, never>
createRepo
=
import HttpApiEndpoint
HttpApiEndpoint
.
const post: <"createRepo", "/repos", never, never, Schema.Struct<{
readonly name: Schema.String;
readonly description: Schema.optional<Schema.String>;
}>, never, typeof CreateRepoResponse, typeof RepoConflict>(name: "createRepo", path: "/repos", options?: {
readonly disableCodecs?: false | undefined;
readonly params?: undefined;
readonly query?: undefined;
readonly headers?: undefined;
readonly payload?: Schema.Struct<{
readonly name: Schema.String;
readonly description: Schema.optional<Schema.String>;
}> | undefined;
readonly success?: typeof CreateRepoResponse | undefined;
readonly error?: typeof RepoConflict | undefined;
} | undefined) => HttpApiEndpoint.HttpApiEndpoint<...> (+1 overload)
post
("createRepo", "/repos", {
payload?: Schema.Struct<{
readonly name: Schema.String;
readonly description: Schema.optional<Schema.String>;
}> | undefined
payload
:
import Schema
Schema
.
function Struct<{
readonly name: Schema.String;
readonly description: Schema.optional<Schema.String>;
}>(fields: {
readonly name: Schema.String;
readonly description: Schema.optional<Schema.String>;
}): Schema.Struct<{
readonly name: Schema.String;
readonly description: Schema.optional<Schema.String>;
}>

Defines a struct schema from a map of field schemas.

Details

Each field value is a schema. Use

optionalKey

or

optional

to mark fields as optional, and

mutableKey

to mark them as mutable.

The resulting schema's Type is a readonly object type with the fields' decoded types. The Encoded form mirrors the field schemas' encoded types.

Example (Basic struct)

import { Schema } from "effect"
const Person = Schema.Struct({
name: Schema.String,
age: Schema.Number,
email: Schema.optionalKey(Schema.String)
})
// { readonly name: string; readonly age: number; readonly email?: string }
type Person = typeof Person.Type
const alice = Schema.decodeUnknownSync(Person)({ name: "Alice", age: 30 })
console.log(alice)
// { name: 'Alice', age: 30 }

@categoryconstructors

@since3.10.0

Struct
({
name: Schema.String
name
:
import Schema
Schema
.
const String: Schema.String

Type-level representation of

String

.

Schema for string values. Validates that the input is typeof "string".

@categorymodels

@since4.0.0

@categoryschemas

@since4.0.0

String
,
description: Schema.optional<Schema.String>
description
:
import Schema
Schema
.
const optional: optionalLambda
<Schema.String>(self: Schema.String) => Schema.optional<Schema.String>

Type-level representation returned by

optional

.

Marks a struct field as optional, allowing the key to be absent or undefined.

Details

The resulting property may be absent or explicitly set to undefined. Equivalent to optionalKey(UndefinedOr(S)).

Use

optionalKey

instead if you want exact optional semantics (absent only, not undefined).

Example (Optional field accepting undefined)

import { Schema } from "effect"
const schema = Schema.Struct({
name: Schema.String,
age: Schema.optional(Schema.Number)
})
// { readonly name: string; readonly age?: number | undefined }
type Person = typeof schema.Type

@categorymodels

@since3.10.0

@categorycombinators

@since3.10.0

optional
(
import Schema
Schema
.
const String: Schema.String

Type-level representation of

String

.

Schema for string values. Validates that the input is typeof "string".

@categorymodels

@since4.0.0

@categoryschemas

@since4.0.0

String
),
}),
success?: typeof CreateRepoResponse | undefined
success
:
class CreateRepoResponse
CreateRepoResponse
,
error?: typeof RepoConflict | undefined
error
:
class RepoConflict
RepoConflict
,
});
export class
class ReposGroup
ReposGroup
extends
import HttpApiGroup
HttpApiGroup
.
const make: <"repos", false>(identifier: "repos", options?: {
readonly topLevel?: false | undefined;
} | undefined) => HttpApiGroup.HttpApiGroup<"repos", never, false>

Creates an empty HttpApiGroup with the supplied identifier.

Details

Add endpoints with add, provide implementations with HttpApiBuilder.group, and set topLevel when the generated client should expose endpoint methods directly instead of nesting them under the group name.

@categoryconstructors

@since4.0.0

make
("repos").
HttpApiGroup<"repos", never, false>.add<readonly [HttpApiEndpoint.HttpApiEndpoint<"createRepo", "POST", "/repos", HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.Json<Schema.Struct<{
readonly name: Schema.String;
readonly description: Schema.optional<Schema.String>;
}>>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.Json<typeof CreateRepoResponse>, HttpApiEndpoint.Json<typeof RepoConflict>, never, never>]>(endpoints_0: HttpApiEndpoint.HttpApiEndpoint<...>): HttpApiGroup.HttpApiGroup<...>

Add an HttpApiEndpoint to an HttpApiGroup.

add
(
const createRepo: HttpApiEndpoint.HttpApiEndpoint<"createRepo", "POST", "/repos", HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.Json<Schema.Struct<{
readonly name: Schema.String;
readonly description: Schema.optional<Schema.String>;
}>>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.Json<typeof CreateRepoResponse>, HttpApiEndpoint.Json<typeof RepoConflict>, never, never>
createRepo
) {}
export class
class RepoApi
RepoApi
extends
import HttpApi
HttpApi
.
const make: <"RepoApi">(identifier: "RepoApi") => HttpApi.HttpApi<"RepoApi", never>

Creates an empty HttpApi with the supplied identifier.

When to use

Use when you need to start defining an HTTP API, add groups with add or addHttpApi, provide endpoint implementations with HttpApiBuilder.group, and register the API with HttpApiBuilder.layer.

@categoryconstructors

@since4.0.0

make
("RepoApi").
HttpApi<"RepoApi", never>.add<readonly [typeof ReposGroup]>(groups_0: typeof ReposGroup): HttpApi.HttpApi<"RepoApi", typeof ReposGroup>

Add a HttpApiGroup to the HttpApi.

add
(
class ReposGroup
ReposGroup
) {}

Schema.Class gives you a runtime-validated class with an inferred TypeScript type. Schema.TaggedErrorClass gives you a typed error that becomes a discriminated union member on the client.

Create src/Worker.ts. The handler group is constructed with HttpApiBuilder.group (pure — safe inside the Worker’s Init phase), and the fetch field is the result of layering the API into an HttpEffect:

src/Worker.ts
import * as
import Cloudflare
Cloudflare
from "alchemy/Cloudflare";
import * as
import Effect
Effect
from "effect/Effect";
import * as
import Layer
Layer
from "effect/Layer";
import * as
import Path
Path
from "effect/Path";
import * as
import Etag
Etag
from "effect/unstable/http/Etag";
import * as
import HttpPlatform
HttpPlatform
from "effect/unstable/http/HttpPlatform";
import * as
import HttpRouter
HttpRouter
from "effect/unstable/http/HttpRouter";
import * as
import HttpApiBuilder
HttpApiBuilder
from "effect/unstable/httpapi/HttpApiBuilder";
import {
import CreateRepoResponse
CreateRepoResponse
,
import RepoApi
RepoApi
,
import RepoConflict
RepoConflict
} from "./Api.ts";
import {
import Repos
Repos
} from "./Repos.ts";
// Workers don't have a FileSystem, so HttpPlatform's file-response
// surface is stubbed. The repo API never serves files.
const
const HttpPlatformStub: Layer.Layer<HttpPlatform.HttpPlatform, never, never>
HttpPlatformStub
=
import Layer
Layer
.
const succeed: <HttpPlatform.HttpPlatform, {
readonly fileResponse: (path: string, options?: Options.WithContent & {
readonly bytesToRead?: SizeInput | undefined;
readonly chunkSize?: SizeInput | undefined;
readonly offset?: SizeInput | undefined;
}) => Effect.Effect<HttpServerResponse, PlatformError>;
readonly fileWebResponse: (file: HttpBody.FileLike, options?: Options.WithContent & {
readonly bytesToRead?: SizeInput | undefined;
readonly chunkSize?: SizeInput | undefined;
readonly offset?: SizeInput | undefined;
}) => Effect.Effect<HttpServerResponse>;
}>(service: Key<...>, resource: {
readonly fileResponse: (path: string, options?: Options.WithContent & {
readonly bytesToRead?: SizeInput | undefined;
readonly chunkSize?: SizeInput | undefined;
readonly offset?: SizeInput | undefined;
}) => Effect.Effect<HttpServerResponse, PlatformError>;
readonly fileWebResponse: (file: HttpBody.FileLike, options?: Options.WithContent & {
readonly bytesToRead?: SizeInput | undefined;
readonly chunkSize?: SizeInput | undefined;
readonly offset?: SizeInput | undefined;
}) => Effect.Effect<HttpServerResponse>;
}) => Layer.Layer<...> (+1 overload)

Constructs a layer that provides a single service from an already available value.

When to use

Use when you need a Layer that provides a service from an already constructed implementation without effectful acquisition.

Example (Creating a layer from a service implementation)

import { Context, Effect, Layer } from "effect"
class Database extends Context.Service<Database, {
readonly query: (sql: string) => Effect.Effect<string>
}>()("Database") {}
const DatabaseLive = Layer.succeed(Database, {
query: Effect.fn("Database.query")((sql: string) => Effect.succeed(`Query result: ${sql}`))
})

@seesync for constructing layers from lazy values

@categoryconstructors

@since2.0.0

succeed
(
import HttpPlatform
HttpPlatform
.
class HttpPlatform

Service for platform-specific HTTP response helpers, including file-backed server responses.

@categoryservices

@since4.0.0

HttpPlatform
, {
fileResponse: (path: string, options?: Options.WithContent & {
readonly bytesToRead?: SizeInput | undefined;
readonly chunkSize?: SizeInput | undefined;
readonly offset?: SizeInput | undefined;
}) => Effect.Effect<HttpServerResponse, PlatformError>
fileResponse
: () =>
import Effect
Effect
.
const die: (defect: unknown) => Effect.Effect<never>

Creates an effect that terminates a fiber with a specified error.

When to use

Use when you need an Effect to report an unrecoverable defect instead of a typed error.

Details

The die function is used to signal a defect, which represents a critical and unexpected error in the code. When invoked, it produces an effect that does not handle the error and instead terminates the fiber.

The error channel of the resulting effect is of type never, indicating that it cannot recover from this failure.

Example (Failing when division by zero)

import { Effect } from "effect"
const divide = (a: number, b: number) =>
b === 0
? Effect.die(new Error("Cannot divide by zero"))
: Effect.succeed(a / b)
// ┌─── Effect<number, never, never>
// ▼
const program = divide(1, 0)
Effect.runPromise(program).catch(console.error)
// Output:
// (FiberFailure) Error: Cannot divide by zero
// ...stack trace...

@categoryconstructors

@since2.0.0

die
("HttpPlatform.fileResponse not supported"),
fileWebResponse: (file: HttpBody.FileLike, options?: Options.WithContent & {
readonly bytesToRead?: SizeInput | undefined;
readonly chunkSize?: SizeInput | undefined;
readonly offset?: SizeInput | undefined;
}) => Effect.Effect<HttpServerResponse>
fileWebResponse
: () =>
import Effect
Effect
.
const die: (defect: unknown) => Effect.Effect<never>

Creates an effect that terminates a fiber with a specified error.

When to use

Use when you need an Effect to report an unrecoverable defect instead of a typed error.

Details

The die function is used to signal a defect, which represents a critical and unexpected error in the code. When invoked, it produces an effect that does not handle the error and instead terminates the fiber.

The error channel of the resulting effect is of type never, indicating that it cannot recover from this failure.

Example (Failing when division by zero)

import { Effect } from "effect"
const divide = (a: number, b: number) =>
b === 0
? Effect.die(new Error("Cannot divide by zero"))
: Effect.succeed(a / b)
// ┌─── Effect<number, never, never>
// ▼
const program = divide(1, 0)
Effect.runPromise(program).catch(console.error)
// Output:
// (FiberFailure) Error: Cannot divide by zero
// ...stack trace...

@categoryconstructors

@since2.0.0

die
("HttpPlatform.fileWebResponse not supported"),
});
export default class
class Worker
Worker
extends
import Cloudflare
Cloudflare
.
const Worker: <Worker>() => {
<Shape, PropsReq, InitReq>(id: string, props: InputProps<Cloudflare.WorkerProps<any, Cloudflare.WorkerAssetsConfig | undefined>, never> | Effect.Effect<Cloudflare.WorkerProps<any, Cloudflare.WorkerAssetsConfig | undefined>, ConfigError, PropsReq>, impl: Effect.Effect<Shape, ConfigError, InitReq>): Effect.Effect<Pipeable & ResourceLike<"Cloudflare.Worker", Cloudflare.WorkerProps<any, Cloudflare.WorkerAssetsConfig | undefined>, {
...;
}, {
...;
}, Cloudflare.Providers> & {
...;
} & {
...;
} & Rpc<...>, never, Cloudflare.Providers | ... 1 more ... | Exclude<...>> & (new (_: never) => MakeShape<...>);
<Shape, PropsReq>(id: string, props: InputProps<...> | Effect.Effect<...>): Effect.Effect<...> & ... 1 more ... & (<InitReq>(impl: Effect.Effect<...>) => Effect.Effect<...>);
} (+3 overloads)
Worker
<
class Worker
Worker
>()(
"Api",
{
main?: Input<string | undefined>

Path to the Worker's entry module. Bundled with rolldown before upload. Mutually exclusive with

script

— provide exactly one.

main
: import.

The type of import.meta.

If you need to declare that a given property exists on import.meta, this type may be augmented via interface merging.

meta
.
ImportMeta.filename: string

Alias of import.meta.path. Exists for Node.js compatibility

filename
,
compatibility?: Input<{
date?: string;
flags?: ("nodejs_compat" | "nodejs_als" | (string & {}))[];
} | undefined>
compatibility
: {
flags: "nodejs_compat"[]
flags
: ["nodejs_compat"],
date: string
date
: "2026-03-17" },
},
import Effect
Effect
.
const gen: <Effect.Effect<Cloudflare.ArtifactsClient, never, Cloudflare.ArtifactsBinding>, {
fetch: Effect.Effect<Effect.Effect<HttpServerResponse, HttpServerError, Scope | HttpServerRequest>, never, Scope | FileSystem>;
}>(f: () => Generator<Effect.Effect<Cloudflare.ArtifactsClient, never, Cloudflare.ArtifactsBinding>, {
fetch: Effect.Effect<Effect.Effect<HttpServerResponse, HttpServerError, Scope | HttpServerRequest>, never, Scope | FileSystem>;
}, never>) => Effect.Effect<...> (+1 overload)

Provides a way to write effectful code using generator functions, simplifying control flow and error handling.

When to use

Use when you want to write effectful code that looks and behaves like synchronous code, while still handling asynchronous tasks, errors, and complex control flow such as loops and conditions.

Generator functions work similarly to async/await but keep errors, requirements, and interruption in the Effect type. You can yield* values from effects and return the final result at the end.

Example (Sequencing effects with generators)

import { Data, Effect } from "effect"
class DiscountRateError extends Data.TaggedError("DiscountRateError")<{}> {}
const addServiceCharge = (amount: number) => amount + 1
const applyDiscount = (
total: number,
discountRate: number
): Effect.Effect<number, DiscountRateError> =>
discountRate === 0
? Effect.fail(new DiscountRateError())
: Effect.succeed(total - (total * discountRate) / 100)
const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))
const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))
export const program = Effect.gen(function*() {
const transactionAmount = yield* fetchTransactionAmount
const discountRate = yield* fetchDiscountRate
const discountedAmount = yield* applyDiscount(
transactionAmount,
discountRate
)
const finalAmount = addServiceCharge(discountedAmount)
return `Final amount to charge: ${finalAmount}`
})

@categoryconstructors

@since2.0.0

gen
(function* () {
const
const artifacts: Cloudflare.ArtifactsClient
artifacts
= yield*
import Cloudflare
Cloudflare
.
const Artifacts: {
(name: string, props?: Cloudflare.ArtifactsProps): Effect.Effect<Cloudflare.Artifacts, never, Stack | Stage>;
bind: (<Req = never>(args_0: Input<Cloudflare.Artifacts> | Effect.Effect<Cloudflare.Artifacts, never, Req>) => Effect.Effect<Cloudflare.ArtifactsClient, never, Cloudflare.ArtifactsBinding | Req>) & ((artifacts: Cloudflare.Artifacts) => Effect.Effect<any, any, any>);
}

Marker for a Cloudflare Artifacts namespace binding.

Artifacts namespaces are implicit (created on first repo write) and require no deploy-time provisioning, so this is a pure binding marker rather than a full Resource. The Worker provider sees this object in bindings: { ... } and emits the corresponding { type: "artifacts", name, namespace } binding to the script.

A Cloudflare Artifacts namespace — the top-level container for Git-compatible versioned repositories. See the

https://blog.cloudflare.com/artifacts-git-for-agents-beta/ Artifacts launch post

and

https://developers.cloudflare.com/artifacts/concepts/namespaces/ Namespaces docs

.

Namespaces on Cloudflare are implicit: there is no POST /namespaces endpoint. The namespace is conjured the first time a repo is created against it (either via the REST API or the Worker binding). Because of that, the Alchemy "resource" is a thin binding marker — there is nothing to provision at deploy time. Repos themselves are typically created at runtime through the bound Artifacts API.

@binding

@productArtifacts

@categoryDeveloper Platform

@sectionDeclaring a Namespace

@example

Default namespace (a unique physical name is generated)

const Repos = Cloudflare.Artifacts("Repos");

@example

Override the namespace name (must be lowercase, 3–63 chars)

const Repos = Cloudflare.Artifacts("Repos", { namespace: "starter-repos" });

@sectionBinding to a Worker

@example

Wiring it into a Worker

export const Worker = Cloudflare.Worker("Worker", {
main: "./src/worker.ts",
bindings: { Repos },
});
export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;
// { Repos: Artifacts }

@example

Async-style worker

export default {
async fetch(request: Request, env: WorkerEnv) {
const repo = await env.Repos.create("starter-repo");
return Response.json({ remote: repo.remote, token: repo.token });
},
};

@example

Effect-style worker

const artifacts = yield* Cloudflare.Artifacts.bind(Repos);
const repo = yield* artifacts.create("starter-repo", {
setDefaultBranch: "main",
});

Artifacts
.
bind: <never>(args_0: Input<Cloudflare.Artifacts> | Effect.Effect<Cloudflare.Artifacts, never, never>) => Effect.Effect<Cloudflare.ArtifactsClient, never, Cloudflare.ArtifactsBinding> (+1 overload)
bind
(
import Repos
Repos
);
const
const handlers: Layer.Layer<ApiGroup<string, never>, never, never>
handlers
=
import HttpApiBuilder
HttpApiBuilder
.
const group: <string, Any, never, unknown>(api: HttpApi<string, Any>, groupName: never, build: (handlers: HttpApiBuilder.Handlers<R, Endpoints extends Any = never>.FromGroup<never>) => "Must return the implemented handlers") => Layer.Layer<ApiGroup<string, never>, never, never>

Create a Layer that implements all endpoints in an HttpApi group.

Details

The build function receives an unimplemented Handlers instance that can be used to add handlers to the group. Implement endpoints with handlers.handle.

@categoryhandlers

@since4.0.0

group
(
import RepoApi
RepoApi
, "repos", (
h: HttpApiBuilder.Handlers.FromGroup<never>
h
) =>
h: HttpApiBuilder.Handlers.FromGroup<never>
h
.
Handlers<never, never>.handle<never, unknown>(name: never, handler: HandlerWithName<never, never, never, unknown>, options?: {
readonly uninterruptible?: boolean | undefined;
} | undefined): HttpApiBuilder.Handlers<HttpRouter.Request<"Requires", unknown>, never>

Add the implementation for an HttpApiEndpoint to a Handlers group.

handle
("createRepo", ({
payload: never
payload
}) =>
const artifacts: Cloudflare.ArtifactsClient
artifacts
.
ArtifactsClient.create(name: string, opts?: Cloudflare.ArtifactsCreateOptions): Effect.Effect<ArtifactsCreateRepoResult, Cloudflare.ArtifactsError, RuntimeContext>
create
(
payload: never
payload
.
any
name
, {
description?: string | undefined
description
:
payload: never
payload
.
any
description
,
setDefaultBranch?: string | undefined
setDefaultBranch
: "main",
})
.
Pipeable.pipe<Effect.Effect<ArtifactsCreateRepoResult, Cloudflare.ArtifactsError, RuntimeContext>, Effect.Effect<any, Cloudflare.ArtifactsError, RuntimeContext>, Effect.Effect<any, any, RuntimeContext>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<ArtifactsCreateRepoResult, Cloudflare.ArtifactsError, RuntimeContext>) => Effect.Effect<any, Cloudflare.ArtifactsError, RuntimeContext>, bc: (_: Effect.Effect<...>) => Effect.Effect<...>): Effect.Effect<...> (+21 overloads)
pipe
(
import Effect
Effect
.
const map: <ArtifactsCreateRepoResult, any>(f: (a: ArtifactsCreateRepoResult) => any) => <E, R>(self: Effect.Effect<ArtifactsCreateRepoResult, E, R>) => Effect.Effect<any, E, R> (+1 overload)

Transforms the value inside an effect by applying a function to it.

When to use

Use to transform an effect's success value with a function that returns a plain value, producing a new effect without changing the original effect's typed error or context requirements.

Details

map takes a function and applies it to the value contained within an effect, creating a new effect with the transformed value.

It's important to note that effects are immutable, meaning that the original effect is not modified. Instead, a new effect is returned with the updated value.

Example (Syntax)

import { Effect, pipe } from "effect"
const myEffect = Effect.succeed(1)
const transformation = (n: number) => n + 1
const mappedWithPipe = pipe(myEffect, Effect.map(transformation))
const mappedWithDataFirst = Effect.map(myEffect, transformation)
const mappedWithMethod = myEffect.pipe(Effect.map(transformation))

Example (Adding a service charge)

import { Effect, pipe } from "effect"
const addServiceCharge = (amount: number) => amount + 1
const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))
const finalAmount = pipe(
fetchTransactionAmount,
Effect.map(addServiceCharge)
)
Effect.runPromise(finalAmount).then(console.log)
// Output: 101

@seemapError for a version that operates on the error channel.

@seemapBoth for a version that operates on both channels.

@seeflatMap or andThen for a version that can return a new effect.

@categorymapping

@since2.0.0

map
(
(
c: ArtifactsCreateRepoResult
c
) =>
new
import CreateRepoResponse
CreateRepoResponse
({
name: string
name
:
c: ArtifactsCreateRepoResult
c
.
ArtifactsCreateRepoResult.name: string

Repository name.

name
,
remote: string
remote
:
c: ArtifactsCreateRepoResult
c
.
ArtifactsCreateRepoResult.remote: string

HTTPS git remote URL.

remote
,
token: string
token
:
c: ArtifactsCreateRepoResult
c
.
ArtifactsCreateRepoResult.token: string

Plaintext access token (only returned at creation time).

token
,
defaultBranch: string
defaultBranch
:
c: ArtifactsCreateRepoResult
c
.
ArtifactsCreateRepoResult.defaultBranch: string

Default branch name.

defaultBranch
,
}),
),
import Effect
Effect
.
const catchTag: <"ArtifactsError", Cloudflare.ArtifactsError, never, any, never, unassigned, never, never>(k: "ArtifactsError", f: (e: Cloudflare.ArtifactsError) => Effect.Effect<never, any, never>, orElse?: ((e: never) => Effect.Effect<unassigned, never, never>) | undefined) => <A, R>(self: Effect.Effect<A, Cloudflare.ArtifactsError, R>) => Effect.Effect<A, any, R> (+1 overload)

Catches and handles specific errors by their _tag field, which is used as a discriminator.

When to use

Use when you need to recover from one specific tagged error in an effect error channel.

Details

The error type must have a readonly _tag field. catchTag matches that field and only handles errors with the requested tag.

Example (Handling a tagged error)

import { Effect } from "effect"
class NetworkError {
readonly _tag = "NetworkError"
constructor(readonly message: string) {}
}
class ValidationError {
readonly _tag = "ValidationError"
constructor(readonly message: string) {}
}
declare const task: Effect.Effect<string, NetworkError | ValidationError>
const program = Effect.catchTag(
task,
"NetworkError",
(error) => Effect.succeed(`Recovered from network error: ${error.message}`)
)

@seecatchTags for handling multiple tagged errors in one call

@seecatchIf for recovering from errors that match a predicate

@categoryerror handling

@since2.0.0

catchTag
("ArtifactsError", (
err: Cloudflare.ArtifactsError
err
) =>
import Effect
Effect
.
const fail: <any>(error: any) => Effect.Effect<never, any, never>

Creates an Effect that represents a recoverable error.

When to use

Use to explicitly signal a recoverable error in an Effect.

Details

The error keeps propagating unless it is handled. You can handle tagged errors with functions like

catchTag

or

catchTags

.

Example (Creating a failed effect)

import { Data, Effect } from "effect"
class OperationFailedError extends Data.TaggedError("OperationFailedError")<{}> {}
// ┌─── Effect<never, OperationFailedError, never>
// ▼
const failure = Effect.fail(
new OperationFailedError()
)

@seesucceed to create an effect that represents a successful value.

@categoryconstructors

@since2.0.0

fail
(new
import RepoConflict
RepoConflict
({
message: string
message
:
err: Cloudflare.ArtifactsError
err
.
Error.message: string
message
})),
),
),
),
);
return {
fetch: Effect.Effect<Effect.Effect<HttpServerResponse, HttpServerError, Scope | HttpServerRequest>, never, Scope | FileSystem>
fetch
:
import HttpApiBuilder
HttpApiBuilder
.
const layer: <string, Any>(api: HttpApi<string, Any>, options?: {
readonly openapiPath?: `/${string}` | undefined;
}) => Layer.Layer<never, never, HttpPlatform.HttpPlatform | FileSystem | Path.Path | Etag.Generator | HttpRouter.HttpRouter>

Registers an HttpApi with a HttpRouter.

@categoryconstructors

@since4.0.0

layer
(
import RepoApi
RepoApi
).
Pipeable.pipe<Layer.Layer<never, never, HttpPlatform.HttpPlatform | FileSystem | Path.Path | Etag.Generator | HttpRouter.HttpRouter>, Layer.Layer<never, never, HttpPlatform.HttpPlatform | FileSystem | Path.Path | Etag.Generator | HttpRouter.HttpRouter>, Layer.Layer<never, never, FileSystem | HttpRouter.HttpRouter>, Effect.Effect<Effect.Effect<HttpServerResponse, HttpServerError, Scope | HttpServerRequest>, never, Scope | FileSystem>>(this: Layer.Layer<...>, ab: (_: Layer.Layer<...>) => Layer.Layer<...>, bc: (_: Layer.Layer<...>) => Layer.Layer<...>, cd: (_: Layer.Layer<...>) => Effect.Effect<...>): Effect.Effect<...> (+21 overloads)
pipe
(
import Layer
Layer
.
const provide: <never, never, ApiGroup<string, never>>(that: Layer.Layer<ApiGroup<string, never>, never, never>) => <RIn2, E2, ROut2>(self: Layer.Layer<ROut2, E2, RIn2>) => Layer.Layer<ROut2, E2, Exclude<RIn2, ApiGroup<string, never>>> (+3 overloads)

Feeds the output services of the dependency layer into the requirements of this layer, returning a layer that only provides the services from this layer.

When to use

Use when you need to hide an implementation dependency layer from callers.

Details

In serviceLayer.pipe(Layer.provide(dependencyLayer)), the dependency layer is built first and is used to satisfy the requirements of serviceLayer.

Example (Providing layer dependencies)

import { Context, Effect, Layer } from "effect"
class Database extends Context.Service<Database, {
readonly query: (sql: string) => Effect.Effect<string>
}>()("Database") {}
class UserService extends Context.Service<UserService, {
readonly getUser: (id: string) => Effect.Effect<{
id: string
name: string
}>
}>()("UserService") {}
class Logger extends Context.Service<Logger, {
readonly log: (msg: string) => Effect.Effect<void>
}>()("Logger") {}
// Create dependency layers
const databaseLayer = Layer.succeed(Database, {
query: Effect.fn("Database.query")((sql: string) => Effect.succeed(`DB: ${sql}`))
})
const loggerLayer = Layer.succeed(Logger, {
log: Effect.fn("Logger.log")((msg: string) => Effect.sync(() => console.log(`[LOG] ${msg}`)))
})
// UserService depends on Database and Logger
const userServiceLayer = Layer.effect(UserService, Effect.gen(function*() {
const database = yield* Database
const logger = yield* Logger
return {
getUser: Effect.fn("UserService.getUser")(function*(id: string) {
yield* logger.log(`Looking up user ${id}`)
const result = yield* database.query(
`SELECT * FROM users WHERE id = ${id}`
)
return { id, name: result }
})
}
}))
// Provide dependencies to UserService layer
const userServiceWithDependencies = userServiceLayer.pipe(
Layer.provide(Layer.mergeAll(databaseLayer, loggerLayer))
)
// Now UserService layer has no dependencies
const program = Effect.gen(function*() {
const userService = yield* UserService
return yield* userService.getUser("123")
}).pipe(
Effect.provide(userServiceWithDependencies)
)

@seeprovideMerge for retaining the dependency services

@categoryproviding services

@since2.0.0

provide
(
const handlers: Layer.Layer<ApiGroup<string, never>, never, never>
handlers
),
import Layer
Layer
.
const provide: <[Layer.Layer<Etag.Generator, never, never>, Layer.Layer<HttpPlatform.HttpPlatform, never, never>, Layer.Layer<Path.Path, never, never>]>(that: [Layer.Layer<Etag.Generator, never, never>, Layer.Layer<HttpPlatform.HttpPlatform, never, never>, Layer.Layer<Path.Path, never, never>]) => <A, E, R>(self: Layer.Layer<A, E, R>) => Layer.Layer<A, E, Exclude<R, HttpPlatform.HttpPlatform | Path.Path | Etag.Generator>> (+3 overloads)

Feeds the output services of the dependency layer into the requirements of this layer, returning a layer that only provides the services from this layer.

When to use

Use when you need to hide an implementation dependency layer from callers.

Details

In serviceLayer.pipe(Layer.provide(dependencyLayer)), the dependency layer is built first and is used to satisfy the requirements of serviceLayer.

Example (Providing layer dependencies)

import { Context, Effect, Layer } from "effect"
class Database extends Context.Service<Database, {
readonly query: (sql: string) => Effect.Effect<string>
}>()("Database") {}
class UserService extends Context.Service<UserService, {
readonly getUser: (id: string) => Effect.Effect<{
id: string
name: string
}>
}>()("UserService") {}
class Logger extends Context.Service<Logger, {
readonly log: (msg: string) => Effect.Effect<void>
}>()("Logger") {}
// Create dependency layers
const databaseLayer = Layer.succeed(Database, {
query: Effect.fn("Database.query")((sql: string) => Effect.succeed(`DB: ${sql}`))
})
const loggerLayer = Layer.succeed(Logger, {
log: Effect.fn("Logger.log")((msg: string) => Effect.sync(() => console.log(`[LOG] ${msg}`)))
})
// UserService depends on Database and Logger
const userServiceLayer = Layer.effect(UserService, Effect.gen(function*() {
const database = yield* Database
const logger = yield* Logger
return {
getUser: Effect.fn("UserService.getUser")(function*(id: string) {
yield* logger.log(`Looking up user ${id}`)
const result = yield* database.query(
`SELECT * FROM users WHERE id = ${id}`
)
return { id, name: result }
})
}
}))
// Provide dependencies to UserService layer
const userServiceWithDependencies = userServiceLayer.pipe(
Layer.provide(Layer.mergeAll(databaseLayer, loggerLayer))
)
// Now UserService layer has no dependencies
const program = Effect.gen(function*() {
const userService = yield* UserService
return yield* userService.getUser("123")
}).pipe(
Effect.provide(userServiceWithDependencies)
)

@seeprovideMerge for retaining the dependency services

@categoryproviding services

@since2.0.0

provide
([
import Etag
Etag
.
const layer: Layer.Layer<Etag.Generator, never, never>

Layer that provides a Generator which produces strong ETags from file size and modification time metadata.

When to use

Use when you need the Generator service to produce strong ETags and file size plus modification time reliably change for every byte-level change.

Gotchas

This layer marks metadata-derived tags as strong. If the underlying storage can update file contents without changing the recorded size or modification time, those tags can stop representing byte-for-byte identity.

@seelayerWeak for weak metadata-derived ETags when byte-for-byte identity is not required

@seeGenerator for the service provided by this layer

@categorylayers

@since4.0.0

layer
,
const HttpPlatformStub: Layer.Layer<HttpPlatform.HttpPlatform, never, never>
HttpPlatformStub
,
import Path
Path
.
const layer: Layer.Layer<Path.Path, never, never>

Layer that provides the built-in POSIX Path implementation.

When to use

Use when you need an effect that requires the Path service to run with the built-in POSIX path implementation.

Details

The layer provides a static service whose separator is / and whose operations use POSIX path semantics.

@seePath for accessing the Path service from an effect

@categorylayers

@since4.0.0

layer
]),
import HttpRouter
HttpRouter
.
const toHttpEffect: <A, E, R>(appLayer: Layer.Layer<A, E, R>) => Effect.Effect<Effect.Effect<HttpServerResponse, HttpRouter.Request.Only<"Error", R> | HttpRouter.Request.Only<"GlobalRequires", R> | HttpServerError, Scope | HttpServerRequest | HttpRouter.Request.Only<"Requires", R> | HttpRouter.Request.Only<"GlobalRequires", R>>, HttpRouter.Request.Without<E>, Exclude<HttpRouter.Request.Without<R>, HttpRouter.HttpRouter> | Scope>

Builds an application layer with a router and returns the router as an HTTP handler effect.

Details

The returned effect handles the current HttpServerRequest in the current Scope; route request markers are converted into the ordinary requirements of the returned handler.

@categoryHttpRouter

@since4.0.0

toHttpEffect
,
),
};
}).
Pipeable.pipe<Effect.Effect<{
fetch: Effect.Effect<Effect.Effect<HttpServerResponse, HttpServerError, Scope | HttpServerRequest>, never, Scope | FileSystem>;
}, never, Cloudflare.ArtifactsBinding>, Effect.Effect<{
fetch: Effect.Effect<Effect.Effect<HttpServerResponse, HttpServerError, Scope | HttpServerRequest>, never, Scope | FileSystem>;
}, never, Cloudflare.WorkerEnvironment | Cloudflare.ArtifactsBindingPolicy>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<...>) => Effect.Effect<...>): Effect.Effect<...> (+21 overloads)
pipe
(
import Effect
Effect
.
const provide: <Cloudflare.ArtifactsBinding, never, Cloudflare.WorkerEnvironment | Cloudflare.ArtifactsBindingPolicy>(layer: Layer.Layer<Cloudflare.ArtifactsBinding, never, Cloudflare.WorkerEnvironment | Cloudflare.ArtifactsBindingPolicy>, options?: {
readonly local?: boolean | undefined;
} | undefined) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Cloudflare.WorkerEnvironment | Cloudflare.ArtifactsBindingPolicy | Exclude<...>> (+5 overloads)

Provides dependencies to an effect using layers or a context. Use options.local to build the layer every time; by default, layers are shared between provide calls.

Example (Providing dependencies with a layer)

import { Context, Effect, Layer } from "effect"
interface Database {
readonly query: (sql: string) => Effect.Effect<string>
}
const Database = Context.Service<Database>("Database")
const DatabaseLive = Layer.succeed(Database)({
query: Effect.fn("Database.query")((sql: string) => Effect.succeed(`Result for: ${sql}`))
})
const program = Effect.gen(function*() {
const db = yield* Database
return yield* db.query("SELECT * FROM users")
})
const provided = Effect.provide(program, DatabaseLive)
Effect.runPromise(provided).then(console.log)
// Output: "Result for: SELECT * FROM users"

@categoryenvironment

@since2.0.0

provide
(
import Cloudflare
Cloudflare
.
const ArtifactsBindingLive: Layer.Layer<Cloudflare.ArtifactsBinding, never, Cloudflare.WorkerEnvironment | Cloudflare.ArtifactsBindingPolicy>
ArtifactsBindingLive
)),
) {}

The handler returns a CreateRepoResponse instance — Schema.Class expects an actual instance, not a plain object. Errors from artifacts.create (the only declared error path) are translated to RepoConflict; anything else dies and surfaces as a 500.

Same Test.make shape as Add a Durable Object, but this time the test calls the worker through HttpApiClient.make — the same RepoApi value drives the typed client:

test/integ.test.ts
import * as Cloudflare from "alchemy/Cloudflare";
import * as Test from "alchemy/Test/Bun";
import { expect } from "bun:test";
import * as Effect from "effect/Effect";
import * as HttpApiClient from "effect/unstable/httpapi/HttpApiClient";
import { RepoApi } from "../src/Api.ts";
import Stack from "../alchemy.run.ts";
const { test, beforeAll, afterAll, deploy, destroy } = Test.make({
providers: Cloudflare.providers(),
state: Cloudflare.state(),
});
const stack = beforeAll(deploy(Stack));
afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack));
const repoName = `tutorial-${Date.now().toString(36)}`;

Add the first assertion. client.repos.createRepo returns Effect<CreateRepoResponse, RepoConflict | HttpClientError> — fields are typed straight from the schema:

const repoName = `tutorial-${Date.now().toString(36)}`;
test(
"repo lifecycle",
Effect.gen(function* () {
const { url } = yield* stack;
const client = yield* HttpApiClient.make(RepoApi, { baseUrl: url });
const created = yield* client.repos.createRepo({
payload: { name: repoName, description: "tutorial repo" },
});
expect(created.name).toBe(repoName);
expect(created.remote).toBeString();
expect(created.token).toBeString();
}),
{ timeout: 120_000 },
);
Terminal window
bun test

Alchemy deploys the Worker, the test posts to /repos, and you get back a remote and token. You could git clone against them right now.

artifacts.get(name) returns an opaque RPC stub — useful for createToken later, but its fields aren’t enumerable. To return repo info as JSON, use artifacts.list(...) and find the entry by name; every record in the list is a plain object.

Add a RepoInfo schema and a getRepo endpoint to the API:

src/Api.ts
import * as Schema from "effect/Schema";
import * as HttpApi from "effect/unstable/httpapi/HttpApi";
import * as HttpApiEndpoint from "effect/unstable/httpapi/HttpApiEndpoint";
import * as HttpApiGroup from "effect/unstable/httpapi/HttpApiGroup";
export class RepoInfo extends Schema.Class<RepoInfo>("RepoInfo")({
id: Schema.String,
name: Schema.String,
description: Schema.NullOr(Schema.String),
defaultBranch: Schema.String,
remote: Schema.String,
status: Schema.String,
readOnly: Schema.Boolean,
createdAt: Schema.String,
updatedAt: Schema.String,
lastPushAt: Schema.NullOr(Schema.String),
}) {}
export class CreateRepoResponse extends Schema.Class<CreateRepoResponse>(
"CreateRepoResponse",
)({ /* … */ }) {}
export class RepoNotFound extends Schema.TaggedErrorClass<RepoNotFound>()(
"RepoNotFound",
{ name: Schema.String },
) {}
export class RepoConflict extends Schema.TaggedErrorClass<RepoConflict>()(
"RepoConflict",
{ message: Schema.String },
) {}
export const createRepo = HttpApiEndpoint.post("createRepo", "/repos", { /* … */ });
export const getRepo = HttpApiEndpoint.get("getRepo", "/repos/:name", {
params: Schema.Struct({ name: Schema.String }),
success: RepoInfo,
error: RepoNotFound,
});
export class ReposGroup extends HttpApiGroup.make("repos").add(createRepo) {}
export class ReposGroup extends HttpApiGroup.make("repos")
.add(createRepo)
.add(getRepo) {}
export class RepoApi extends HttpApi.make("RepoApi").add(ReposGroup) {}

Implement the handler in the Worker, and factor out a findRepo helper since several handlers will need to look up a repo:

// src/Worker.ts — inside Effect.gen
const artifacts = yield* Cloudflare.Artifacts.bind(Repos);
const findRepo = (name: string) =>
artifacts.list({ limit: 100 }).pipe(
Effect.flatMap((res) => {
const found = res.repos.find((r: { name: string }) => r.name === name);
return found
? Effect.succeed(found)
: Effect.fail(new RepoNotFound({ name }));
}),
Effect.catchTag("ArtifactsError", () =>
Effect.fail(new RepoNotFound({ name })),
),
);
const handlers = HttpApiBuilder.group(RepoApi, "repos", (h) =>
h
.handle("createRepo", ({ payload }) =>
// …existing
)
.handle("getRepo", ({ params }) =>
findRepo(params.name).pipe(
Effect.map(
(found) =>
new RepoInfo({
id: found.id,
name: found.name,
description: found.description ?? null,
defaultBranch: found.defaultBranch,
remote: found.remote,
status: found.status,
readOnly: found.readOnly,
createdAt: found.createdAt,
updatedAt: found.updatedAt,
lastPushAt: found.lastPushAt ?? null,
}),
),
),
),
);

Extend the test:

test(
"repo lifecycle",
Effect.gen(function* () {
const { url } = yield* stack;
const client = yield* HttpApiClient.make(RepoApi, { baseUrl: url });
const created = yield* client.repos.createRepo({
payload: { name: repoName, description: "tutorial repo" },
});
expect(created.name).toBe(repoName);
expect(created.remote).toBeString();
expect(created.token).toBeString();
const info = yield* client.repos.getRepo({ params: { name: repoName } });
expect(info.name).toBe(repoName);
expect(info.defaultBranch).toBe("main");
expect(info.description).toBe("tutorial repo");
}),
{ timeout: 120_000 },
);
Terminal window
bun test

The token returned by create expires. Clients that already know a repo’s name should be able to ask for a new one without recreating the repo. Add a cloneToken endpoint:

src/Api.ts
export class CloneToken extends Schema.Class<CloneToken>("CloneToken")({
id: Schema.String,
plaintext: Schema.String,
scope: Schema.Literals(["read", "write"]),
expiresAt: Schema.String,
}) {}
export const getRepo = HttpApiEndpoint.get("getRepo", "/repos/:name", { /* … */ });
export const cloneToken = HttpApiEndpoint.post(
"cloneToken",
"/repos/:name/clone-token",
{
params: Schema.Struct({ name: Schema.String }),
payload: Schema.Struct({
scope: Schema.optional(Schema.Literals(["read", "write"])),
ttl: Schema.optional(Schema.Number),
}),
success: CloneToken,
error: RepoNotFound,
},
);
export class ReposGroup extends HttpApiGroup.make("repos")
.add(createRepo)
.add(getRepo)
.add(cloneToken) {}

repo.createToken(scope, ttl) on the runtime stub returns { id, plaintext, scope, expiresAt } — wrap it in a CloneToken instance:

.handle("getRepo", ({ params }) => /* … */)
.handle("cloneToken", ({ params, payload }) =>
artifacts.get(params.name).pipe(
Effect.flatMap((handle) =>
handle.createToken(payload.scope ?? "read", payload.ttl ?? 3600),
),
Effect.map(
(t) =>
new CloneToken({
id: t.id,
plaintext: t.plaintext,
scope: t.scope as "read" | "write",
expiresAt: t.expiresAt,
}),
),
Effect.catchTag("ArtifactsError", () =>
Effect.fail(new RepoNotFound({ name: params.name })),
),
),
),
test(
"repo lifecycle",
Effect.gen(function* () {
const { url } = yield* stack;
const client = yield* HttpApiClient.make(RepoApi, { baseUrl: url });
const created = yield* client.repos.createRepo({
payload: { name: repoName, description: "tutorial repo" },
});
expect(created.name).toBe(repoName);
expect(created.remote).toBeString();
expect(created.token).toBeString();
const info = yield* client.repos.getRepo({ params: { name: repoName } });
expect(info.name).toBe(repoName);
expect(info.defaultBranch).toBe("main");
expect(info.description).toBe("tutorial repo");
const token = yield* client.repos.cloneToken({
params: { name: repoName },
payload: { scope: "read", ttl: 600 },
});
expect(token.plaintext).toBeString();
expect(token.scope).toBe("read");
}),
{ timeout: 120_000 },
);

That covers the Git half. Artifacts is now storing history and handing out tokens. Next we’ll add the metadata that lives around the repo.

Artifacts owns commits, refs, and the clone protocol. It does not store the things a GitHub-like API needs alongside that — descriptions you can rename, topics, stars. The Repo Durable Object represents one repository: a single addressable instance per repo name with its own transactional storage.

Start with the smallest possible DO — empty public API, no state:

src/Repo.ts
import * as
import Cloudflare
Cloudflare
from "alchemy/Cloudflare";
import * as
import Effect
Effect
from "effect/Effect";
export default class
class Repo
Repo
extends
import Cloudflare
Cloudflare
.
const DurableObjectNamespace: Cloudflare.DurableObjectNamespaceClass
<Repo>() => <Shape, InitReq>(name: string, impl: Effect.Effect<Effect.Effect<Shape, never, Cloudflare.DurableObjectServices>, never, InitReq>) => Effect.Effect<Cloudflare.DurableObjectNamespace<Repo>, never, Cloudflare.Worker | Exclude<InitReq, Cloudflare.DurableObjectServices>> & (new (_: never) => Shape) (+3 overloads)
DurableObjectNamespace
<
class Repo
Repo
>()(
"Repo",
import Effect
Effect
.
const gen: <never, Effect.Effect<{}, never, never>>(f: () => Generator<never, Effect.Effect<{}, never, never>, never>) => Effect.Effect<Effect.Effect<{}, never, never>, never, never> (+1 overload)

Provides a way to write effectful code using generator functions, simplifying control flow and error handling.

When to use

Use when you want to write effectful code that looks and behaves like synchronous code, while still handling asynchronous tasks, errors, and complex control flow such as loops and conditions.

Generator functions work similarly to async/await but keep errors, requirements, and interruption in the Effect type. You can yield* values from effects and return the final result at the end.

Example (Sequencing effects with generators)

import { Data, Effect } from "effect"
class DiscountRateError extends Data.TaggedError("DiscountRateError")<{}> {}
const addServiceCharge = (amount: number) => amount + 1
const applyDiscount = (
total: number,
discountRate: number
): Effect.Effect<number, DiscountRateError> =>
discountRate === 0
? Effect.fail(new DiscountRateError())
: Effect.succeed(total - (total * discountRate) / 100)
const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))
const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))
export const program = Effect.gen(function*() {
const transactionAmount = yield* fetchTransactionAmount
const discountRate = yield* fetchDiscountRate
const discountedAmount = yield* applyDiscount(
transactionAmount,
discountRate
)
const finalAmount = addServiceCharge(discountedAmount)
return `Final amount to charge: ${finalAmount}`
})

@categoryconstructors

@since2.0.0

gen
(function* () {
return
import Effect
Effect
.
const gen: <never, {}>(f: () => Generator<never, {}, never>) => Effect.Effect<{}, never, never> (+1 overload)

Provides a way to write effectful code using generator functions, simplifying control flow and error handling.

When to use

Use when you want to write effectful code that looks and behaves like synchronous code, while still handling asynchronous tasks, errors, and complex control flow such as loops and conditions.

Generator functions work similarly to async/await but keep errors, requirements, and interruption in the Effect type. You can yield* values from effects and return the final result at the end.

Example (Sequencing effects with generators)

import { Data, Effect } from "effect"
class DiscountRateError extends Data.TaggedError("DiscountRateError")<{}> {}
const addServiceCharge = (amount: number) => amount + 1
const applyDiscount = (
total: number,
discountRate: number
): Effect.Effect<number, DiscountRateError> =>
discountRate === 0
? Effect.fail(new DiscountRateError())
: Effect.succeed(total - (total * discountRate) / 100)
const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))
const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))
export const program = Effect.gen(function*() {
const transactionAmount = yield* fetchTransactionAmount
const discountRate = yield* fetchDiscountRate
const discountedAmount = yield* applyDiscount(
transactionAmount,
discountRate
)
const finalAmount = addServiceCharge(discountedAmount)
return `Final amount to charge: ${finalAmount}`
})

@categoryconstructors

@since2.0.0

gen
(function* () {
return {};
});
}),
) {}

Each DO instance has its own SQLite-backed key/value storage. Pull the current metadata out of storage in the inner init so it survives restarts and hibernation:

export type Meta = {
description: string;
topics: string[];
stars: number;
createdAt: number;
};
export default class Repo extends Cloudflare.DurableObjectNamespace<Repo>()(
"Repo",
Effect.gen(function* () {
return Effect.gen(function* () {
const state = yield* Cloudflare.DurableObjectState;
let meta = (yield* state.storage.get<Meta>("meta")) ?? null;
return {};
});
}),
) {}

meta is null until the repo is initialized.

Any function returned from the inner Effect that produces an Effect becomes a typed RPC method. Add one to seed the metadata the first time a repo is created, and one to read it:

return Effect.gen(function* () {
const state = yield* Cloudflare.DurableObjectState;
let meta = (yield* state.storage.get<Meta>("meta")) ?? null;
const ensure = Effect.gen(function* () {
if (meta === null) {
return yield* Effect.fail(new Error("repo not initialized"));
}
return meta;
});
return {};
return {
init: (description: string) =>
Effect.gen(function* () {
if (meta !== null) return meta;
meta = { description, topics: [], stars: 0, createdAt: Date.now() };
yield* state.storage.put("meta", meta);
return meta;
}),
get: () => ensure,
};
});

Add a Metadata schema to Api.ts and extend RepoInfo with a nullable metadata field:

src/Api.ts
export class Metadata extends Schema.Class<Metadata>("Metadata")({
description: Schema.String,
topics: Schema.Array(Schema.String),
stars: Schema.Number,
createdAt: Schema.Number,
}) {}
export class RepoInfo extends Schema.Class<RepoInfo>("RepoInfo")({
id: Schema.String,
name: Schema.String,
description: Schema.NullOr(Schema.String),
defaultBranch: Schema.String,
remote: Schema.String,
status: Schema.String,
readOnly: Schema.Boolean,
createdAt: Schema.String,
updatedAt: Schema.String,
lastPushAt: Schema.NullOr(Schema.String),
metadata: Schema.NullOr(Metadata),
}) {}

Yield the Repo class in the Worker’s init phase. The handle is a DO namespace — repos.getByName(name) returns a typed RPC stub for that repo’s instance:

src/Worker.ts
import Repo from "./Repo.ts";
import {
CloneToken,
CreateRepoResponse,
Metadata,
RepoApi,
RepoConflict,
RepoInfo,
RepoNotFound,
} from "./Api.ts";
Effect.gen(function* () {
const artifacts = yield* Cloudflare.Artifacts.bind(Repos);
const repos = yield* Repo;
// …findRepo helper

Now extend the handlers — createRepo calls init after the repo is created, getRepo reads metadata and merges it:

.handle("createRepo", ({ payload }) =>
artifacts
.create(payload.name, {
description: payload.description,
setDefaultBranch: "main",
})
.pipe(
Effect.tap(() =>
repos
.getByName(payload.name)
.init(payload.description ?? "")
.pipe(Effect.orDie),
),
Effect.map(
(c) =>
new CreateRepoResponse({
name: c.name,
remote: c.remote,
token: c.token,
defaultBranch: c.defaultBranch,
}),
),
Effect.catchTag("ArtifactsError", (err) =>
Effect.fail(new RepoConflict({ message: err.message })),
),
),
)
.handle("getRepo", ({ params }) =>
findRepo(params.name).pipe(
Effect.flatMap((found) =>
repos
.getByName(params.name)
.get()
.pipe(
Effect.catch(() => Effect.succeed(null)),
Effect.map((m) => ({ found, meta: m })),
),
),
Effect.map(
(found) =>
new RepoInfo({
Effect.map(
({ found, meta }) =>
new RepoInfo({
id: found.id,
name: found.name,
description: found.description ?? null,
defaultBranch: found.defaultBranch,
remote: found.remote,
status: found.status,
readOnly: found.readOnly,
createdAt: found.createdAt,
updatedAt: found.updatedAt,
lastPushAt: found.lastPushAt ?? null,
metadata: meta ? new Metadata(meta) : null,
}),
),
),
),

The DO’s get() fails with a plain Error when the repo wasn’t initialised — recover with Effect.catch so the route still returns the Artifacts info even if the DO has no metadata yet.

Update the test — info.metadata is now typed as Metadata | null:

const info = yield* client.repos.getRepo({ params: { name: repoName } });
expect(info.name).toBe(repoName);
expect(info.defaultBranch).toBe("main");
expect(info.description).toBe("tutorial repo");
expect(info.metadata?.description).toBe("tutorial repo");
expect(info.metadata?.stars).toBe(0);
Terminal window
bun test

Add an update method to the DO:

return {
init: (description: string) =>
Effect.gen(function* () {
if (meta !== null) return meta;
meta = { description, topics: [], stars: 0, createdAt: Date.now() };
yield* state.storage.put("meta", meta);
return meta;
}),
get: () => ensure,
update: (patch: Partial<Pick<Meta, "description" | "topics">>) =>
Effect.gen(function* () {
const current = yield* ensure;
meta = { ...current, ...patch };
yield* state.storage.put("meta", meta);
return meta;
}),
};

Add an updateRepo endpoint to the API:

src/Api.ts
export const updateRepo = HttpApiEndpoint.patch(
"updateRepo",
"/repos/:name",
{
params: Schema.Struct({ name: Schema.String }),
payload: Schema.Struct({
description: Schema.optional(Schema.String),
topics: Schema.optional(Schema.Array(Schema.String)),
}),
success: Metadata,
error: RepoNotFound,
},
);
export class ReposGroup extends HttpApiGroup.make("repos")
.add(createRepo)
.add(getRepo)
.add(cloneToken)
.add(updateRepo) {}

And the handler:

.handle("cloneToken", ({ params, payload }) => /* … */)
.handle("updateRepo", ({ params, payload }) =>
findRepo(params.name).pipe(
Effect.flatMap(() =>
repos
.getByName(params.name)
.update({
description: payload.description,
topics: payload.topics ? [...payload.topics] : undefined,
})
.pipe(Effect.orDie),
),
Effect.map((m) => new Metadata(m)),
),
),

Test it:

expect(token.scope).toBe("read");
const updated = yield* client.repos.updateRepo({
params: { name: repoName },
payload: { description: "now with stars", topics: ["demo", "alchemy"] },
});
expect(updated.description).toBe("now with stars");
expect(updated.topics).toEqual(["demo", "alchemy"]);

Same pattern — add star to the DO, starRepo to the API, and the handler:

src/Repo.ts
return {
// …
update: (patch) => /* … */,
star: () =>
Effect.gen(function* () {
const current = yield* ensure;
meta = { ...current, stars: current.stars + 1 };
yield* state.storage.put("meta", meta);
return meta;
}),
};
src/Api.ts
export const starRepo = HttpApiEndpoint.post(
"starRepo",
"/repos/:name/star",
{
params: Schema.Struct({ name: Schema.String }),
success: Metadata,
error: RepoNotFound,
},
);
export class ReposGroup extends HttpApiGroup.make("repos")
.add(createRepo)
.add(getRepo)
.add(cloneToken)
.add(updateRepo)
.add(starRepo) {}
src/Worker.ts
.handle("updateRepo", ({ params, payload }) => /* … */)
.handle("starRepo", ({ params }) =>
findRepo(params.name).pipe(
Effect.flatMap(() =>
repos.getByName(params.name).star().pipe(Effect.orDie),
),
Effect.map((m) => new Metadata(m)),
),
),
expect(updated.topics).toEqual(["demo", "alchemy"]);
const starred = yield* client.repos.starRepo({
params: { name: repoName },
});
expect(starred.stars).toBe(1);
Terminal window
bun test

Each call round-trips through Artifacts, the Durable Object, or both — created via artifacts.create, looked up via artifacts.list, mutated via the DO’s init/update/star RPC methods. The whole flow is type-checked end-to-end through the same RepoApi schema, on both the server and the client.

Artifacts and the per-repo DO each do one thing well:

  • Artifacts is the Git server — it owns commits, refs, and the clone/push protocol. Tokens are scoped and short-lived, so you mint them on demand instead of handing out long-lived secrets.
  • The DO is the source of truth for everything that lives around the repo. Each repo gets its own instance, so a hot repo’s writes never contend with another’s.
  • HttpApi ties the two together. The same RepoApi value drives the Worker, the integration test, and any external client — so contract drift between server and consumer is caught at compile time, not in production.

Combine more primitives the same way: a Workflow that runs CI on push, a Container that builds and publishes artifacts, an AI Gateway that summarizes diffs. The Worker stays a thin handler; each primitive owns its own state.