Skip to content

Primitives

Forge gives you eight primitives on one client. Each one covers the part of a familiar service that most apps actually use, with the same behavior across Rust, Node, and Python. This page is the tour. Every method, return type, and limit is in the Reference.

A few things hold for all of them. Keys and names are byte-limited and oversized input is an error rather than a silent truncation. Every primitive can be prefixed with a namespace so several apps share one database. Failures map to a small error taxonomy that tells you whether a retry might help.

Small values, counters, and cached state. Strings up to 1 MiB with optional TTLs, atomic counters, compare-and-swap, and prefix scans. Lineage: Redis.

// set with a one-hour TTL
await forge.kvSet("session:abc", token, 3600);
const value = await forge.kvGet("session:abc"); // string | null
const n = await forge.kvIncr("views:42", 1); // atomic
// write only if absent: a one-line uniqueness guard
const claimed = await forge.kvSet("slug:abc", body, null, true); // false if taken

Keys go up to 512 bytes, values up to 1 MiB. It’s a string and counter store, not a blob store. Expired keys read as missing. incr on a non-numeric value is an error. For binary values that aren’t valid UTF-8, use the bytes variants (kvSetBytes / kv_set_bytes and their getters).

Durable background jobs. At-least-once delivery, visibility-timeout leasing, retries with a dead-letter queue, delays, and dedup. Lineage: AWS SQS.

const emails = forge.queue<typeof msg>("emails");
// producer
await emails.enqueue(msg, { maxAttempts: 5 });
// consumer: lease a job, do the work, settle it by receipt
const job = await emails.dequeue({ visibilitySeconds: 30, waitSeconds: 20 });
if (job) {
try {
await send(job.payload);
await emails.ack(job.receipt);
} catch {
await emails.nack(job.receipt);
}
}

Payloads go up to 256 KiB. A handler that might outlive its lease should heartbeat to extend it. dedup_id collapses repeated enqueues within a window. Each binding also ships a managed worker (forge.worker("emails", handler) in Node/Python, forge.worker("emails") in Rust) covered in Operations.

Live fan-out to whoever is connected: presence, typing indicators, a counter that updates on a dashboard. It is not durable. Delivery is at-most-once and only reaches current subscribers. Lineage: Postgres LISTEN/NOTIFY and Redis pub/sub. When a message must not be lost, use the queue.

const room = forge.topic<typeof msg>("room:42");
await room.publish(msg);
for await (const event of await room.subscribe()) {
// forward payload to the connected client
}

Payloads are UTF-8 and capped near 7 KB, the Postgres NOTIFY limit. For anything larger, publish a reference such as a row id and let the subscriber read it. A message sent while a subscriber is offline is not kept for it.

Object storage for uploads and generated files: put, get, delete, list, and presigned upload and download URLs. Lineage: S3. Bytes live in Postgres by default or on the local filesystem; the metadata always stays in Postgres.

await forge.blobPut("avatars/42.png", png, "image/png");
const data = await forge.blobGet("avatars/42.png"); // string | null
// for binary, blobGetBytes returns a Buffer
const url = await forge.blobPresignDownload("avatars/42.png", 3600);

Presigned URLs need a signing_secret in forge.toml. Without one, ordinary CRUD still works but the presign calls return a config error. Objects go up to 50 MiB, and a presigned upload URL is capped by a max_bytes you choose. The filesystem backend keeps large files out of the database but needs a shared mount across replicas.

Passwords, sessions, and API keys. argon2id hashing, opaque session tokens with sliding idle and hard absolute deadlines, and fk_ API keys. Lineage: OWASP, the PHC string format, and the Stripe/GitHub key style. Forge stores only hashes and never owns your users table.

const hash = await forge.hashPassword(password); // store this
const ok = await forge.verifyPassword(password, hash);
const token = await forge.createSession(userId);
const userId2 = await forge.validateSession(token); // string | null

Only the hash of a session token or API key is stored, and the plaintext is shown once. validate_session slides the idle deadline and returns nothing for an unknown, expired, or revoked token rather than erroring. After a password check passes, needs_rehash tells you whether to re-hash and upgrade an old hash quietly. The user_id is whatever opaque id your app already uses.

An atomic check-and-consume against a limit, returning a decision carrying the IETF RateLimit header fields. Token bucket by default, or sliding window. Lineage: token bucket and GCRA, with the IETF RateLimit headers.

const d = await forge.rateLimitCheck("login", email, 20, 60);
if (!d.allowed) {
// d.retryAfterSeconds says when the client can try again
}

A denied request is a normal decision with allowed: false, not an error. By default the limiter fails open: if its backend is unavailable it allows the request and logs a warning, so an outage doesn’t lock everyone out. Turn that off (per call or in forge.toml) for payment- or abuse-sensitive buckets. The policy lives in the call you make, the limit and the window, not in server config.

Recurring cron jobs and one-off work at a future time. A schedule enqueues onto a normal queue when it fires, so an ordinary worker drains it. Lineage: cron, Unix at, and Kubernetes CronJob.

// every day at midnight UTC, enqueue onto "reports"
await forge.scheduleCron("nightly", "0 0 * * *", "reports", "{}");
// once, at a specific time
await forge.scheduleAt(Date.now() + 3600_000, "reminders", payload);

For example, to delete an expiring link, schedule a payload onto an expire-link queue, run a worker for that queue, and keep a scheduler loop ticking in the background.

const EXPIRE_QUEUE = "expire-link";
// Producer: register future work. This does not run deleteLink yet.
await forge.scheduleAt(
Date.now() + 3600_000,
EXPIRE_QUEUE,
JSON.stringify({ slug }), // schedule payloads use the raw queue contract
);
// Worker: this is the code that runs when the scheduled job is delivered.
void forge.worker<{ slug: string }>(EXPIRE_QUEUE, async (job) => {
await deleteLink(job.payload.slug);
});
// Scheduler: turns due schedules into queue jobs.
setInterval(async () => {
await forge.runSchedulerOnce();
}, 30_000);

Nothing fires until something ticks the scheduler. Run the managed loop (run_scheduler() in Rust) or call run_scheduler_once() on an interval, as shown in Operations. Ticking is safe on every replica; each due job is claimed once. Cron expressions are 5-field and UTC.

Runtime settings and boolean feature flags. Flags can be always on, always off, a percentage rollout, or an allow-list, evaluated per user. Lineage: 12-factor config and OpenFeature.

await forge.setFlagPercent("new-ui", 25);
const on = await forge.flag("new-ui", false, userId);
await forge.configSet("max_upload_mb", "25");
const raw = await forge.configGet("max_upload_mb"); // string | null

flag never throws. If the flag is missing or the backend is down it returns the default you passed and logs why, so a flag check can’t take down a request. Reads are cached in-process for about 30 seconds, so a change is visible everywhere within that window rather than instantly. An environment variable FORGE_CFG_<KEY> overrides a stored config value, which is useful for an ops override.