Skip to content
Examples

Examples

Run your own Apps on Cloudflare Workers or celld: installable App Releases, each with its own Durable Object, SQLite database and declared bindings

examples/standalone ships in the npm package, ready to copy as in the quick start. The generated API reference and the in-memory example below ship with them. The rest live in the Oyster repository:

  • examples/standalone is the whole host from the quick start.
  • examples/notes-app is a full App: UI, backend methods and SQLite. Copy it to start an App with a backend. test/standalone.native.mjs packs it, installs it on the standalone Oyster and drives its UI in a browser, on workerd and on celld.
  • examples/notes.js is a small backend with its own SQLite and no external grants. It reads or writes one note.
  • src/testing/worker.ts with wrangler.test.jsonc hosts Instances in a Durable Object with a Worker Loader. The same pre-bundled Worker runs under wrangler dev (workerd) and celld dev.
  • test/core.mjs exercises replay, state transitions and owner fences in plain Node.
  • https://oyster.shiit.app/ runs that same host with Fungi’s seven starter Apps.

This self-contained example drives one Instance through in-memory ports. A disabled Instance never resolves or connects a backend, so no Release is needed. The request runs in a frame from createInvocationGate, the same gate the standalone Instances and the Hub open for every call; the example is docs/home.ts, which the package docs check typechecks.

TS
import * as Effect from "effect/Effect";
import * as Schema from "effect/Schema";
import {
AppInstanceError,
createAppInstance,
instanceIds,
ownerStateSchema,
type Instance,
} from "@fungi.computer/oyster";
import {
APP_INVOCATION_CEILING_MS,
createInvocationGate,
invocationFactsSchema,
} from "@fungi.computer/oyster/invocation";
const instanceId = instanceIds.instance.generate();
let stored: Instance | null = null;
const instance = createAppInstance({
instanceId,
read: () => Effect.succeed(stored),
write: (record) =>
Effect.sync(() => {
stored = record;
}),
resolve: () => Effect.fail(new Error("No Releases in this example")),
abort: () => Effect.void,
purge: () => Effect.void,
connect: () => Effect.fail(new Error("No backend in this example")),
});
const selection = {
instanceId,
owner: { kind: "installation", id: "installation_example" },
appId: "app_example",
releaseId: "rel_example",
authorityVersion: 2,
} as const;
const disabled = Schema.decodeUnknownSync(ownerStateSchema)({
...selection,
status: "disabled",
});
const receipt = await Effect.runPromise(instance.apply(disabled));
console.log(receipt.status); // "disabled"
// A request runs inside an invocation frame: the caller facts the host admitted,
// a deadline, and an abort signal the gate owns.
const gate = createInvocationGate<null>({
abortGuest: () => Promise.resolve(),
close: () => Promise.resolve(),
});
const facts = Schema.decodeUnknownSync(invocationFactsSchema)({
instanceId,
selection,
artifactDigest: `sha256:${"0".repeat(64)}`,
subject: {
kind: "call",
context: {
actor: { kind: "member", memberId: "member_example" },
computerId: null,
},
},
grants: [],
ancestry: [],
deadlineAt: Date.now() + APP_INVOCATION_CEILING_MS,
});
const refused = await gate.run(facts, null, (frame) =>
Effect.runPromise(
Effect.result(instance.fetch(frame, new Request("https://app.test/"))),
),
);
if (refused._tag === "Failure" && refused.failure instanceof AppInstanceError)
console.log(refused.failure.code); // "instance_inactive"