Skip to content

Recipes

These are the patterns that show up in the example apps (a todo app, a URL shortener, and a chat app, each built three times on the Rust, Node, and Python bindings). The code is trimmed to the Forge calls so the shape is easy to lift into your own handlers.

Email and password auth is about thirty lines of Forge calls per handler: hash on the way in, verify on the way out, and issue sessions you can revoke. There’s no separate users table to wire up first: kv stores the user record and auth owns the hashing and the sessions.

Sign-up does four things: throttle by email, hash the password, claim the email atomically, and open a session. Writing the email key with “set if not present” makes it its own uniqueness check, so there’s no read-then-write race.

// throttle by email, fail open so a limiter outage doesn't lock people out
const limit = await forge.rateLimitCheck("auth", email, 20, 60, true);
if (!limit.allowed) return c.json({ error: "slow down" }, 429);
const user = { id: randomUUID(), email, passwordHash: await forge.hashPassword(password) };
const body = JSON.stringify(user);
// the email key is its own uniqueness guard: false means already taken
const created = await forge.kvSet(`user:email:${email}`, body, null, true);
if (!created) return c.json({ error: "email already registered" }, 409);
await forge.kvSet(`user:id:${user.id}`, body);
const token = await forge.createSession(user.id, 86_400, 30 * 86_400);

Logging in runs the same throttle, verifies the password against the stored hash, then opens a fresh session. Keep the rate-limit check ahead of verifyPassword so a stuffing run can’t burn CPU on hashing.

const limit = await forge.rateLimitCheck("auth", email, 20, 60, true);
if (!limit.allowed) return c.json({ error: "slow down" }, 429);
const stored = await forge.kvGet(`user:email:${email}`);
if (!stored) return c.json({ error: "invalid email or password" }, 401);
const user = JSON.parse(stored);
// the throttle runs before verifyPassword, so credential stuffing can't burn CPU on hashing
if (!(await forge.verifyPassword(password, user.passwordHash)))
return c.json({ error: "invalid email or password" }, 401);
const token = await forge.createSession(user.id, 86_400, 30 * 86_400);

Every protected route resolves the caller’s token to a user (or 401s), and logout revokes the session so the token stops working immediately.

// middleware: resolve the bearer token (or session cookie) to a user id, else 401
const userId = await forge.validateSession(bearerToken(c.req.header("authorization")));
if (!userId) return c.json({ error: "authentication required" }, 401);
// logout: revoke the session so the token stops resolving
await forge.revokeSession(bearerToken(c.req.header("authorization")));

Keep the request fast by doing only the cheap part inline and handing the rest to a queue. The URL shortener bumps a click counter on the redirect and queues everything else, so the user gets their redirect immediately.

// in the redirect handler: cheap now, the rest later
await forge.kvIncr(`clicks:${slug}`, 1);
await forge.queue<{ slug: string }>("clicks").enqueue({ slug }, { maxAttempts: 3 });

A worker leases jobs, does the work, and acks on success or nacks on failure so the job is retried. Run one of these per queue (see Operations for a managed version).

await forge.worker<{ slug: string }>("clicks", async (job) => {
await handle(forge, job.payload);
});

The worker that drains the clicks queue publishes the new total on a topic. An SSE route subscribes to that topic and forwards each message to the connected client, then drops the subscription when the client disconnects.

// in the worker handler, after reading the new count
await forge.topic<{ slug: string; clicks: number }>(`clicks:${slug}`)
.publish({ slug, clicks: total });
// in the SSE route (Hono)
return streamSSE(c, async (stream) => {
const events = await forge.topic<{ slug: string; clicks: number }>(`clicks:${slug}`)
.subscribe();
stream.onAbort(() => { void events.return?.(); });
for await (const event of events) {
await stream.writeSSE({ data: JSON.stringify(event) });
}
});

Put generated or uploaded bytes under a key with a content type, read them back to serve them, or hand the client a presigned link so it can fetch the file directly.

await forge.blobPut(`qr/${slug}.svg`, svg, "image/svg+xml");
const stored = await forge.blobGet(`qr/${slug}.svg`); // string | null
const url = await forge.blobPresignDownload(`qr/${slug}.svg`, 3600);

Presigned links need a signing_secret set in forge.toml. For uploads, hand back a presigned upload URL capped at a max_bytes you choose and let the client PUT straight to it.

Schedule a one-shot for delayed work, like expiring a link at its TTL, or a cron for recurring jobs. Both land on a normal queue, so the same workers drain them.

// expire this link in an hour
await forge.scheduleAt(Date.now() + 3600_000, "expire", JSON.stringify({ slug }));
// a daily report
await forge.scheduleCron("nightly-report", "0 0 * * *", "reports", "{}");

Schedules only fire when the scheduler is ticked. The same loop that ticks it is a good place to run Forge’s maintenance sweep. See Operations for the loop these apps run on every replica.