Configuration
forge.toml at your project root is the single source of configuration. init()
reads it, builds the runtime, and migrates the system database. Anything you leave
out keeps a production-safe default, so a real file can be as short as a connection
string.
[postgres]# The database Forge owns. Required.url = "${DATABASE_URL:-postgres://localhost/myapp}"max_connections = 10acquire_timeout_secs = 30statement_timeout_ms = 15000lock_timeout_ms = 5000idle_in_transaction_timeout_ms = 15000
[forge]# Prefix every key, name, and channel so several apps can share one database.namespace = ""
[backends]# "postgres" (durable, shared) or "memory" (in-process, for tests).default = "${FORGE_BACKEND:-postgres}"
[queue]dedup_window_secs = 300 # 5 minutesretention_secs = 604800 # 7 days
[ratelimit]fail_open = true
[blob]backend = "postgres" # "postgres", "fs", or "memory"signing_secret = "${FORGE_BLOB_SIGNING_SECRET:-}"base_url = "/api/files"An unknown key is an error rather than a silent typo, so a misspelled setting fails loudly at startup.
Environment variables
Section titled “Environment variables”Any string value can read the environment. ${VAR} substitutes the variable, and
${VAR:-default} falls back to default when it isn’t set. This is how one file
serves both tests and production: point the backend at memory from the environment
in tests and postgres in production, with the same forge.toml.
[postgres]url = "${DATABASE_URL:-postgres://localhost/myapp}"
[backends]default = "${FORGE_BACKEND:-postgres}" # FORGE_BACKEND=memory in testsA ${VAR} with no value and no default is a hard error, so a missing secret stops
startup instead of quietly resolving to an empty string. Interpolation only runs on
string values, so numbers and booleans (the timeouts, fail_open) are written as
plain literals.
Postgres
Section titled “Postgres”The system database is the one Forge fully owns. It creates its forge_* tables
there and runs its migrations at startup. Give it its own database or schema and
keep your product tables elsewhere.
Forge supports Postgres 17 and newer.
| Key | Default | What it does |
|---|---|---|
url |
required* | Connection string for the database Forge owns. (*Or set embedded.) |
embedded |
false |
Provision an embedded Postgres when no url is set. See below. |
embedded_dir |
.forge/pg |
Where the embedded server keeps its data, relative to the working directory. |
max_connections |
10 |
Pool size. At least 2, since a migration holds one connection while another runs the SQL. |
acquire_timeout_secs |
30 |
Seconds to wait for a free connection before erroring. |
statement_timeout_ms |
15000 |
Server-side statement timeout for runtime queries. 0 disables it. |
lock_timeout_ms |
5000 |
Server-side lock timeout. 0 disables it. |
idle_in_transaction_timeout_ms |
15000 |
Server-side idle-in-transaction timeout. 0 disables it. |
Embedded Postgres
Section titled “Embedded Postgres”For local development without installing Postgres, let Forge run one for you:
[postgres]embedded = trueinit() downloads the Postgres 17 binaries once per machine (to
~/.theseus/postgresql), boots a server on a free port, and connects to it. Data
persists in embedded_dir across restarts; the server stops when the process’s
last Forge handle is dropped.
A non-empty url wins over embedded, so one file covers both modes:
[postgres]url = "${DATABASE_URL:-}" # deploys against $DATABASE_URL when set...embedded = true # ...and runs embedded when it isn'tIf your app keeps its own tables on the same database, build your pool from
forge.postgresUrl() / forge.postgres_url() (Rust also has forge.pool()) —
that is the only way to reach an embedded server’s runtime-minted connection
string.
The Node and Python packages ship with this built in. In Rust it’s behind the
embedded cargo feature:
forgelib = { version = "1", features = ["embedded"] }Backends
Section titled “Backends”Every non-blob primitive runs on one of two backends. postgres is durable and
shared across processes and replicas. memory keeps state in this process only, so
it’s fast and needs no database, but it’s lost on restart and not shared. Memory is
meant for tests; the two pass the same conformance suite.
Set a default for everything, then override individual primitives as needed.
| Key | Default | What it does |
|---|---|---|
default |
"postgres" |
Backend for any primitive without its own entry. |
kv queue auth config ratelimit schedule pubsub |
inherits default |
Override one primitive. "postgres" or "memory". |
blob |
inherits default |
"postgres" or "memory" here. For the filesystem backend, set it under [blob]. |
Namespaces
Section titled “Namespaces”namespace prefixes every key, queue name, topic, and stored row so multiple apps
can share one database without colliding. It’s empty by default, which means no
prefixing. The only rule is that it can’t contain a colon, which Forge reserves as
its separator.
[forge]namespace = "${APP_NAME:-}"Blob storage
Section titled “Blob storage”Blob bytes can live in Postgres, on the local filesystem, or in memory. The metadata always stays in Postgres regardless.
| Key | Default | What it does |
|---|---|---|
backend |
"postgres" |
Where object bytes go: "postgres", "fs", or "memory". |
fs_root |
none | Directory for bytes when backend = "fs". Required for that backend. |
signing_secret |
none | HMAC secret that turns on presigned URLs. CRUD works without it. |
base_url |
"/api/files" |
URL prefix your presigned links point at, where your app serves them. |
The filesystem backend keeps large files out of the database, but a write is no longer atomic with your app’s SQL and multiple replicas need a shared mount.
Queue and rate limit
Section titled “Queue and rate limit”A couple of primitives have their own knobs.
| Key | Default | What it does |
|---|---|---|
queue.dedup_window_secs |
300 |
How long a repeated dedup_id is treated as the same job. |
queue.retention_secs |
604800 |
How long finished jobs are kept before the maintenance sweep removes them. |
ratelimit.fail_open |
true |
Whether a limiter backend outage allows the request (and warns) instead of failing it. Set false for sensitive buckets. |
A separate database per feature
Section titled “A separate database per feature”A primitive can run on its own Postgres database, isolated from the rest, for
bulkheading or because one piece (say the queue) needs different capacity. Add a
[databases.<feature>] table where <feature> is one of the eight primitive names.
Only url is required; the pool settings inherit the top-level [postgres] values.
[databases.queue]url = "${QUEUE_DATABASE_URL}"max_connections = 20That feature gets its own pool, and Forge migrates each distinct database at startup like it does the system one. Your application code doesn’t change.