Every app needs the same plumbing, and the usual answer is a separate service for each piece:
- Redis for caching and sessions
- a queue for background jobs
- object storage for uploads
- an auth service for logins and sessions
- a cron runner for scheduled work
- a rate limiter
Six things to provision, secure, mock in tests, and learn before you ship a feature. It's worse for an AI agent building the app, where each service is more to wire up and another API to get wrong.
Forge does all of it in one library, with the same API in Rust, Node, and Python.
npm install forgelibConfiguration lives in a forge.toml at your project root. init() reads it and
instantiates the runtime; string values may reference the environment as ${VAR}:
[postgres]
url = "${DATABASE_URL:-}"
embedded = trueSet DATABASE_URL to use a managed or shared Postgres. Leave it unset and Forge
downloads and runs Postgres for you (data persists in .forge/pg) — built into
the Node and Python packages, behind the embedded cargo feature in Rust. Forge
supports Postgres 17+.
Omit settings you do not need; Forge applies production-safe defaults for the rest.
import { ForgeClient } from "forgelib";
const forge = await ForgeClient.init(); // instantiates the runtime from ./forge.toml
// auth: argon2 password hashing and sessions
const hash = await forge.hashPassword(password);
const session = await forge.createSession(userId);
// rate limit: 20 attempts per minute, keyed by email
const limit = await forge.rateLimitCheck("login", email, 20, 60);
if (!limit.allowed) throw new Error("slow down");
// key/value: TTL cache and atomic counters
await forge.kv<typeof profile>(`user:${userId}`).set(profile, { ttlSeconds: 3600 });
const views = await forge.kvIncr(`views:${userId}`, 1);
// queue: background jobs
await forge.queue<{ to: string }>("emails").enqueue({ to: email });
// pub/sub: fan an event out to subscribers
await forge.topic<{ userId: string }>("user.created").publish({ userId });
// blob: store a file, hand back a link that expires in an hour
await forge.blobPut(`exports/${userId}.csv`, csv, "text/csv");
const link = await forge.blobPresignDownload(`exports/${userId}.csv`, 3600);
// schedule: recurring work on a cron
await forge.scheduleCron("nightly-report", "0 0 * * *", "reports", "{}");
// feature flags: roll a feature out to 25% of users
await forge.setFlagPercent("new-ui", 25);
const newUi = await forge.flag("new-ui", false, userId);The same in Rust
cargo add forgelibuse std::time::Duration;
use forgelib::{
Forge, Bytes, SetOpts, EnqueueOpts, PutOpts,
ScheduleOpts, SessionOpts, Limit, FlagRule, EvalCtx,
};
let forge = Forge::init().await?; // instantiates the runtime from ./forge.toml
// auth
let hash = forge.auth().hash_password(&password).await?;
let session = forge.auth().create_session(&user_id, SessionOpts::default()).await?;
// rate limit
let limit = forge.ratelimit()
.check("login", &email, Limit::per_duration(20, Duration::from_secs(60)))
.await?;
// key/value
forge.kv().set(&format!("user:{user_id}"), profile.into(),
SetOpts::new().with_ttl(Duration::from_secs(3600))).await?;
let views = forge.kv().incr(&format!("views:{user_id}"), 1).await?;
// queue
forge.queue().enqueue("emails", payload.into(), EnqueueOpts::new()).await?;
// pub/sub
forge.pubsub().publish("user.created", Bytes::from(event)).await?;
// blob
forge.blob().put(&key, body, PutOpts::new()).await?;
let link = forge.blob().presign_download(&key, Duration::from_secs(3600)).await?;
// schedule
forge.schedule().cron("nightly-report", "0 0 * * *", "reports", Bytes::new(), ScheduleOpts::new()).await?;
// feature flags
forge.config().set_flag("new-ui", FlagRule::Percent(25)).await?;
let new_ui = forge.config().flag("new-ui", false, &EvalCtx::user(user_id)).await;The same in Python
pip install forgelibimport forgelib
forge = await forgelib.ForgeClient.init() # instantiates the runtime from ./forge.toml
# auth
hash = await forge.hash_password(password)
session = await forge.create_session(user_id)
# rate limit
limit = await forge.rate_limit_check("login", email, 20, 60)
if not limit.allowed:
raise RuntimeError("slow down")
# key/value
await forge.kv(f"user:{user_id}").set(profile, ttl_seconds=3600)
views = await forge.kv_incr(f"views:{user_id}", 1)
# queue
await forge.queue("emails").enqueue({"to": email})
# pub/sub
await forge.topic("user.created").publish({"user_id": user_id})
# blob
await forge.blob_put(f"exports/{user_id}.csv", csv, "text/csv")
link = await forge.blob_presign_download(f"exports/{user_id}.csv", 3600)
# schedule
await forge.schedule_cron("nightly-report", "0 0 * * *", "reports", "{}")
# feature flags
await forge.set_flag_percent("new-ui", 25)
new_ui = await forge.flag("new-ui", False, user_id)| Primitive | What it does |
|---|---|
| key/value | get, set, mget, incr, compare-and-swap, prefix scan, TTLs |
| queue | enqueue and dequeue with acks, retries, delays, dedup, depth |
| pub/sub | publish and subscribe on topics |
| blob | put, get, delete, presigned upload and download URLs |
| auth | password hashing, sessions, API keys |
| rate limit | token-bucket or sliding-window checks |
| schedule | cron and one-off jobs |
| config | settings and feature flags with percentage rollout |
By default every primitive runs on one Postgres database, so there's nothing else to operate. When one needs to scale, move that piece onto its own backend without changing your code.
The memory and Postgres backends pass the same conformance suite, so tests run in-process with no database and behave the way production will. Pick the backend in forge.toml, using a ${VAR} reference so the same file flips between tests and production from the environment.
[backends]
default = "${FORGE_BACKEND:-postgres}" # FORGE_BACKEND=memory in tests
[postgres]
url = "${DATABASE_URL:-}"
embedded = trueForge's API isn't in any model's training data, so a coding agent won't know it cold. Install the forge-idiomatic-developer skill so your agent learns the conventions before it writes any Forge code.
npx skills add isala404/forgeIt teaches which primitive fits which task and the idioms for each language, so you get working code instead of guesses.
- The APIs are a shared subset, not a full reimplementation. key/value covers the Redis commands most apps use, not Lua scripts or sorted sets. blob does CRUD and presigned URLs, not S3 lifecycle rules.
- auth is the essentials: password hashing, sessions, and API keys, enough to have working auth in minutes while you prototype. For OAuth, 2FA, or email verification, bring Clerk, Firebase, or a dedicated provider.
- Forge ships two backends, memory and Postgres, plus local disk for blob. If you need Redis or S3, implement the primitive's trait and inject it, or use that service directly.
- Not an ORM or a web framework. Your tables and routes stay yours.
Three full apps in examples/, each built on the Rust, Node, and Python backends with a React frontend.
- todo: accounts, lists, and a background audit queue
- links: a URL shortener with click counts, QR images, and analytics
- chat: real-time messaging with presence
MIT. Do whatever you want.
Postgres is enough.
Quick Start ·
Examples ·
Discussions