Skip to content

Configuration

forge.toml uses the same runtime-profile keys and naming in Rust, JavaScript, Python, and Go. init() reads ./forge.toml; initFrom/init_from/InitFrom reads another path; initFromString/init_from_string/init_from_str/InitFromString parses TOML already held in memory. Unknown keys and missing environment variables fail before Forge makes network calls.

PostgreSQL is the default and normal production profile. It owns Forge’s durable metadata, coordination, leases, schedules, authentication records, configuration, shared rate limits, and—unless another blob byte store is selected—blob bytes.

forge.toml
[forge]
mode = "postgres"
environment = "production"
namespace = "myapp"
[postgres]
url = "${DATABASE_URL}"
max_connections = 10
acquire_timeout_secs = 30
auto_migrate = false
migration_lock_timeout_secs = 30
statement_timeout_ms = 15000
lock_timeout_ms = 5000
idle_in_transaction_timeout_ms = 15000
[queue]
dedup_window_secs = 300
payload_retention_secs = 86400
terminal_retention_secs = 604800
succeeded_retention_secs = 604800
dead_retention_secs = 2592000
cancelled_retention_secs = 604800
[ratelimit]
fail_open = true
[blob]
backend = "postgres"
signing_secret = "${FORGE_BLOB_SIGNING_SECRET}"
base_url = "/api/files"

Queue payload bytes and terminal status have separate retention clocks. Payload retention defaults to one day. Succeeded, dead, and cancelled status each default to seven days and can be tuned independently; terminal_retention_secs sets all three before an individual override.

Normal initialization requires a reachable PostgreSQL service, usable credentials, and a compatible Forge schema. Development and test default auto_migrate to true; production defaults it to false. With automatic migration disabled, initialization performs a read-only schema validation and tells the operator to run the migration API when the schema is pending, locked, incompatible, or drifted. Applications use whichever primitives they need; there is no feature-enable matrix and no configuration-driven plugin loader.

Forge requires PostgreSQL 18. The system pool defaults to ten connections and requires at least two while startup migrations are enabled; validation-only initialization can use one. acquire_timeout_secs and migration_lock_timeout_secs are seconds. The three server-side timeouts are milliseconds and accept 0 to use PostgreSQL’s unlimited default.

Local Rust, Node, and Python development can run a real embedded PostgreSQL server:

[forge]
mode = "postgres"
environment = "development"
[postgres]
url = "${DATABASE_URL:-}"
embedded = true
embedded_dir = ".forge/pg"

A non-empty URL wins; otherwise Forge starts the embedded server. The Rust package requires the embedded feature. Go intentionally uses the system PostgreSQL profile or explicit memory profile and does not download an embedded server. Embedded state is durable on that machine but is not a multi-replica production deployment.

Memory is one complete runtime variant, not eight independent backend switches:

[forge]
mode = "memory"
environment = "test"
namespace = "unit-test"

Memory initialization creates no pool, starts no embedded service, runs no migration, and requires no PostgreSQL URL. Every primitive keeps the same public API, so tests do not branch around Forge. A declared production environment rejects memory unless allow_memory_in_production = true is written explicitly.

Property PostgreSQL Memory
Restart persistence Durable Lost on process exit
Sharing Processes and replicas One process
Pub/sub Cross-process LISTEN/NOTIFY, connected subscribers only In-process subscribers only
Rate-limit scope Shared Process-local
PostgreSQL accessor Pool/credential-bearing URL NOT_CONFIGURED

backendCapabilities() / backend_capabilities() / BackendCapabilities() provides one provider line per primitive without performing a health check. Use probe() / Probe() for bounded live readiness checks.

blob.backend = "postgres" keeps bytes and metadata in PostgreSQL, which preserves the default “Postgres is enough” deployment. blob.backend = "fs" stores bytes below blob.fs_root while PostgreSQL retains Forge’s durable metadata; use it only with a shared mount when multiple replicas must see the same objects. The explicit memory profile selects memory blob storage automatically. S3-compatible storage is the intentional production backend for large bytes, but it does not replace Forge’s system PostgreSQL responsibilities.

forge.toml · S3-compatible blob bytes
[blob]
backend = "s3"
bucket = "my-app-production"
region = "us-east-1"
prefix = "forge"
path_style = false
connect_timeout_secs = 3
request_timeout_secs = 30
max_retries = 3
signing_secret = "${FORGE_BLOB_SIGNING_SECRET}"
# Omit endpoint for AWS S3. Set it, and usually path_style = true, for MinIO/R2/etc.
# endpoint = "https://object-storage.example.com"
# Omit static credentials to use the standard AWS credential chain.
# access_key = "${S3_ACCESS_KEY}"
# secret_key = "${S3_SECRET_KEY}"
# session_token = "${S3_SESSION_TOKEN}"

Initialization performs HeadBucket followed by create-and-abort multipart operations. This verifies bucket access and required permissions without leaving a permanent object. Access keys, secret keys, session tokens, and presigned URL query strings are credentials and must remain out of logs and traces.

A hot primitive can use a separate PostgreSQL target without changing its schema version or public behavior:

[databases.queue]
url = "${QUEUE_DATABASE_URL}"
max_connections = 20

Every distinct target receives the same coordinated Forge migration history. This is a measured pool/server bulkhead, not an independently versioned primitive database.

forge.namespace scopes all keys, queues, rows, topics, schedules, caches, notifications, signing operations, maintenance work, and authentication records. It is case-sensitive, immutable after init, not normalized, and may contain at most 128 bytes using ASCII letters, digits, ., _, and -. Empty means the unnamespaced profile and must not share a backend with an application that expects isolation.

Namespacing prevents accidental collisions; it does not authenticate a caller or authorize access. Applications make authorization decisions before calling Forge. The scopeKvKey / scope_kv_key / ScopeKVKey family composes bounded application, tenant, user, and resource names separately for KV keys, blob keys, rate-limit subjects, and topics without installing a hidden universal tenant context.

The v1 helpers require all four components in one call. They validate 1–255 UTF-8 bytes, reject control characters, and check the selected primitive’s complete backend-safe limit before any Forge backend call. Length-prefixed UTF-8 components make delimiters unambiguous, and parseScopedName / parse_scoped_name / ParseScopedName recovers the four components.

let key = forgelib::scope_kv_key("billing", "tenant-7", "user-42", "invoice:9")?;
let parsed = forgelib::parse_scoped_name(&key)?;
import { scopeKvKey, parseScopedName } from "forgelib";
const key = scopeKvKey("billing", "tenant-7", "user-42", "invoice:9");
const parsed = parseScopedName(key);
key = forgelib.scope_kv_key("billing", "tenant-7", "user-42", "invoice:9")
parsed = forgelib.parse_scoped_name(key)
key, err := forge.ScopeKVKey("billing", "tenant-7", "user-42", "invoice:9")
parsed, err := forge.ParseScopedName(key)

These helpers produce names, not permissions. Authorize tenant-7, user-42, and the invoice in application code before calling Forge; parsing a key proves only that it is well formed. Queue names, authentication identities, config keys, schedules, and other primitives remain independently named because one hidden tenant prefix would create incorrect authorization assumptions.

String values accept ${VAR} and ${VAR:-default}; a missing variable without a default is an error, and interpolation cannot inject TOML structure. Environment interpolation and FORGE_CFG_* config overrides are captured deterministically when the process initializes. Changing the host environment afterward does not mutate a running Forge instance; write dynamic values through the config API instead.

Never log postgresUrl()/postgres_url()/PostgresURL(): it returns a credential-bearing DSN. In memory mode the same accessor returns NOT_CONFIGURED.

Applications own process signals. On shutdown, stop accepting requests and call close with a deadline. Close is idempotent, stops new work, wakes subscriptions, closes dedicated listeners and pools, and stops an embedded server. Native Rust and Go workers stop dequeuing and drain handlers within their configured grace; after grace they cancel or abort supervisors and release or expire leases. JavaScript and Python userland workers cancel active handlers and release leases immediately; JavaScript handlers can inspect job.signal, while Python handlers receive task cancellation and can inspect job.cancelled. Forge never installs SIGINT or SIGTERM handlers.

Rust’s trait-injection builder remains an advanced compile-time extension for custom backends. It is not exposed as portable configuration, dynamic discovery, or a plugin loader; new first-party backends require evidence and a complete conformance story.