Help us shape the future of Go! Take the 2026 Go Developer Survey

forge

package module
v0.0.0-...-d267593 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 28, 2026 License: MIT Imports: 31 Imported by: 0

README

Forge

Forge is a Go framework for building HTTP services on PostgreSQL, MySQL, MariaDB, or SQLite with secure defaults, a conventional application layout, and a production container from the first commit. The core packages use only the Go standard library; each database adapter adds its driver.

New to Forge? Getting started builds a small blog from forge new to deploy: scaffolds, sign-in, live updates, translations, email, jobs, OpenAPI and tests.

Status

Pre-release. Phases 1 (core runtime, configuration, CLI, lifecycle, HTTP) and 2 (databases, migrations, transactions, repositories) are implemented, including support for several databases. Phase 3 provides an identity and tenancy foundation with PostgreSQL, MySQL/MariaDB and SQLite persistence, password and browser-session flows, recovery and MFA primitives, security auditing, generated sign-in pages and a setup command for the first owner. Phase 4 adds background jobs, a transactional outbox with bounded concurrent dispatch, dead-letter replay, and an in-process scheduler. Its remaining scope and adapter boundaries are listed in the roadmap. APIs may change without notice until a tagged release.

Phase 7 now includes a provider-neutral agent action engine and a PostgreSQL-backed execution journal. It does not call an LLM: applications register actions and supply policy, approval, and identity integrations. The engine takes identity only from authenticated middleware, checks tenant consistency, and durably marks uncertain post-execution outcomes for reconciliation. Policy and approval callbacks remain application trust points. Destructive/external actions require an approval adapter; the CLI itself is not an agent authorization boundary. See the AI agent guide.

The PostgreSQL transactional outbox supports bounded concurrent delivery, leases, acknowledgements, bounded retries, dead-letter replay, and queue metrics. An in-process job scheduler (jobs.Scheduler) runs recurring work with configurable concurrency. Phase 5 adds dependency-free observability: traces with W3C propagation, bounded-cardinality metrics for Prometheus and OTLP, trace-correlated logs, and database and job instrumentation (docs/OBSERVABILITY.md). Phase 6 starts with an OpenAPI 3.1 document generated from the routes' Go types (docs/OPENAPI.md). Phase 10 starts with continuous fuzzing, SBOMs and signed release images (docs/SUPPLY_CHAIN.md). Performance work and further security hardening remain planned. See docs/ROADMAP.md for the full status.

Packages

Core packages import only the standard library and each other; a test (architecture_test.go) enforces this (ADR 0007, ADR 0009), and the dependency directions of ADR 0052: auth speaks no HTTP, and only applications import the root forge package.

Package Purpose
forge forge.Main entrypoint, App composition, shutdown hooks, binary subcommands, health routes
command Command, Env and ErrUsage, the contract of binary subcommands (forge.Command is the same type)
auth Opaque session tokens, principals and deny-by-default permissions, password hashing, sign-in, MFA, password recovery, throttling and the security audit, with the identity-store ports; no HTTP
auth/authhttp The HTTP side of auth: bearer and cookie session middleware (authhttp.NewMiddleware), Require/RequireAudited/RequireCSRF, the JSON sign-in, MFA and recovery handlers with their OpenAPI, browser sign-in, sign-out, MFA and password-reset flows (authhttp.Browser) and sign-in redirects for HTML pages
agent Provider-neutral allowlisted action pipeline with policy, validation, principal permissions, explicit approval and mandatory journaling
tenancy Fail-closed tenant context; used by authentication and tenant-aware database transactions
config Environment configuration with fail-fast validation; database selection by URL scheme; config.Secret for redacted values
credentials Encrypted per-environment credentials (config/credentials/<env>.json.enc, AES-256-GCM, key from FORGE_MASTER_KEY or a git-ignored key file), typed Store, fail-fast required credentials; forge credentials edit
encryption Attribute encryption with key rotation: randomized AES-256-GCM bound to table, column and record id, key rings, opt-in HMAC blind index, Value[T] for database/sql, resumable batch re-encryption
router Route registry on net/http.ServeMux (Go 1.22 patterns), frozen after the first request; JSON 400/404/405; conventional Resources routes
openapi OpenAPI 3.1 from described routes: Describe, Typed handlers, schemas reflected from Go types (UUIDv7, date-time, nullable, $ref components), deterministic JSON, contract validation of responses
httpserver HTTP server with timeouts, body limit, security headers, request IDs, panic recovery, graceful shutdown, and composable application middleware
web JSON responses, the single error format, strict JSON and form decoding, safe redirects, content negotiation, flash messages, method override, request ID and request-scoped logger
csrf CSRF protection for browser requests: double-submit token in forms and headers, cross-origin rejection
views Concurrent-safe server-rendered HTML with layouts, partials, CSRF, flash and translation helpers, contextual escaping and buffered rendering
i18n Rails I18n: JSON translations in config/locales, %{name} interpolation, CLDR plural rules, per-request locale (path, parameter, cookie, Accept-Language), localized numbers and dates, forge i18n check
pagination Keyset pagination over UUIDv7 with opaque cursors
events Versioned event envelope with bounded JSON payloads
jobs In-process scheduler with bounded concurrency, fixed-rate, daily, and numeric 5-field cron schedules
ratelimit Rails-style rate_limit: fixed-window limiter, per-route 429/Retry-After middleware, bounded in-memory store; shared stores in ratelimit/postgres, /mysql, /sqlite
cache Solid Cache equivalent: Cache with Fetch stampede protection, bounded LRU+TTL memory cache, typed JSON helpers; shared stores in cache/postgres, /mysql, /sqlite
storage Active Storage equivalent for local disk: Storage port, HMAC-signed expiring links, safe serving handler, strict multipart upload; storage/disk adapter
telemetry Tracing with W3C trace context, metrics with a cardinality cap, Prometheus text and OTLP/HTTP JSON export, trace-correlated logs
health Readiness check registry; /health, /health/live, /health/ready
uuid RFC 9562 UUIDv7 (default identifier), monotonic generator, JSON/text/SQL support
fault Typed application errors (code, message, category, cause, metadata, retryable, HTTP status)
sqldb Database-independent error translation, transaction retries, bounded pool shutdown, database/sql wrapper with a sqlc-compatible DBTX
migrate Migration engine (SQL files, checksums, locks, migrate/rollback commands) with a per-database Dialect
dbtest Test-server discovery shared by the adapters' test packages

Adapter packages (third-party dependencies allowed):

Package Purpose
postgres PostgreSQL on pgx v5: pool, DBTX, transactions with retry, error translation, advisory locks, migrations
outbox/postgres PostgreSQL transactional outbox persistence and concurrent dispatcher
agent/postgres Tenant-RLS-protected PostgreSQL journal with replay refusal, bounded reconciliation listing, and one-time operator decisions
mysql MySQL and MariaDB on go-sql-driver/mysql: hardened session, error translation, migrations
sqlite SQLite on the pure-Go modernc.org/sqlite: enforced pragmas, error translation, migrations with a file lock
ratelimit/{postgres,mysql,sqlite}, cache/{postgres,mysql,sqlite} Shared rate limit buckets and application cache in the database, with embedded migrations
postgres/postgrestest, mysql/mysqltest, sqlite/sqlitetest A fresh database per test

An application links only the adapter it imports. The forge CLI lives in cmd/forge; the deploy proxy, forge-proxy, in cmd/forge-proxy (its go.yaml.in/yaml/v3 and golang.org/x/crypto dependencies are not linked into applications, ADR 0030). The core packages import no third-party module: Argon2id password hashing (golang.org/x/crypto/argon2) comes with the identity stores auth/postgres, auth/mysql and auth/sqlite, which register it, and an application with another store calls auth.RegisterArgon2id(argon2.IDKey); with neither, new passwords are PBKDF2-HMAC-SHA256 (ADR 0053, password hashing).

Install

Forge-Go ships as a Go module and, from a v* tag, as signed binaries. There is no installer script to pipe into a shell: you either build a pinned version with Go or download an archive and verify it first.

With Go 1.27.3+; the Go checksum database verifies the module:

go install github.com/rocketinsidecorp/forge-go/cmd/forge@latest
forge version                   # forge v0.1.0 (unknown, unknown, go1.27.3 linux/amd64)

To pin a release, go install github.com/rocketinsidecorp/forge-go/cmd/forge@vX.Y.Z with a version from the releases. A build from the module proxy reports unknown for the commit and the date; they are stamped into the release archives below, which is why forge version there shows both.

Without Go, from a release archive: forge and forge-proxy for linux, darwin and windows on amd64 and arm64 (.tar.gz, .zip on Windows), named forge_<version>_<os>_<arch>. Each release carries checksums.txt (SHA-256 of every archive and SBOM), checksums.txt.sigstore.json (its keyless cosign signature) and a GitHub build provenance attestation that covers every archive and SBOM. Download, verify (cosign v3+, gh 2.68+), then install:

VERSION=vX.Y.Z OS=linux ARCH=amd64        # a release tag; OS: linux, darwin, windows; ARCH: amd64, arm64
BASE=https://github.com/rocketinsidecorp/forge-go/releases/download/$VERSION
curl -sSLO "$BASE/forge_${VERSION}_${OS}_${ARCH}.tar.gz"
curl -sSLO "$BASE/checksums.txt"
curl -sSLO "$BASE/checksums.txt.sigstore.json"

# 1. checksums.txt was signed by this repository's release workflow, for a v* tag
cosign verify-blob --bundle checksums.txt.sigstore.json \
  --certificate-identity-regexp '^https://github\.com/rocketinsidecorp/forge-go/\.github/workflows/release\.yml@refs/tags/v' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  checksums.txt
# 2. the archive is the one checksums.txt lists (macOS: grep " forge_${VERSION}_${OS}_${ARCH}" checksums.txt | shasum -a 256 -c)
sha256sum -c --ignore-missing checksums.txt
# 3. GitHub attests this repository's release.yml built it, from the tag $VERSION
gh attestation verify "forge_${VERSION}_${OS}_${ARCH}.tar.gz" --repo rocketinsidecorp/forge-go \
  --signer-workflow rocketinsidecorp/forge-go/.github/workflows/release.yml --source-ref "refs/tags/$VERSION"

tar -xzf "forge_${VERSION}_${OS}_${ARCH}.tar.gz"        # Windows: Expand-Archive forge_..._windows_amd64.zip
sudo install -m 0755 "forge_${VERSION}_${OS}_${ARCH}/forge" /usr/local/bin/forge
forge version

forge-proxy_<version>_<os>_<arch> verifies the same way. Verifying a single-release identity, an offline attestation bundle, the SBOMs and the signed container images: docs/SUPPLY_CHAIN.md. Maintainers cut a release with docs/RELEASING.md.

Quickstart

Requires Go 1.27.3+ and Docker with BuildKit and Compose, and the forge command (Install).

go install github.com/rocketinsidecorp/forge-go/cmd/forge@latest
forge new myapp                 # SQLite; or -d postgresql, -d mysql, -d mariadb, -d none
cd myapp
docker compose up --build       # or, without Docker: forge migrate && forge dev
curl -i http://127.0.0.1:8080/v1/status
curl -i http://127.0.0.1:8080/health/ready   # includes the "database" check

Authentication and organizations are opt-in (owner decisions of 2026-10-26):

forge new site                  # SQLite, no sign-in: a welcome page and plain tables
forge new shop --auth           # SQLite with sign-in, two-factor and password-reset pages
forge new saas --tenants        # PostgreSQL (row-level security), sign-in, organizations
forge new saas --tenants -d mysql   # organizations on MySQL: isolation by mandatory org_id predicates
forge new shop -d mariadb --auth    # any -d works with --auth

Like Rails' --skip-docker, and with the same names as Forge-Rust, the container and CI files are optional:

forge new shop --skip-docker           # no Dockerfile, .dockerignore, compose.yaml, compose.observability.yaml, docker/ or observability/ (and no image job in CI)
forge new shop --skip-ci               # no .github/ (no CI, release workflow or Dependabot)
forge new shop -d postgresql --skip-docker --skip-ci   # neither; start the PostgreSQL server yourself

--tenants implies --auth and selects PostgreSQL unless -d is given. An application grows later without edits: forge generate authentication adds sign-in to one created without --auth, and forge generate tenancy turns an --auth application into one with organizations (below).

An API-only application, like rails new --api: JSON handlers with no views, assets, JavaScript, translations, sign-in pages or browser middleware (CSRF, method override, flash). Every -d works with it.

forge new myapi --api           # or: forge new myapi --api -d postgresql
cd myapi
docker compose up --build       # or, without Docker: forge migrate && forge dev
curl -s http://127.0.0.1:8080/            # {"application":"myapi","version":"0.1.0"}
curl -s http://127.0.0.1:8080/v1/status   # {"service":"myapi","status":"ok"}
curl -s http://127.0.0.1:8080/health      # JSON liveness and readiness
curl -s http://127.0.0.1:8080/openapi.json
forge generate resource post title:string                  # a JSON API under /v1/posts (closed until you write its guard, or --public)
forge generate controller status_checks index --api         # read-only JSON endpoints
forge generate scaffold post title:string                  # refused: an --api application has no pages
--database (-d) Compose service Adapter
sqlite (default; also sqlite3) none; the database file is on a storage volume sqlite
postgresql (default with --tenants; also postgres, pg) postgres:18-alpine postgres
mysql mysql:8.4 mysql
mariadb mariadb:11.8 mysql
none (same as --skip-database) none none

docker compose up starts the database server (if any), runs the application's migrate command once, then starts the application. A database server is published on 127.0.0.1:<port>, a port between 20000 and 29999 derived from the application name so it does not clash with a local server on its default port; the generated README and .env.development contain the exact value.

Running the application outside Docker (server databases):

docker compose up -d postgres   # or mysql, mariadb
forge db prepare                # create the database if missing, then migrate (and seed a new one)
forge dev                       # rebuilds, restarts and reloads the browser when files change

With SQLite, forge db prepare and forge dev use storage/development.db directly.

End to end: a CRUD with sign-in, no code edited

The generated README carries these sequences too. SQLite with sign-in, no Docker:

forge new shop --auth
cd shop
forge migrate
forge setup --email you@example.com
forge generate scaffold product name:string price:integer
forge migrate
forge dev

forge setup asks for the owner's password twice (or reads FORGE_SETUP_PASSWORD). Open http://localhost:8080/session/new, sign in with your email and password, then list, create, edit and delete products at http://localhost:8080/products. The scaffold mounted itself in app/bootstrap/routes_gen.go behind sign-in and the products:read and products:write permissions its migration grants to the owner role. Field types take the names Rails uses too: integer, boolean and timestamp (or datetime) are int, bool and time.

With organizations (PostgreSQL, row-level security), every user signs in to an organization, and every scaffolded row belongs to one:

forge new saas --tenants
cd saas
docker compose up -d --wait postgres
forge migrate
forge setup --org acme --email you@example.com
forge generate scaffold product name:string price:integer --org-scoped
forge migrate
forge dev

Sign in with organization acme. Tables are --org-scoped (an org_id column every query and policy filters by) or --global (shared by every organization). The JSON API at /v1/products authenticates the same session cookie (and, in an application created with --api-auth, bearer tokens); changes send the CSRF token in the X-CSRF-Token header (drop --data-urlencode tenant=acme in an application without organizations):

jar=$(mktemp)
token=$(curl -s -c "$jar" -H 'Accept: text/html' http://localhost:8080/session/new | sed -n 's/.*name="authenticity_token" value="\([^"]*\)".*/\1/p' | head -n 1)
read -rsp 'Password: ' password; echo
curl -s -o /dev/null -b "$jar" -c "$jar" -H 'Accept: text/html' --data-urlencode "authenticity_token=$token" --data-urlencode tenant=acme --data-urlencode email=you@example.com --data-urlencode "password=$password" http://localhost:8080/session
curl -s -b "$jar" http://localhost:8080/v1/products
curl -s -b "$jar" -H "X-CSRF-Token: $(awk '$6 == "__Host-forge_csrf" {print $7}' "$jar")" -H 'Content-Type: application/json' -d '{"name":"Lamp","price":1999}' http://localhost:8080/v1/products

MySQL and MariaDB work the same way (forge new shop -d mysql --auth, then docker compose up -d --wait mysql or mariadb after cd).

An application without --auth has no sign-in, so generated routes are closed (403) unless generated with --public, which serves them to anyone who can reach the server (forms still need their CSRF token):

forge new shop
cd shop
forge migrate
forge generate scaffold product name:string price:integer --public
forge migrate
forge dev

Then open http://localhost:8080/products, or curl -s http://localhost:8080/v1/products. To put your own authentication in front, copy the guard function (public, or protect for routes generated without --public) from app/bootstrap/routes_gen.go into another file of app/bootstrap, change it there, and run any forge generate command again (repeating the last one is enough): until then the build fails with public redeclared, never silently; afterwards the generators leave their own out and use yours. Or add Forge's: forge generate authentication, then forge migrate and forge setup --email you@example.com.

Growing an --auth application into one with organizations converts every generated table: a migration adds org_id to each (filled with the one organization forge setup created, then indexed and NOT NULL; on PostgreSQL with row-level security forced), the generated code is rewritten as --org-scoped would write it, and sign-in starts asking for the organization. It refuses files you edited and runs backwards with forge rollback, which refuses (changing nothing) while a second organization exists:

forge generate tenancy --org acme --dry-run   # the migration and every file it rewrites
forge generate tenancy --org acme             # the default organization becomes acme
forge migrate

CI runs the SQLite, MySQL and PostgreSQL sequences above as written, with a real forge binary, and drives them over HTTP: sign-in, the CRUD pages, the JSON API and, with organizations, a second organization that gets 404 for every row of the first (TestGeneratedApplication/readme_quickstart_*).

Database roles (PostgreSQL)

A PostgreSQL application serves as a runtime role that row-level security applies to and that cannot change the schema, and migrates as a separate role that owns it. docker/postgres/init.sql creates both in the Compose database (app_runtime and app_migrator, development-only passwords), and .env.development points FORGE_DATABASE_URL and FORGE_MIGRATION_DATABASE_URL at them:

docker compose up -d postgres     # first start: init.sql creates app_migrator, app_runtime and the database
forge migrate                     # runs as app_migrator (FORGE_MIGRATION_DATABASE_URL)
forge dev                         # serves as app_runtime (FORGE_DATABASE_URL)
forge test                        # repository and sign-in tests run as a fresh runtime role
docker compose down --volumes     # once, for a volume created before the roles existed

Only forge migrate and forge rollback read PGOPTIONS, as for psql; every command that opens the application's pool (forge dev, seed, jobs, setup) refuses to start while it is set, naming the variable and never its value, because a forge.tenant_id in it would give every session a default tenant:

PGOPTIONS='-c lock_timeout=5s' forge migrate   # works: bounds lock waits for this run
PGOPTIONS='-c lock_timeout=5s' forge dev       # refused: PGOPTIONS is set in the environment, ...

In production, provision the same two roles once (SQL in Database roles), migrate with the migration role and serve with the runtime role only. In staging and production the application refuses to start when the role it logs in as, or acts as, is a superuser or has BYPASSRLS or CREATEROLE, holds TRUNCATE, REFERENCES or TRIGGER on a table with row-level security (a GRANT ALL), or when the URL switches it to another role. postgres.Open does this for every pool, also in a hand-written application; a pool that must use such a role on purpose (a reporting tool, never the serving pool) opts out in code with postgres.Open(ctx, cfg, postgres.AllowRowSecurityBypass()).

docker run --rm -e FORGE_ENV=production \
  -e FORGE_DATABASE_URL="$RUNTIME_DATABASE_URL" \
  -e FORGE_MIGRATION_DATABASE_URL="$MIGRATION_DATABASE_URL" myapp migrate
docker run -e FORGE_ENV=production -e FORGE_DATABASE_URL="$RUNTIME_DATABASE_URL" myapp

With forge deploy, name the migration credential under migrate.env in config/deploy.yml and keep it out of env: only the one-off migrate container receives it (DEPLOYMENT.md):

env:
  secret: [FORGE_DATABASE_URL]                    # every container
migrate:
  env:
    secret: [FORGE_MIGRATION_DATABASE_URL]        # the migrate step only
# .forge/secrets: FORGE_MIGRATION_DATABASE_URL=$FORGE_MIGRATION_DATABASE_URL
export FORGE_DATABASE_URL="$RUNTIME_DATABASE_URL" FORGE_MIGRATION_DATABASE_URL="$MIGRATION_DATABASE_URL"
forge deploy --dry-run     # prints the upload, run and rm commands for the migrate env file; values stay hidden, nothing runs
forge deploy
Development loop
forge dev                        # http://127.0.0.1:8080, reloads on save
forge dev --poll-interval 1s     # scan less often (large trees, slow disks)
forge dev --no-reload            # build and run once, no watching

forge db setup                   # create + migrate + seed
forge db prepare                 # setup if the database is missing, else migrate (safe to repeat)
forge db reset                   # drop + setup: a clean development database
forge db status                  # applied / pending migrations
forge db rollback -steps 1       # your newest migration; never a Forge framework table
forge db schema:dump             # write db/schema.sql (pg_dump, mysqldump or sqlite3 on PATH)
forge db schema:load             # load db/schema.sql into an existing, empty database (CI, tests)
forge db console                 # psql, mysql or sqlite3 on the application's database
forge db console -- -c 'select count(*) from forge_schema_migrations'

forge dev watches Go sources, templates, assets, migrations and .env.development/.env.local. The old process keeps serving until the new build succeeds, then gets SIGTERM and is replaced; build errors are printed and shown over the page, and the running app is not stopped. With FORGE_ENV=development (the default under forge dev) the forge process listens on FORGE_HTTP_ADDR as a development-only proxy, runs the application on a private loopback port and injects the reload script into HTML pages; the page's Content-Security-Policy gets a per-response nonce only if it would block that one script. Nothing of this is in your application binary (ADR 0035, Database).

drop, reset, schema:load and rollback are refused unless the database is local and FORGE_ENV is development or test: in production and staging, and for any server that is not on this machine, you must pass --force-production and either type the database name at the prompt or pass --yes with FORGE_ALLOW_DESTRUCTIVE=true in the process environment:

FORGE_ALLOW_DESTRUCTIVE=true forge db drop --force-production --yes

FORGE_MIGRATION_DATABASE_URL names the migration role (on PostgreSQL, the schema owner) for create, drop, schema:*, console, migrate and rollback; the database itself always comes from FORGE_DATABASE_URL.

Sign in

Applications created with --auth (or --tenants) come with Rails-style sign-in, sign-out, two-factor and password-reset pages, on every database: Forge identity persists in PostgreSQL, MySQL, MariaDB and SQLite alike (auth/postgres, auth/mysql, auth/sqlite, one shared contract suite). End to end, from a new application to a signed-in owner using a scaffold:

forge new myapp --auth && cd myapp
forge migrate                   # identity tables and the application's migrations
forge setup --email you@example.com
                                # prompts twice for the owner password (12+ characters)
forge seed                      # optional: loads db/seeds.go (idempotent)
forge dev

Open http://localhost:8080/session/new and sign in with your email and password (use localhost: the __Host- cookies need a secure context, which browsers grant to http://localhost). An application with organizations (--tenants) creates the first one with forge setup --org acme --email you@example.com --org-name "Acme Inc", and its sign-in page also asks for the organization. Then add pages and use them as the owner:

forge generate scaffold post title:string body:text   # --org-scoped with organizations; mounts itself in app/bootstrap/routes_gen.go
forge migrate                   # also grants posts:read and posts:write to the owner role
forge dev                       # http://localhost:8080/posts (signed-out visitors go to /session/new and back)
URL Page
/session/new sign in (email, password; and the organization, with --tenants)
/session/mfa authenticator or backup code, when FORGE_AUTH_MFA_KEY is set
/mfa two-factor settings of the signed-in account: turn on, replace the authenticator, new backup codes, turn off
/passwords/new forgot password (only when reset emails are configured, below)
/passwords/edit?token=... choose a new password from the emailed link

Two-factor authentication: forge new puts a development FORGE_AUTH_MFA_KEY in .env.local, so signed in, open http://localhost:8080/mfa, enter your password, add the key to an authenticator app (the otpauth:// link or the Base32 text) and confirm with the password and its 6-digit code; the page shows 10 backup codes once and signs you out everywhere. Every later change asks for the password and a current code (or a backup code), throttled and audited. For production, create a key with openssl rand -base64 32 | tr '+/' '-_' | tr -d '=' and keep it in a secret manager (docs/MFA.md, ADR 0045).

Release note: the MFA service and store contracts changed (a step-up proof bound to the account is required by every change of an existing factor, and a step-up backup code is used up with the change); custom stores and direct callers port as docs/MFA.md describes.

Release note: pages now embed the CSRF token masked with a fresh pad on every render (BREACH), so it no longer equals the __Host-forge_csrf cookie. Two tests of applications generated earlier compare them byte for byte and fail: TestLayoutRendersFlashAndCSRF (app/views/views_test.go) and TestBrowserPipelineRequiresCSRFToken (app/bootstrap/bootstrap_test.go). Replace the comparison with csrf.Match(cookie, embedded), as docs/VIEWS.md shows:

if !csrf.Match(cookies[0].Value, form.Body.String()) { // was: cookies[0].Value != form.Body.String()
	t.Fatalf("form body %q does not carry the CSRF cookie's token", form.Body.String())
}

Password-reset emails are off until outgoing mail is configured, and so are the reset pages and the "Forgot your password?" link. To try them locally, add to .env.local (or uncomment the same lines in .env.development), restart forge dev, request a reset at http://localhost:8080/passwords/new and open the link in the newest tmp/mail/*.eml:

cat >> .env.local <<'ENV'
FORGE_MAIL_URL=file:tmp/mail
FORGE_MAIL_FROM="myapp <no-reply@example.com>"
FORGE_AUTH_PASSWORD_RESET_URL=http://localhost:8080/passwords/edit
ENV

Logins are ASCII email addresses (setup refuses others) and the reset page answers at once, no sooner than 100 ms: the email is issued and sent a moment later by a bounded queue the application owns. After upgrading an existing application, run forge migrate (the login throttle gains a counter) and list accounts whose stored login is not ASCII, which can no longer sign in (docs/AUTHENTICATION.md):

forge migrate
psql "$FORGE_DATABASE_URL" -c "SELECT id, email FROM forge_users WHERE email_normalized !~ '^[!-~]+\$' OR email_normalized <> lower(email_normalized)"

Sessions end after two hours without a request (and at the latest 24 hours after sign-in). Set another idle limit, from 5m to 720h, with FORGE_AUTH_SESSION_IDLE_TIMEOUT. Existing applications run forge migrate first: sessions gain their last use, and those open during the upgrade count as used then (docs/AUTHENTICATION.md):

forge migrate
echo 'FORGE_AUTH_SESSION_IDLE_TIMEOUT=30m' >> .env.local

In production set FORGE_MAIL_URL=smtps://user:password@smtp.example.com:465 (or smtp://...:587 for STARTTLS), FORGE_MAIL_FROM and FORGE_AUTH_PASSWORD_RESET_URL=https://app.example.com/passwords/edit; see docs/MAIL.md.

Sign out with a DELETE /session form: the layout forge new --auth generates shows it (the sessions/_sign_out partial) to signed-in users, with {{if signedIn}}...{{end}}; in an application that added sign-in with forge generate authentication, add that line to your layout. forge setup refuses once an organization exists. The same commands exist in the application binary, for containers and production (the password is never an argument; --org SLUG only with organizations):

docker compose run --rm app setup --email you@example.com
printf '%s\n' "$OWNER_PASSWORD" | myapp setup --email you@example.com --password-stdin
printf '%s\n' "$OWNER_PASSWORD" | myapp setup --org acme --email you@example.com --password-stdin   # --tenants
myapp seed

These pages are the only way to sign in to a new application. The JSON authentication APIs (bearer POST /auth/login and /auth/logout, the cookie login for browser scripts, MFA completion, JSON password recovery and GET /v1/auth/session) are opt-in, since each is another place a password is entered:

forge new myapi --api-auth      # implies --auth: the pages plus the JSON authentication APIs
curl -s -X POST http://127.0.0.1:8080/auth/login -H 'Content-Type: application/json' \
  -d '{"email":"you@example.com","password":"your password"}'   # with organizations, also "tenant":"acme"

An application created without the flag adds them later: in one that has the pages, --api-auth writes nothing and prints only the JSON API wiring to paste into identityRoutes:

forge generate authentication --api-auth   # the pages exist: prints the JSON API wiring only

Applications generated earlier mount the JSON routes; remove those no API client uses (forge generate authentication lists them, docs/AUTHENTICATION.md). An application created without --auth gets everything --auth generates, wired with no edits (the pages, app/bootstrap/identity.go, the identity migrations in db/migration_sets_gen.go and the setup command in app/bootstrap/app_gen.go, the identityRoutes call in routes_gen.go, a development MFA key in .env.local, and an AGENTS.md that describes sign-in). Routes generated before keep the guard they were generated with:

forge generate authentication --dry-run   # preview the files and what is wired
forge generate authentication             # app/adapters/htmlauth, app/views/{sessions,passwords,mfa}, identity.go
forge migrate && forge setup --email you@example.com
forge generate authentication --api-auth  # the same, and the wiring of the JSON authentication APIs

Passwords are stored as Argon2id (m=19456,t=2,p=1, the same hashes Forge-Rust writes); PBKDF2 hashes from earlier releases are upgraded at the next sign-in (until none remain, failed sign-ins wait as long as a PBKDF2 check, so dormant accounts cannot be told from unknown ones; the guide has the SQL that resets dormant accounts), and at most one hash per CPU (16 at most) runs at once, beyond which sign-ins answer 503 with Retry-After. An application created with this release never stored a PBKDF2 hash and can drop that wait where identityRoutes builds the login service:

hasher, err := auth.NewPasswordHasher(auth.WithLegacyHashTimingFloor(false))
login, err := auth.NewLoginService(repository, repository, append(loginOptions, auth.WithLoginPasswordHasher(hasher))...)

A FIPS 140 build writes PBKDF2-HMAC-SHA256 instead (docs/AUTHENTICATION.md):

go build -tags forge_pbkdf2 -o bin/myapp ./cmd/myapp

Argon2id comes with the identity store the application links (auth/postgres, auth/mysql or auth/sqlite register it). An application with its own credential store registers it once, or it writes PBKDF2 and answers 503 for a stored $argon2id$ hash:

import "golang.org/x/crypto/argon2"

func init() { auth.RegisterArgon2id(argon2.IDKey) }

Applications created with --api have no pages and are refused. See docs/AUTHENTICATION.md.

Tables, pages and jobs

Adding a table:

forge generate migration create_notes --dry-run # preview exact migration files first
forge generate migration create_notes   # edit db/migrations/<timestamp>_create_notes.{up,down}.sql
forge migrate
forge test

Generate a working HTML CRUD (index, show, new, edit, create, update and destroy with forms, validation errors, flash notices and CSRF-protected forms) together with the migration, domain, application, persistence and JSON API of the resource:

forge generate scaffold post title:string body:text published:bool --global --dry-run
forge generate scaffold post title:string body:text published:bool --global
forge migrate
forge test

The command mounts the routes in app/bootstrap/routes_gen.go (below); the pages are served at /posts and are never public (see docs/VIEWS.md). In a PostgreSQL application a signed-out browser visiting /posts is sent to the sign-in page and back; once signed in as the owner, the pages work.

Generate a single server-rendered page (templates use Go's contextual HTML escaping and are embedded into the application binary):

forge generate view welcome --dry-run   # preview files without writing
forge generate view welcome
forge generate job cleanup --dry-run   # preview job and test files
forge generate job cleanup

The generator creates app/views/pages/welcome.gohtml (or app/views/DIR/NAME.gohtml for forge generate view DIR/NAME) and, for an application that has no views package, the base forge new generates: app/views/views.go, layouts/application.gohtml and shared/_flash.gohtml. Build the renderer once during application composition with views.New(), then call renderer.RenderRequest(w, r, "pages/welcome", http.StatusOK, data) from an HTTP handler (data needs Title and Content). Execution errors happen before response headers or body bytes are written.

Associations: belongs_to, has_many and many-to-many

A comment belongs to a post, a post has many comments, and posts and tags are joined many to many (ADR 0051). From an empty directory, with no code edited:

forge new blog --auth                # also: forge new blog -d mysql --auth
cd blog
forge migrate
forge setup --email you@example.com
forge generate scaffold post title:string body:text
forge generate scaffold comment body:text post:references   # belongs_to; a post has many comments
forge generate scaffold tag name:string
forge generate join post tag         # many-to-many: post_tags
forge migrate && forge dev

With organizations (forge new blog --tenants, PostgreSQL), sign up the organization and give each table its scope; the join takes its sides':

forge new blog --tenants && cd blog
docker compose up -d --wait postgres
forge migrate
forge setup --org acme --email you@example.com
forge generate scaffold post title:string body:text --org-scoped
forge generate scaffold comment body:text post:references --org-scoped
forge generate scaffold tag name:string --org-scoped
forge generate join post tag
forge migrate && forge dev

Sign in at http://localhost:8080/session/new, create a post, and its page shows its comments and tags in Turbo Frames (/posts/{id}/comments, /posts/{id}/tags), with a link to add a comment (the comment form picks the post in a select) and forms to tag and untag it. As JSON:

curl -s -b "$jar" http://localhost:8080/v1/posts/$post/comments
curl -s -b "$jar" http://localhost:8080/v1/posts/$post/tags
curl -s -b "$jar" -H "X-CSRF-Token: $csrf" -H 'Content-Type: application/json' -d "{\"tag_id\":\"$tag\"}" http://localhost:8080/v1/posts/$post/tags
curl -s -b "$jar" -H "X-CSRF-Token: $csrf" -X DELETE http://localhost:8080/v1/posts/$post/tags/$tag

($jar and $csrf as in the sign-in example of the generated README.)

  • post:references (alias post:belongs_to, optional with post:references?) is the column post_id with an index and a foreign key. With organizations the key is composite, (org_id, post_id) to posts (org_id, id), so no row can reference another organization's post even when the application's own checks are bypassed; the first migration that needs it declares UNIQUE (org_id, id) on posts. A single-organization application gets a plain foreign key. The referenced resource must exist, with the same scope.
  • The service checks the post before it writes: a post id that names no post (or another organization's) is 422 comment_post_not_found, even when the form's select is tampered with.
  • Deleting a post that still has comments is refused: 409 post_in_use as JSON, an alert on the post's page. Generate the child with --on-delete=cascade to delete its rows with the parent instead (forge generate scaffold note body:text post:references --on-delete=cascade).
  • forge generate join post tag creates post_tags (composite primary key, foreign keys to both sides that cascade), attach, detach and list use cases, GET|POST /v1/posts/{post_id}/tags, DELETE /v1/posts/{post_id}/tags/{tag_id} and the HTML page the post shows, guarded by posts:read/posts:write and tags:read. forge destroy join post tag removes it.
  • forge destroy refuses to remove a resource that something still references; forge generate tenancy refuses an application with associations (convert first, or create it with --tenants).
  • Field types are string text integer boolean timestamp uuid references, also spelled int bool time datetime belongs_to.
Generators

Every generator refuses to overwrite a file, writes nothing when it refuses, takes --dry-run, never edits app/bootstrap/bootstrap.go and never makes anything public by accident. forge generate scaffold, resource and controller mount their routes in app/bootstrap/routes_gen.go, a file they own: bootstrap.go calls its generatedRoutes once, each generator adds a block (and the guards, view renderer or live-update stream it needs, once), forge destroy takes the block back out, and running a generator again with the same arguments changes nothing. A hand edit of routes_gen.go is detected by its checksum and refused, never overwritten; to replace a guard (protect, protectCookie, protectPlatformWrite, public), declare a function of that name in another file of app/bootstrap. Applications generated before the seam (no generatedRoutes call in bootstrap.go) still get the wiring printed to paste, and so do those created before forge new passed the job registry to the seam (for generate job --queue). The mailer and job generators wire themselves into the seam too: generate mailer writes the mail sender and the previews, generate job --queue the handler's registration on the queue registry, and destroy takes them out again. Fields are name:type with type one of string, text, int, bool, time, uuid (a trailing ? makes one optional; quote it in shells that expand ?). The full reference is docs/GENERATORS.md.

In an application with organizations (forge new --tenants), tables take a scope, --org-scoped (--tenant-scoped is an alias) or --global; in any other application they are plain tables and take none, as below.

# g is short for generate and d for destroy, as in Rails
forge g scaffold product name:string price:integer
forge d scaffold product name:string price:integer

# Migration: add_COLUMNS_to_TABLE / remove_COLUMNS_from_TABLE with fields write the ALTER TABLE
forge g migration add_sku_to_products sku:string 'discontinued_at:time?'
forge g migration remove_sku_from_products sku:string
forge g migration backfill_prices   # blank: fill in both files before forge migrate (an empty migration is refused)

# Model: migration, domain entity, repository port and use cases, persistence adapter, tests; no HTTP
forge generate model post title:string 'body:text?' published:bool --dry-run
forge generate model post title:string 'body:text?' published:bool
forge migrate && forge test

# Sign-in and organizations, added to an existing application
forge generate authentication            # what forge new --auth generates, wired
forge generate tenancy --org acme        # an --auth application gains organizations: org_id on every table

# Controller: handlers, routes, views and tests for read-only actions, no persistence
forge generate controller pages home about_us --public     # GET /pages/home, GET /pages/about_us
forge generate controller reports monthly                  # guarded: needs the reports:read permission
forge generate controller status_checks index --api        # JSON handlers under /v1
forge routes

# Mailer: typed methods, text and HTML templates, browser previews, tests
forge generate mailer user welcome:name,url password_reset:name,reset_url
FORGE_MAIL_URL=file:tmp/mails FORGE_MAIL_FROM='App <app@example.com>' FORGE_MAIL_PREVIEW=true forge dev
#   http://localhost:8080/_mail/user/   (the generator already wired the previews)

# Job: a typed handler for the persistent queue with its enqueue function and tests
forge generate job send_welcome user_id:uuid --queue
forge generate job cleanup                                 # the recurring in-process stub, unchanged

# Everything above, then the same arguments with destroy to take it away again
forge destroy controller pages home about_us --dry-run
forge destroy controller pages home about_us
forge destroy model post title:string 'body:text?' published:bool --migrations

forge destroy GENERATOR ARGUMENTS... (the arguments the generator was run with) removes exactly the files the generator creates, and only while each is unchanged: a file you edited is listed, nothing is removed, and --force overrides. The same goes for files a bigger generation still needs: a model inside a resource or scaffold, a view a controller renders. Migrations are kept unless --migrations (run forge rollback first if they ran), shared files such as the views base are never removed, and the routes, mailers and job registrations are taken out of app/bootstrap/routes_gen.go (in an application without it, the wiring to take out of bootstrap.go is printed).

forge new myapp generates:

myapp/
  go.mod
  cmd/myapp/main.go            # forge.Main(bootstrap.Configure, <adapter> migrate/rollback, seed (and setup) commands)
  app/bootstrap/bootstrap.go   # opens the database, readiness check "database", GET /v1/status
  app/bootstrap/bootstrap_test.go
  app/bootstrap/routes_gen.go  # the routes generate scaffold/resource/controller add (generator-owned)
  app/architecture_test.go     # enforces the ring dependency rules (see Architecture test)
  AGENTS.md                    # guide for AI coding agents (see AGENTS.md for AI coding agents)
  app/adapters/htmlauth/       # PostgreSQL: sign-in, sign-out, two-factor and password-reset pages
  app/views/                   # layout, flash partial (and, with PostgreSQL, sessions/ and passwords/)
  db/db.go                     # embeds db/migrations into the binary
  db/seeds.go                  # db.Seed: idempotent seed data for `seed`
  db/migration_sets_gen.go     # the migration sets migrate applies, the job queue's among them (generator-owned)
  db/migrations/.keep
  .env.development             # committed development settings; no secrets
  Dockerfile                   # multi-stage, distroless, nonroot
  .dockerignore                # excludes .git, .github, storage, all .env* files
  compose.yaml                 # database server (if any), one-off migrate, app
  compose.observability.yaml   # opt-in overlay: collector, Prometheus, Tempo, Loki, Grafana
  observability/               # their configuration, Grafana data sources and dashboard
  .gitignore                   # ignores .env, .env.local, .env.*.local (and /storage/ for SQLite)
  README.md
  .github/workflows/ci.yml     # gofmt, vet, test -race (with a database service), govulncheck, docker build
  .github/dependabot.yml       # weekly Go module, Actions, Docker and Compose updates

--skip-docker leaves out Dockerfile, .dockerignore, compose.yaml, compose.observability.yaml, docker/ and observability/ (and CI's image job, Dependabot's Docker updates and the release workflow); --skip-ci leaves out .github/.

With --database none, db/, the database wiring, and the Compose and CI database services are omitted. With SQLite, the image creates /app/storage owned by the nonroot user and sets FORGE_DATABASE_URL=sqlite:///app/storage/production.db; mount a persistent volume there.

The generated Dockerfile builds a static binary (CGO_ENABLED=0, also for SQLite) and runs it in gcr.io/distroless/static-debian12:nonroot, configured for production behind a TLS-terminating proxy. See docs/DEPLOYMENT.md and docs/DATABASE.md.

Architecture test

Every generated application carries app/architecture_test.go, a test that enforces the dependency direction of its rings with the standard library (go/parser on the import lines, no tools to install). app/domain imports only itself, the standard library without net, net/http, database/sql, os, io/fs, io/ioutil, path/filepath, embed, html/template, unsafe and cgo, and Forge's uuid and fault; app/application imports app/domain, itself and Forge's uuid, fault, pagination and tenancy; app/adapters and every other package outside app/bootstrap never import app/bootstrap, the only composition root. Test files are checked too: domain and application tests use in-memory fakes. It passes on a fresh application and after every generator, and a violation fails with the file, the line and the import:

forge new myapp -d sqlite3 && cd myapp
forge g scaffold product name:string --global
forge test ./app/ -run TestArchitecture -v        # passes
printf 'package domain\n\nimport _ "net/http"\n' > app/domain/leak.go
forge test ./app/ -run TestArchitecture           # app/domain/leak.go:3: imports "net/http": the domain ring must not do I/O or speak HTTP: ...
rm app/domain/leak.go

docs/ARCHITECTURE.md has the rules; extraAllowedImports in the test admits a third-party package to the inner rings on purpose.

AGENTS.md for AI coding agents

Every generated application carries AGENTS.md, a guide of under 120 lines for AI coding agents (and new teammates): the rings and the architecture test, the forge commands (generate, destroy, migrate, dev, test, check, build), the routes seam (app/bootstrap/routes_gen.go is never edited by hand), the security defaults (CSRF, tenant scoping, secrets never logged) and how to run the tests. It describes only what that application has: an --api application has no CSRF or view paragraph, one without a database has no migration or tenant paragraph, and the sign-in paragraph appears only with PostgreSQL. A framework test checks that every command and file it names exists.

forge new myapp -d sqlite3 && cd myapp
head -20 AGENTS.md                       # what an agent reads first
printf '@AGENTS.md\n' > CLAUDE.md        # optional: tools that read only CLAUDE.md import it

Translations (i18n)

Rails I18n with JSON files: every generated page reads its text from config/locales (en.json, pt-BR.json, posts.en.json...), each request gets a locale (the locale cookie, Accept-Language, then the default), and forge i18n check lists what each locale lacks.

forge new shop --locale pt-BR                       # en.json + pt-BR.json; pt-BR is the default
cd shop && forge dev
curl -H 'Accept-Language: en' http://127.0.0.1:8080/        # Welcome to shop
curl -H 'Accept-Language: pt-BR' http://127.0.0.1:8080/     # Bem-vindo ao shop
forge generate scaffold post title:string --global  # adds config/locales/posts.en.json and posts.pt-BR.json (English text)
forge i18n check                                    # nothing missing: translate posts.pt-BR.json (values only; keep the "pt-BR" key)
FORGE_I18N_DEFAULT_LOCALE=en forge dev              # change the default without a rebuild

To let the URL choose the locale, set PathPrefix where useBrowserPipeline (app/bootstrap/bootstrap.go) builds the middleware: translator.Middleware(i18n.NegotiationOptions{PathPrefix: true}).

curl http://127.0.0.1:8080/en/      # Welcome to shop: GET / in English, whatever Accept-Language says
curl http://127.0.0.1:8080/pt-BR/   # Bem-vindo ao shop

The locale segment is removed before any application middleware runs (request IDs, telemetry and the panic log see the original path), so a path guard sees /admin for /pt-BR/admin wherever it is registered (App.Use or forge.WithHTTPMiddleware, before or after the i18n middleware).

In templates: {{t "posts.index.title"}}, {{t "users.inbox" "count" .Unread}} (CLDR plural forms), {{l .CreatedAt "short"}}, {{number .Total 2}}, <html lang="{{locale}}">; in Go: i18n.T(r.Context(), "posts.flash.created"). Keys ending in _html are trusted markup with escaped arguments; everything else is escaped by html/template. Missing translations show as translation missing: LOCALE.KEY in development and test and fall back to the default locale in production. See docs/I18N.md and ADR 0042.

Credentials and attribute encryption

Rails' credentials:edit and Active Record Encryption, with the sharp edges removed. forge new already created config/credentials/development.json.enc (committed) and development.key (git-ignored, mode 0600).

forge credentials edit                                   # edit development secrets in $EDITOR
forge credentials show                                   # structure only; values redacted
forge credentials init --environment production          # new key + encrypted file
forge credentials edit --environment production
forge credentials show --environment production --reveal # needs an interactive terminal
git add config/credentials/production.json.enc           # the key file is ignored

# deploy: the server needs only the key
docker run --rm -p 8080:8080 -e FORGE_ENV=production \
  -e FORGE_MASTER_KEY="$(cat config/credentials/production.key)" myapp

# rotate the key, keeping the old one readable while deployments catch up
forge credentials rotate-key --environment production --keep-old --print-key

Read secrets in the composition root, failing at startup if one is missing:

creds, err := credentials.Open(string(cfg.Environment), credentials.Options{
	Required: []string{"stripe.api_key"}, // enforced outside development and test
})
stripeKey, err := creds.Secret("stripe", "api_key") // config.Secret: prints as [REDACTED]

Encrypt a column (the ciphertext is bound to table, column and row, so it cannot be moved to another row; ssn is a TEXT column):

ring, _ := creds.KeyRing("encryption")        // created by `init`
cipher, _ := encryption.NewCipher(ring)
ssn := encryption.NewValue[string](cipher, "people", "ssn").ForRecord(id.String())
ssn.Set("123-45-6789")
_, err = db.Exec(ctx, `INSERT INTO people (id, ssn) VALUES ($1, $2)`, id, ssn)

Key rotation, blind indexes for exact-match search and batch re-encryption are in docs/ENCRYPTION.md; the credentials workflow, CI and rotation are in docs/CREDENTIALS.md.

To rotate a data key, make the new key active, re-encrypt each column and remove the old key only when a count of rows not on the active key is 0 (docs/ENCRYPTION.md). PostgreSQL tenant tables (row-level security) rotate once per tenant; an unscoped run on them is refused:

rewriter, err := postgres.EncryptedColumn(db, encryption.SQLColumn{Table: "people", Column: "ssn"},
	postgres.WithTenant(tenant))
if err != nil {
	return err
}
report, err := cipher.Reencrypt(ctx, rewriter, encryption.RotateOptions{})
-- as a role that sees every row (superuser or BYPASSRLS); stale must be 0
SELECT count(CASE WHEN ssn IS NOT NULL
                   AND substr(ssn, 1, length('fg1.2026-10.')) <> 'fg1.2026-10.' THEN 1 END) AS stale,
       count(*) AS total
  FROM people;

OpenAPI

JSON routes declare their request, response and error types next to the route, and Forge builds an OpenAPI 3.1 document from the Go types: accurate by construction for openapi.Typed handlers, checked by contract tests for the rest. Generated applications, resources and --api controllers are described out of the box, and so are the auth package's JSON routes (/auth/login, logout, MFA completion, password recovery and the cookie routes) in an application that registers them (forge new --api-auth).

forge new shop && cd shop && forge dev
curl http://127.0.0.1:8080/openapi.json             # served in development and test
curl -s http://127.0.0.1:8080/openapi.json | grep '"/auth/'   # --api-auth apps: the sign-in, recovery and cookie routes
forge openapi                                       # print it (runs Configure, never listens)
forge openapi --output openapi.json                 # write it to commit and diff
forge openapi --check openapi.json                  # exit 1 when the committed file is stale
FORGE_OPENAPI_ENABLED=false forge dev               # off; staging and production serve it only with =true
app.Handle("GET /v1/status", openapi.Typed(http.StatusOK,
	openapi.Operation{Summary: "Service status", Tags: []string{"status"}},
	func(*http.Request, openapi.NoBody) (statusResponse, error) { return statusResponse{Status: "ok"}, nil }))

app.Handle("POST /v1/posts", openapi.Describe(create, openapi.Operation{
	Summary: "Create a post", Permission: "posts:write",
	Request:   openapi.TypeOf[createPost](),
	Responses: []openapi.Response{openapi.JSON[post](http.StatusCreated)},
	Errors:    []int{http.StatusConflict},
}))

In tests, spec.CheckResponses(app.Handler(), report) validates every response against the document. Details, type mapping and tags: docs/OPENAPI.md, ADR 0043.

CLI

Command Description
forge new NAME [--database DB] [--auth] [--tenants] [--locale LOCALE] [--js] [--api-auth | --api] [--module PATH] [--dir DIR] [--forge-path DIR] [--skip-deps | --skip-lockfile] [--skip-database] [--skip-docker] [--skip-ci] Create an application. Refuses to write into a non-empty directory. --skip-lockfile is Forge-Rust's name for --skip-deps
forge dev [NAME] [--no-reload] [--poll-interval D] [--debounce D] Build and run ./cmd/<name>; on a change to Go sources, templates, assets, migrations or .env.* files rebuild, restart gracefully and reload the browser (development only). A failed build leaves the running app alone. --no-reload builds and runs once
forge test [ARGS] go test with the development environment loaded; ARGS default to ./...
forge check Type-check the application: compile its TypeScript, then go build ./..., go vet ./... and, when tsc is on PATH, tsc --noEmit (without tsc one note says how to install it; Forge-Rust: cargo check --all-targets)
forge format [--check] gofmt -l -w on the application's Go files (not vendor/, testdata/ or nested modules), listing the files it rewrote; --check lists the files that need formatting, writes nothing and exits 1 when there are any (CI)
forge lint golangci-lint run ./... (with the application's .golangci.yml when it has one); fails with the install command when golangci-lint is not on PATH (Forge-Rust: cargo clippy -D warnings)
forge build [NAME] go build -trimpath into bin/<name>
forge db COMMAND Database lifecycle like Rails' db:*: create, drop, reset, setup, prepare, migrate, rollback, status, seed, schema:dump, schema:load, console; see Development loop
forge migrate [status] Build the application and run its migrate (or migrate status) command (same as forge db migrate)
forge rollback [-steps N] Build the application and run its rollback command (default 1 step): only the application's own migrations, never Forge's framework migrations
forge setup [--org SLUG] --email EMAIL [--org-name NAME] [--password-stdin] Applications with Forge identity (--auth): create the first organization and owner account (--org only with organizations, --tenants; otherwise the one hidden organization); the password is prompted for (or piped, or FORGE_SETUP_PASSWORD), never an argument; refuses once an organization exists
forge seed Run the application's seed command: db.Seed in one transaction
forge generate migration NAME [--dry-run] Create or preview db/migrations/<UTC timestamp>_NAME.{up,down}.sql
forge generate view [DIR/]NAME [--dry-run] Create or preview a server-rendered page, app/views/DIR/NAME.gohtml (DIR defaults to pages), in the layout forge new generates
forge generate job NAME [--dry-run] Create or preview a cancellation-aware recurring job stub and tests; registration and schedule remain explicit
forge generate model NAME field:type... [--org-scoped | --global] [--plural P] [--dry-run] Create or preview a model: migration, domain, application (repository port, use cases) and persistence layers with tests; the model and persistence files of generate resource, without the HTTP layer
forge generate controller NAME ACTION... [--api] [--public] [--dry-run] Create or preview HTTP handlers for read-only actions, their routes, views (HTML) and tests, without persistence; --api generates JSON handlers; guarded unless --public
forge generate mailer NAME METHOD[:field,field]... [--dry-run] Create or preview a mailer: typed methods, text and HTML templates, previews and tests, on the mail package
forge generate job NAME [field:type...] --queue [--dry-run] Create or preview a typed handler for the persistent job queue, its enqueue function and tests
forge generate resource NAME field:type... [--org-scoped | --global] [--plural P] [--public] [--dry-run] Create or preview a complete resource: migration, domain, application, persistence and HTTP layers with tests, for the application's database, mounted in app/bootstrap/routes_gen.go. A scope only with organizations. Never public unless --public (no Forge identity): see below
forge generate scaffold NAME field:type... [--org-scoped | --global] [--plural P] [--broadcast] [--public] [--dry-run] Everything generate resource creates plus server-rendered HTML CRUD pages, forms and their tests; --broadcast adds live updates over Server-Sent Events. Never public unless --public: see below
forge generate authentication [--api-auth] [--dry-run] Add what forge new --auth generates (sign-in, sign-out, two-factor and password-reset pages, identity migrations, the setup command) to an application created without it, wired with no edits; in one that has it, --api-auth prints the JSON authentication APIs' wiring
forge generate tenancy [--org SLUG] [--dry-run] Turn an --auth application into one with organizations: a reversible migration adds org_id to every generated table (backfilled, indexed, NOT NULL, row-level security on PostgreSQL), rewrites the generated code as --org-scoped, and renames the default organization to SLUG; refuses edited files, applications without --auth and those that have organizations
forge destroy GENERATOR ARGUMENTS... [--force] [--migrations] [--dry-run] Remove what a generator created (model, resource, scaffold, controller, mailer, job, view): the same arguments as the generate command; a file that differs from what the generator writes, or that a bigger generation still needs, is never removed without --force, migrations are kept without --migrations
forge deploy [init|setup|rollback VERSION|prune|details|lock|maintenance] [--dry-run] [--json] Deploy the image to your servers over SSH with zero-downtime switches through forge-proxy, Kamal-style: see Deploy
forge logs [-f] [--role R] [--host H] [--since 1h] [--grep REGEX] [--lines N] Stream container logs from every host, prefixed host|
forge app exec [--role R] [--host H] [--interactive] [--reuse] CMD..., forge app shell [--image IMAGE] Run a command or a shell next to the deployed application
forge credentials init | edit | show | rotate-key [--environment E] Encrypted credentials: create a key and skeleton, edit in $EDITOR (plaintext shredded on every exit path), show redacted (--reveal on a terminal only), rotate the key. See docs/CREDENTIALS.md
forge i18n check [--json] Read-only: list the translations each locale in config/locales lacks (keys of the other locales and literal keys used by t and i18n.T), missing CLDR plural forms and unused keys; exit 1 when anything is missing. See Translations
forge doctor [--json] Load and validate configuration and check that the database server answers (a TCP connection within 3 seconds, no login; SQLite: the file); exits 1 when anything is wrong. JSON mode emits versioned, secret-safe diagnostics with remediation codes
forge inspect --json Read-only, versioned inventory of commands, migrations, static routes, modules and database adapter
forge routes [NAME] Table of every route registered while the application composes (HTML and JSON, including the /v1/<plural> routes of generated resources) with its resource action and the source line that registered it, plus Forge's built-in routes. Builds cmd/NAME and runs its routes command without serving HTTP: Configure sees app.Serving() false, so a generated application opens its PostgreSQL or MySQL pool lazily (postgres.Lazy, mysql.Lazy) and starts no scheduler or worker, and the command works with the database stopped and records no job tick; NAME picks cmd/NAME when there are several binaries. When the application cannot be built or configured it says why on stderr and lists what a scan of the source finds (literal paths only). Try forge routes | grep /v1
forge openapi [--output FILE | --check FILE] Build the application and run its openapi command with the development environment: print or write the OpenAPI 3.1 document of the described routes, list undescribed ones on stderr; --check exits 1 when FILE is out of date. See OpenAPI
forge help COMMAND, forge COMMAND --help (or -h) Print one command's usage, flags, examples and notes, and exit 0; for example forge help new or forge generate --help
forge help --json Emit the versioned command catalog with per-command flags, examples, environment inputs, exit codes and side effects
forge uuid [--count N] Print N UUIDv7 values (1-1000)
forge version Print the CLI version, commit, commit time and Go toolchain, e.g. forge v0.1.0 (3f2a9c1d07be, 2026-10-27T12:00:00Z, go1.27.3 linux/amd64); forge-proxy version does the same

Check, format and lint an application, from its directory (the counterparts of Forge-Rust's forge check, forge format and forge lint):

forge check             # go build ./..., go vet ./... and tsc --noEmit (if installed): type-check, no binary written
forge format --check    # list the Go files that need gofmt and exit 1 if any; writes nothing (CI)
forge format            # gofmt -l -w: rewrite them, listing each file
forge lint              # golangci-lint run ./...; if it is missing, prints the go install command

NAME for dev and build is needed only if cmd/ has several commands. migrate, rollback, setup, seed and db require exactly one.

Flags for forge new:

  • NAME must match ^[a-z][a-z0-9-]{0,62}$.
  • --module PATH sets the Go module path (default: NAME).
  • --dir DIR sets the output directory (default: NAME); it must be absent or empty.
  • --skip-deps skips dependency resolution (go get, go mod tidy).
  • --database DB / -d DB chooses the database: sqlite (default; alias sqlite3), postgresql (default with --tenants; aliases postgres, pg), mysql, mariadb, or none.
  • --auth adds Forge identity: sign-in, sign-out, two-factor and password-reset pages, the identity migrations and the setup command, on any database. Without it the application has no sign-in.
  • --tenants (implies --auth) gives users organizations: sign-in and setup take the organization, and generated tables are --org-scoped (an org_id column) or --global; PostgreSQL adds forced row-level security.
  • --skip-database is --database none; combining it with --database is an error.
  • --locale LOCALE sets the default locale of the generated pages: en (default), es or pt-BR. config/locales/en.json is always generated, plus LOCALE.json.
  • --js keeps the front end in plain JavaScript (app/javascript/*.js, served as written) instead of the default TypeScript; it cannot be combined with --api (usage error, exit 2). See TypeScript front end.
  • --api-auth also mounts the JSON authentication APIs (bearer and cookie login and logout, MFA completion, JSON password recovery, GET /v1/auth/session) beside the HTML sign-in pages; implies --auth. Without it the pages are the only way to sign in.
  • --api generates an API-only application, like rails new --api and Forge-Rust's forge new --api: GET / answers {"application":"NAME","version":"0.1.0"}, GET /v1/status, the health routes and the OpenAPI document stay, and there is no app/views, app/assets, app/javascript, config/locales, app/adapters/htmlauth, setup command or browser middleware (CSRF, method override, flash, locale negotiation). Authentication is opt-in, so migrate applies the application's own migrations (and the job schedules') and no identity schema. The database flags are unchanged. --api-auth, --auth, --tenants and --locale cannot be combined with it (usage error, exit 2). In such an application forge generate view, scaffold, authentication and controller without --api refuse and write nothing; resource, model, controller --api, mailer, job and migration work and write no view or asset.
  • --forge-path DIR is for framework contributors: it adds a replace directive pointing at a local checkout, then runs go mod tidy and go mod vendor so the Docker build context is self-contained. In such an application (any with a vendor/ directory) every forge generate ... that writes files ends with go mod vendor as well, so what it generated, which may import framework packages vendor/ did not hold, still builds; a --dry-run does not. Without it, forge new runs go get github.com/rocketinsidecorp/forge-go@<cli version or latest> followed by go mod tidy. The CLI version is the release stamped at build time or, for a CLI installed from the module proxy with go install github.com/rocketinsidecorp/forge-go/cmd/forge@VERSION, the module (pseudo-)version recorded in its build info (forge version reports it too). A CLI built from a checkout (go build, or go install run in it) resolves latest, even when Go stamps a VCS pseudo-version, because that commit may not be published.
  • --framework-path DIR is the deprecated former name of --forge-path. It still works for one more minor version and prints one deprecation line to stderr; passing both flags with different directories is a usage error.

forge generate migration NAME requires NAME to match ^[a-z][a-z0-9_]{0,99}$ and db/migrations to exist. If a migration with the same second already exists, the version is moved forward one second. It never overwrites a file.

forge generate job NAME [--dry-run] creates app/jobs/NAME.go and its test file. The generated stub returns an explicit not-implemented error until it is replaced, so registering it cannot silently report successful work. It does not edit bootstrap.JobsConfigure: choose a schedule and register the handler there explicitly. NAME must be lowercase snake_case with each segment starting with a letter. The generated shutdown hook calls scheduler.StopContext(ctx), which cancels running jobs and waits for them only within the shutdown budget (FORGE_HTTP_SHUTDOWN_TIMEOUT); a job that ignores its context keeps running and the hook returns the deadline error. Scheduler failures are reported but are not retried automatically. Every replica runs the scheduler, and the generated JobsConfigure passes the database's tick claimer (jobspostgres.NewTickClaimer, jobsmysql.NewTickClaimer) as jobs.SchedulerOptions.Ticks: each tick of a job runs on exactly one replica, the first to record it in forge_job_schedules (a framework migration that the generated main.go composes, so migrate creates it). Recording a tick is one statement; no connection is held while the job runs, and the outcome does not depend on the replicas' clocks. A tick that cannot be recorded is skipped and reported, and two replicas can run consecutive ticks of one job at the same time. A job that falls behind its schedule (a run longer than its interval, a paused VM, a slow tick claim, a wait for a free slot) does not replay the ticks it missed back to back: only the latest tick due runs, once, and only while it is at most jobs.SchedulerOptions.MaxLateness late (zero, the default, means one interval of the job's schedule; an explicit value below a minute is raised to a minute, as in Forge-Rust's Schedule::max_lateness); the others are skipped and reported to OnError as a *jobs.SkippedTicksError, which the generated JobsConfigure logs as a warning. To skip a tick that is more than ten minutes late, even for a daily job:

options := jobs.SchedulerOptions{
	// ... MaxConcurrentJobs, Telemetry, OnError and Ticks as generated ...
	MaxLateness: 10 * time.Minute,
}

An application generated earlier adopts it by composing the migration in cmd/<name>/main.go and passing the claimer to JobsConfigure in app/bootstrap/bootstrap.go. PostgreSQL:

// cmd/<name>/main.go: import jobspostgres "github.com/rocketinsidecorp/forge-go/jobs/postgres"
postgres.CommandsFromSets(authpostgres.Migrations(), jobspostgres.Migrations(), db.Migrations()),

// app/bootstrap/bootstrap.go, at the end of Configure (same import):
return JobsConfigure(app, jobspostgres.NewTickClaimer(database))

// JobsConfigure takes the claimer and sets it on the scheduler:
func JobsConfigure(app *forge.App, ticks jobs.TickClaimer) error {
	options := jobs.SchedulerOptions{
		// ... MaxConcurrentJobs, Telemetry and OnError as before ...
		Ticks: ticks,
	}

MySQL and MariaDB:

// cmd/<name>/main.go: import jobsmysql "github.com/rocketinsidecorp/forge-go/jobs/mysql"
forge.Main(bootstrap.Configure, append(mysql.CommandsFromSets(jobsmysql.Migrations(), db.Migrations()), mysql.SeedCommand(db.Seed))...)

// app/bootstrap/bootstrap.go, at the end of Configure (same import):
return JobsConfigure(app, jobsmysql.NewTickClaimer(database))

Then create the table and check the application:

forge migrate        # creates forge_job_schedules once main.go composes the jobs migrations
forge test

forge generate resource NAME field:type... [--org-scoped | --global] [--plural PLURAL] [--public] [--dry-run] generates a working, tested CRUD resource for the application's database (read from FORGE_DATABASE_URL, as forge inspect does). NAME is singular snake_case; the table and routes use the plural (category becomes categories; pass --plural for irregular words). In an application with organizations exactly one scope flag is required; in any other there is none. A field is name:type with type one of string (up to 255 characters), text, int (64-bit; alias integer), bool (alias boolean), time (UTC; aliases timestamp, datetime) or uuid; a trailing ? makes it optional (NULL). Quote optional fields in shells that expand ?. There is no floating-point or decimal type: store money as an int in minor units.

forge generate resource product name:string 'summary:text?' price_cents:int --dry-run
forge generate resource product name:string 'summary:text?' price_cents:int   # --org-scoped with organizations
forge migrate

It writes db/migrations/<timestamp>_create_products.{up,down}.sql and, for each of app/domain, app/application, app/adapters/persistence and app/adapters/httpapi, product.go with its product_test.go. Identifiers are UUIDv7 (uuid on PostgreSQL, CHAR(36) on MySQL and MariaDB, TEXT in a STRICT table on SQLite). Org-scoped resources read the organization from the context, bind org_id in every statement and, on PostgreSQL, run behind forced row-level security; MySQL, MariaDB and SQLite rely on the statement predicates alone. Updates and deletes need the version the client read (409 when stale), lists are newest-first keyset pages, and PATCH changes only the fields it names (null clears an optional one). PUT /v1/NAME/{id} is the same handler under Rails' other name for update, and the OpenAPI document describes both (updateNAME and replaceNAME):

curl -s -X PUT -H 'Content-Type: application/json' -d '{"version":1,"price":5900}' http://localhost:8080/v1/products/ID   # as PATCH (add the CSRF header with sign-in)

The generator refuses to overwrite any file (running it again with the same arguments is a no-op) and never edits app/bootstrap/bootstrap.go: it mounts the routes in app/bootstrap/routes_gen.go. RegisterProducts needs a protect function and asks it for products:read or products:write per route, so the endpoints are never public by accident. With Forge identity (--auth) the migration adds those permissions to forge_permissions, grants them to the owner role, and protect uses authhttp.RequireAudited: behind the bearer token of POST /auth/login in an application created with --api-auth (or given it by forge generate authentication --api-auth), and otherwise behind the session cookie of the sign-in page, with the CSRF token required in the X-CSRF-Token header of every change (authhttp.RequireCSRF):

forge generate authentication --api-auth   # paste the JSON authentication wiring it prints

The guard protect in routes_gen.go is written by the first generator that needs it and kept as it is, so adding --api-auth later does not change the routes already generated: once no route uses the cookie guard (destroy them), the next generator writes the bearer one, or replace protect with your own (copy it out of routes_gen.go into another file of app/bootstrap, then run a generator again).

With organizations, a global resource's write routes additionally stay behind a deny-all protectPlatformWrite you replace with operator-level authorization (organization roles must not be able to change rows every organization reads); without Forge identity the guard denies every request until you replace protect with your own (copy it out of routes_gen.go into another file of app/bootstrap, then run a generator again) (or generate the routes with --public, which serves them to anyone). The generated integration tests need the test database (FORGE_TEST_DATABASE_URL, set by .env.development) and fail instead of skipping when FORGE_TEST_REQUIRE_DATABASE=true.

forge generate scaffold NAME field:type... [--org-scoped | --global] [--plural PLURAL] [--broadcast] [--public] [--dry-run] is forge generate resource plus the HTML pages, the equivalent of rails g scaffold. It takes exactly the same arguments, writes exactly the same resource files, and adds:

  • app/adapters/htmlui/<name>.go and <name>_test.go: thin handlers for index, new, create, show, edit, update and destroy, and Register<Plural>(app, service, renderer, protect);
  • app/views/<plural>/{index,show,new,edit,_form,not_found}.gohtml;
  • the views base (app/views/views.go, layouts/application.gohtml, shared/_flash.gohtml) when the application lacks it.

The pages live at /<plural> (the JSON API stays at /v1/<plural>, behind the session cookie in an application without bearer sign-in, as above). Forms carry the CSRF token and use _method for PATCH and DELETE; invalid input re-renders the form with 422 and a message beside each field, success sets a flash notice and answers 303 See Other, a missing record shows a 404 page, and an edit based on a stale version shows the form again with 409 instead of overwriting the other change. The pages use the same guard as the JSON routes (<plural>:read for index and show, <plural>:write for forms and changes); with Forge identity the printed wiring authenticates the session cookie rather than a bearer token and sends signed-out browsers to the sign-in page. On PostgreSQL the migration also grants the new permissions to the owner role forge setup creates. --public (applications without Forge identity, global resources only) serves the pages and the JSON API to anyone instead. See docs/VIEWS.md.

TypeScript front end (the default)

forge new generates the front end in TypeScript, because Rails' Turbo setup has no types (ADR 0049). There is still no mandatory Node toolchain and no bundler: forge itself strips the types with esbuild's Go API (one pure-Go module, linked into the forge command only, never into your application), writes plain JavaScript to app/javascript/dist, and the binary embeds and serves that with digested URLs and the import map, as before. tsc only checks types, and only when you have it.

forge new myapp -d sqlite3 && cd myapp     # TypeScript by default
ls app/javascript app/javascript/dist      # application.ts, controllers/*.ts -> dist/*.js
forge dev                                  # recompiles on every .ts change, inline source maps
forge build                                # compiles to dist, then bin/myapp serves the precompiled JS (forge deploy compiles first too)
forge check                                # go build, go vet, and tsc --noEmit when tsc is installed
npm install --global --ignore-scripts typescript@6.0.3      # optional: the type checker (Node is needed for this only)
forge new myapp --js                       # opt out: plain JavaScript, exactly as before

What you get, beyond the sources: a strict tsconfig.json (noEmit, strict, moduleResolution: "bundler", the import map's bare names as paths) and app/javascript/types/, the declarations of Turbo and Stimulus copied unchanged from npm (Stimulus 3.2.2's own, the exact version Forge serves; Turbo 8.0.x's from DefinitelyTyped, because Turbo ships none). Without tsc, forge check still compiles and says once how to install it. In production no compiler runs: app/javascript/dist is generated, committed, embedded by go build (so the Dockerfile and CI need nothing else), and a generated test fails when a .ts file changed and dist did not, or when a compiled file is not the one forge wrote (each header records the hashes of its source and of its body; review dist/ diffs like code). In development forge dev and forge dev --no-reload hand go build the recompiled files through -overlay, so dist in your working tree stays the production output. Commands that only run the application (forge migrate, seed, routes, openapi) compile nothing and print nothing of their own. A .ts file outside controllers/ (say lib/format.ts) is compiled and embedded; add forgeassets.PinAllFrom(set, "lib") in app/assets/assets.go to import it by name.

Add a controller:

cp app/javascript/controllers/hello_controller.ts app/javascript/controllers/clipboard_controller.ts
#   in app/javascript/controllers/index.ts:
#     import ClipboardController from "controllers/clipboard_controller"
#     application.register("clipboard", ClipboardController)
forge build

The generated CI installs the same pinned tsc (npm install --global --ignore-scripts typescript@6.0.3, the one npm package any workflow installs) and runs tsc --noEmit. esbuild is pinned to 0.28.2 in go.mod; the update policy and the alternatives that were rejected are in the ADR, the supply-chain facts in docs/SUPPLY_CHAIN.md.

Hotwire: JavaScript, Turbo and live updates

Generated applications are full-stack like Rails 8, without Node. Static files are embedded and served with digested, immutable URLs; Turbo and Stimulus are vendored by Forge (pinned by SHA-256, licences retained) and loaded through an import map that the strict Content-Security-Policy allows by hash. Forms and links work under Turbo Drive with no changes.

forge new myapp && cd myapp
cat app/javascript/application.ts                       # import "@hotwired/turbo"; import "controllers"
cat app/javascript/controllers/hello_controller.ts      # a Stimulus controller (TypeScript; --js for JavaScript)
curl -s localhost:8080/session/new | grep -E 'importmap|modulepreload|stylesheet'   # with forge dev running (PostgreSQL)

# add a Stimulus controller
cp app/javascript/controllers/hello_controller.ts app/javascript/controllers/clipboard_controller.ts
#   in app/javascript/controllers/index.ts:
#     import ClipboardController from "controllers/clipboard_controller"
#     application.register("clipboard", ClipboardController)
#   in a template: <div data-controller="clipboard"> ... </div>
forge build                                             # compile it (forge dev does on save)

Real-time updates: Turbo Streams pushed to every open page over Server-Sent Events, with signed, tenant-scoped channel names, and PostgreSQL LISTEN/NOTIFY to reach every replica:

export FORGE_STREAMS_KEY=$(openssl rand -base64 32)     # required in staging and production
forge generate scaffold post title:string body:text --broadcast   # --org-scoped with organizations
forge migrate
forge test                                              # includes an end-to-end event stream test on PostgreSQL

Rotating the key without breaking open pages takes two redeploys, with the old key kept for verification only (FORGE_STREAMS_PREVIOUS_KEYS, up to 3 keys, comma-separated; why two steps: docs/VIEWS.md).

OLD_STREAMS_KEY="$FORGE_STREAMS_KEY"              # the key every replica uses now
NEW_STREAMS_KEY="$(openssl rand -base64 32)"      # keep both somewhere safe
# step 1: every replica learns the new key, but still signs with the old one
export FORGE_STREAMS_KEY=$OLD_STREAMS_KEY
export FORGE_STREAMS_PREVIOUS_KEYS=$NEW_STREAMS_KEY
# redeploy every replica, then
# step 2: sign with the new key; the old one keeps verifying pages already open
export FORGE_STREAMS_KEY=$NEW_STREAMS_KEY
export FORGE_STREAMS_PREVIOUS_KEYS=$OLD_STREAMS_KEY
# redeploy every replica; after the token lifetime (24 hours by default)
# unset FORGE_STREAMS_PREVIOUS_KEYS and redeploy

Release note: the generated htmlstreams.NewSigner now takes the previous keys as a third argument (FORGE_STREAMS_PREVIOUS_KEYS). streams.go is written once, so an application generated earlier must update it before the newly printed wiring compiles: see docs/VIEWS.md.

Paste the printed wiring into app/bootstrap/bootstrap.go (it defines the stream signer, mounts GET /streams behind the session cookie and the posts:read permission, and passes the broker to the pages). Open /posts in two browsers as users of the same organization: creating, editing or deleting a post in one updates the other without a reload. In your own code:

channel, _ := broadcast.ForTenant(tenant.ID, "posts")
row, _ := renderer.Fragment(nil, "posts/_item", data)      // escaped partial, no request-bound content
stream, _ := turbo.Prepend("posts", row)
_ = broadcast.Publish(ctx, broker, channel, stream)         // and {{turboStreamFrom "posts"}} in the page

Details, the multi-replica setup and the delivery guarantees (best effort, at most once) are in docs/VIEWS.md and ADR 0029.

Deploy

forge deploy ships the application image to servers that have only SSH and Docker (DEPLOYMENT.md, ADR 0030). Build the proxy image once from this repository and reference it by digest in proxy.image:

docker build -f cmd/forge-proxy/Dockerfile -t registry.example.com/acme/forge-proxy:0.1.0 .
docker push registry.example.com/acme/forge-proxy:0.1.0

Then, in the application:

forge deploy init                      # config/deploy.yml + .forge/secrets (0600, git-ignored)
$EDITOR config/deploy.yml .forge/secrets
forge deploy setup --dry-run           # every remote command, secrets redacted
forge deploy setup                     # docker check, network, forge-proxy, accessories
forge deploy                           # build, push, migrate, health-gated switch on every host
forge deploy details                   # versions per host and role, proxy target, lock
forge logs -f --grep ERROR             # host|line from every web host
forge app exec migrate status          # one-off container of the deployed image
forge app shell --image busybox:1.37   # debug sidecar (distroless has no shell)
forge deploy rollback 3f2a9c1d07be     # back to a retained version
forge deploy prune                     # drop stopped versions beyond retain
forge deploy lock acquire --message 'db maintenance'   # hold the deploy lock (status | acquire | release [--force])
forge deploy maintenance on            # forge-proxy serves its 503 page; `off` restores the application
forge deploy maintenance off
ssh deploy@host docker exec forge-proxy /usr/local/bin/forge-proxy status   # forge-proxy: current targets per host

A deploy migrates before any switch, keeps the previous containers running until every host passed its health gate, and routes all hosts back if one fails. Exit codes: 2 invalid arguments (nothing ran), 3 lock held, 4 unhealthy (reverted), 5 migration failed, 6 hook failed, 7 not fully reverted.

forge-proxy bounds what one client can make it hold: request headers up to -max-header-bytes (64 KiB; 431 above it) and -max-connections open connections per listener (10000). They are flags of forge-proxy run, with those defaults in every deploy:

go run ./cmd/forge-proxy run -h   # in a forge-go checkout: lists -max-header-bytes, -max-connections and the rest

On PostgreSQL with separate roles, the schema owner's credential goes to the migrate step alone (migrate.env, see Database roles):

$EDITOR config/deploy.yml .forge/secrets   # uncomment migrate.env and the FORGE_MIGRATION_DATABASE_URL line
forge deploy --dry-run                     # prints the migrate env upload, run and rm commands; no value is printed, nothing runs

Release note: retain: 1 in config/deploy.yml is now refused (retain counts the running version, so 1 would prune the previous one and leave nothing to roll back to); use 2 to 100, default 5. The deploy lock also needs ~/.forge on the hosts to be on a file system with hard links.

Client addresses: behind forge-proxy the proxied role trusts X-Forwarded-For only from the forge Docker network (forge deploy sets FORGE_HTTP_TRUSTED_PROXIES to its subnets), so sign-in and reset throttles, ratelimit.ByClientIP and audit count each client, not the proxy. Behind any other proxy or load balancer, list its networks yourself (empty, the default, trusts nobody; /0 and any IPv4 prefix shorter than /8 or IPv6 prefix shorter than /16 are refused):

export FORGE_HTTP_TRUSTED_PROXIES=10.0.0.0/24,fd00:1::/64   # proxy CIDRs or addresses
# config/deploy.yml: override the automatic value ("" trusts nobody)
env:
  clear:
    FORGE_HTTP_TRUSTED_PROXIES: "172.18.0.0/16"

See ADR 0037.

Development environment files

forge dev, forge test, forge db, forge migrate, forge rollback, and forge doctor read .env.development and then .env.local from the current directory; a later file overrides an earlier one, and variables already set in the process environment always win. dev, test, db, migrate, and rollback set FORGE_ENV=development when no source sets it.

The format is deliberately small: KEY=VALUE lines, blank lines and lines starting with # ignored, an optional export prefix, and one pair of matching single or double quotes removed from the value. There is no variable interpolation and no inline comment syntax. Parse errors name the line number, never its content.

.env.development is committed and must not contain real secrets; put personal overrides in .env.local, which is git-ignored.

forge doctor validates that configuration and checks that the database server answers: a TCP connection to its host within 3 seconds, never a login (SQLite: whether the file exists). It exits 1 when anything is wrong:

forge doctor          # configuration: ok ... database reachability: the server accepts connections
forge doctor --json   # {"ok":true,...}; database.unreachable when the server does not answer
Application binary commands

Binaries built with forge.Main accept:

Command Description
serve Default. Serve HTTP until SIGINT/SIGTERM
healthcheck Probe /health/live on loopback; used by the Docker HEALTHCHECK
openapi [--output FILE | --check FILE] Run Configure without listening and print or write the OpenAPI document (what forge openapi runs)
routes Run Configure without listening and print the route table (what forge routes runs)

While routes and openapi run Configure, app.Serving() is false. The generated Configure then opens its database pool lazily and starts no scheduler, job worker or recovery queue, so both work without a reachable database and never run or claim a job. A hand-written Configure does the same:

database, err := postgres.Open(ctx, app.Config().Database, postgres.Lazy(!app.Serving()))
// ...
if app.Serving() {
	scheduler.Start() // and worker.Start(ctx), with their OnShutdown hooks
}
FORGE_DATABASE_URL=postgres://app:app@127.0.0.1:1/down forge routes   # still lists every route

| help | List the available commands | | migrate [status], rollback [-steps N] | Added by <adapter>.Commands (generated applications with a database); rollback reverts application migrations only | | seed | Added by <adapter>.SeedCommand(db.Seed): the application's idempotent seed data in one transaction | | setup [--org SLUG] --email EMAIL [--org-name NAME] [--password-stdin] | Added by auth<adapter>.SetupCommand(auth<adapter>.SetupOrganizations(Organizations)) in app/bootstrap/app_gen.go (applications with --auth): the first organization and owner, once | | jobs status, jobs dead [-queue Q] [-kind K] [-limit N], jobs show ID, jobs retry ID, jobs discard ID, jobs prune [-older D] | Added by queuepostgres.Commands() (queuemysql, queuesqlite for the other databases) (forge new wires them in every application with a database; JOBS): inspect and repair the queue; payloads are never printed |

The jobs commands ship inside the production image, so operators need no extra tooling:

docker compose run --rm app jobs status                # jobs per queue and state
docker compose run --rm app jobs dead                  # dead jobs; also -queue Q, -kind K, -limit N
docker compose run --rm app jobs show JOB_ID           # one job's metadata
docker compose run --rm app jobs retry JOB_ID          # move a dead job back to pending
docker compose run --rm app jobs discard JOB_ID        # delete a dead job permanently
docker compose run --rm app jobs prune -older 24h      # delete finished jobs

Workers claim up to WorkerOptions.BatchSize jobs (default 10, never more than their free slots) per round trip, and the outbox dispatcher claims DispatcherOptions.BatchSize events (default 10) per transaction and acknowledges each on its own. On PostgreSQL, queuepostgres.WithNotify() wakes idle workers as soon as a job is enqueued (LISTEN/NOTIFY, with polling as the fallback) instead of after up to 10 seconds of idle backoff:

store, err := queuepostgres.NewStore(database, queuepostgres.WithNotify())
worker, err := queue.NewWorker(store, registry, queue.WorkerOptions{BatchSize: 10})
dispatcher, err := outboxpostgres.NewDispatcher(database.Pool(), handler, outboxpostgres.DispatcherOptions{Workers: 2, BatchSize: 50})

FORGE_JOBS_NOTIFY=true|false switches the wake-up per deployment, with Forge-Rust's meaning: unset keeps the code's choice, false turns it off even where the code passes WithNotify() (cfg.Jobs.Notifies(codeChoice) decides; the QueueConfigure that forge new generates asks it with false, so PostgreSQL applications wake only when it is true):

FORGE_JOBS_NOTIFY=true forge dev          # enqueues announce; workers LISTEN
FORGE_JOBS_NOTIFY=false ./bin/shop serve  # off, whatever the code chose
docker compose run --rm app jobs status                # ready, running and oldest-ready per queue: latency at a glance

Exit status is 0 on success, 1 on failure (including invalid configuration), and 2 for an unknown command, an unknown or invalid flag, or invalid arguments (the same statuses as Forge-Rust).

For agent and script integrations, prefix any Forge CLI command with --error-format=json to emit a versioned, secret-safe error object on stderr when the command fails. Error messages intentionally omit underlying error details and input values; doctor --json and inspect --json keep their command-specific diagnostic reports on stdout.

Rate limiting, cache and file storage

Rails 8's rate_limit, Solid Cache and Active Storage, on the standard-library core. Details: RATE_LIMITING, CACHING, STORAGE.

Configure (all optional; invalid values stop the process at startup):

export FORGE_RATE_LIMIT_MAX_KEYS=100000        # in-memory limiter buckets
export FORGE_CACHE_MAX_ENTRIES=10000 FORGE_CACHE_DEFAULT_TTL=1h
export FORGE_STORAGE_ROOT=/var/lib/app/storage # enables disk storage
export FORGE_STORAGE_SIGNING_KEY=$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=')  # required in production

Rate limit one route (rate_limit to: 10, within: 3.minutes, only: :create), per client address:

limiter, _ := ratelimit.NewMemory(ratelimit.MemoryOptionsFromConfig(app.Config().RateLimit))
create, err := ratelimit.Middleware(limiter, ratelimit.ByClientIP, ratelimit.Limit{N: 10, Per: 3 * time.Minute})
err = r.Handle("POST /comments", create(http.HandlerFunc(h.Create))) // 429 + Retry-After over the limit

Cache (memory, or shared in the database; stampede-protected Fetch):

c, _ := cache.NewMemory(cache.MemoryOptionsFromConfig(cfg.Cache))
price, err := cache.FetchJSON(ctx, c, cache.Key("price", sku), 10*time.Minute, loadPrice)
// A cached value that no longer decodes is a miss, logged once per key at
// warn level (key hashed) and counted:
undecodable := cache.DecodeFailures()

Shared stores for several replicas (PostgreSQL shown; mysql and sqlite work the same). Compose their migrations, then run app migrate:

forge.Main(bootstrap.Configure, postgres.CommandsFromSets(db.Migrations(),
    ratelimitpostgres.Migrations(), cachepostgres.Migrations())...)
limiter, _ := ratelimitpostgres.NewLimiter(db, ratelimit.StoreOptions{})
c, _ := cachepostgres.NewStore(db, cache.StoreOptions{})
app migrate          # creates forge_rate_limits and forge_cache_entries
app rollback         # reverts your newest migration, never these framework tables
app migrate status   # a framework set you stopped composing shows as "disabled"

rollback refuses (exit status 1) when only framework migrations are left, and removing a framework set from CommandsFromSets later keeps migrate working; reverting a framework migration is the explicit Migrator.DownFramework API (ADR 0039).

File storage: uploads with size and type limits, signed expiring links, safe serving (only raster images inline, everything else a download):

store, _ := disk.NewFromConfig(cfg.Storage)
signer, _ := storage.NewSignerFromConfig(cfg.Storage)
files, _ := storage.NewHandler(store, signer, storage.HandlerOptions{})
_ = r.Handle("GET /files/{key...}", files)

got, err := storage.Receive(w, r, store, storage.UploadOptions{
    Field: "avatar", AllowedTypes: []string{"image/png", "image/jpeg"},
    MaxFileBytes: cfg.Storage.EffectiveMaxUploadBytes(), Prefix: "avatars"})
url, _ := signer.URL("/files", got.Files[0].Key, storage.SignOptions{Inline: true})

There is no S3 adapter yet (see the roadmap). The upload limit cannot exceed FORGE_HTTP_MAX_BODY_BYTES.

Observability

Development log

forge dev writes a Rails-style log: one line per request (method, route pattern, status, time, number of SQL queries, request ID) and per SQL statement (text and time, never the arguments; yellow when slow), in color on a terminal. Job lines show failures and retries always; successes only when the worker is built with LogSuccesses (below). Production keeps JSON with the same event names (duration is in nanoseconds there).

forge dev                                  # pretty on a terminal: nothing to set
FORGE_LOG_FORMAT=pretty forge dev          # the same layout on a pipe or a file
NO_COLOR=1 forge dev                       # the layout without ANSI colors
FORGE_LOG_SLOW_QUERY=50ms forge dev        # statements slower than 50ms turn yellow (default 100ms)
FORGE_LOG_FORMAT=json forge dev            # text, json or auto (the default) also work
FORGE_LOG_LEVEL=debug ./your-app           # the request, SQL and job lines are Debug outside development
FORGE_LOG_REDACT_KEYS=iban,card forge dev  # more keys to hide, on top of the defaults
01:42:35.143  HTTP  GET /  200  470µs  queries=0  req=01a17b06-0787-7333-a91f-9b6aeeb55a45
              SQL   182µs  SELECT "id", "name", "price" FROM "products" ORDER BY "id" DESC LIMIT ?
01:42:35.153  HTTP  GET /products  200  566µs  queries=1  req=01a17b06-0790-727b-8ebe-36a51ae90690
01:42:35.172  HTTP  GET -  404  91µs  queries=0  req=01a17b06-07a4-71ee-9912-e2e721cf4158
01:42:35.200  JOB   send_welcome succeeded  35ms  queue=default attempt=1

The value of every log attribute whose key contains password, passwd, secret, token, authorization, cookie, api_key, apikey, session, email, phone, cpf or cnpj (and api-key, access_key, private_key, credential, csrf, jwt, otp, dsn, database_url) is written as [REDACTED] in every format. Redaction is by key only: messages and values (structs, headers) are not inspected, so log identifiers, not objects. A worker you build logs its successes at Info in development with queue.WorkerOptions{LogSuccesses: cfg.EventLevel() == slog.LevelInfo}. The layout, the variables, the redaction rules and the JSON events: docs/OBSERVABILITY.md.

Telemetry

Telemetry is on by default and exports nothing: logs carry trace_id, and incoming traceparent headers are ignored unless you opt in with FORGE_TELEMETRY_TRUST_TRACEPARENT=true behind a proxy that sanitizes them. Pick an export path with environment variables:

# Prometheus metrics on a separate, private listener
FORGE_TELEMETRY_METRICS_ADDR=127.0.0.1:9464 forge dev
curl -s http://127.0.0.1:9464/metrics | grep http_server_request_duration_seconds

# Traces and metrics to an OpenTelemetry Collector over OTLP/HTTP
docker run --rm -p 4318:4318 -v "$PWD/otel-collector.yaml:/etc/otelcol/config.yaml:ro" otel/opentelemetry-collector:0.111.0
FORGE_TELEMETRY_EXPORTER=otlp FORGE_TELEMETRY_OTLP_ENDPOINT=http://127.0.0.1:4318 \
  FORGE_TELEMETRY_SERVICE_NAME=myapp forge dev

# Logs too: every record, with trace_id and span_id, also goes to the collector
# (stdout keeps the same lines; same FORGE_TELEMETRY_OTLP_* settings)
FORGE_TELEMETRY_LOGS_EXPORTER=otlp FORGE_TELEMETRY_OTLP_ENDPOINT=http://127.0.0.1:4318 forge dev

# In a container: bind to all interfaces and confirm it (never publish the port)
FORGE_TELEMETRY_METRICS_ADDR=0.0.0.0:9464 FORGE_TELEMETRY_METRICS_PUBLIC=true forge dev

# Spans in the development log, or no telemetry at all
FORGE_TELEMETRY_EXPORTER=log forge dev
FORGE_TELEMETRY_ENABLED=false forge dev

If your standard-output logger redacts with slog.HandlerOptions.ReplaceAttr, build it with telemetry.NewOutputHandler so the log exporter applies the same redaction (otherwise the collector receives what stdout hides):

logger := slog.New(telemetry.NewOutputHandler(os.Stdout, telemetry.LogFormatJSON,
	&slog.HandlerOptions{ReplaceAttr: redact}))
app, err := forge.New(forge.WithLogger(logger))

Pass app.Telemetry() to adapters to trace database statements and jobs and to export their connection pool (in use, idle, limit, waits): postgres.Open(ctx, cfg, postgres.WithTelemetry(app.Telemetry())) (generated applications already do). Go runtime metrics (goroutines, heap, GC) are always recorded. Collector and Prometheus configuration, the metric catalogue, cardinality rules and what is never recorded: docs/OBSERVABILITY.md.

Grafana, Prometheus, Tempo and Loki, preconfigured

Every generated application includes an opt-in observability stack (compose.observability.yaml and observability/): an OpenTelemetry Collector, Prometheus, Tempo (traces), Loki (logs) and Grafana with the data sources, trace-to-log links and a starter dashboard (requests, database and connection pool, Go runtime, jobs, logs, traces) provisioned; the app's logs reach Loki over OTLP. Everything stays on your machine and every port is bound to 127.0.0.1 (ADR 0038).

forge new myapp && cd myapp
export GRAFANA_ADMIN_PASSWORD="$(openssl rand -base64 18)"   # required; or put it in .env
docker compose -f compose.yaml -f compose.observability.yaml up --build
curl http://127.0.0.1:8080/v1/status
# Grafana http://127.0.0.1:3000 (user admin): dashboard "Forge application",
# Explore > Tempo (traces), Explore > Loki (logs); Prometheus http://127.0.0.1:9090
# Alert rules (observability/alerts.yml): http://127.0.0.1:9090/alerts
# compose interpolates the whole file, so GRAFANA_ADMIN_PASSWORD must be set for down too
docker compose -f compose.yaml -f compose.observability.yaml down      # add -v to delete the data

Prometheus also loads observability/alerts.yml: scrape target down or no metrics arriving, more than 5% of requests failing with 5xx, p95 latency over 500 ms, database pool over 90% in use or callers waiting for a connection, queue jobs that died, scheduled jobs that failed. Open http://127.0.0.1:9090/alerts to see which fire; edit the thresholds in the file and docker compose -f compose.yaml -f compose.observability.yaml restart prometheus. The stack has no Alertmanager, and a framework test fails if a rule uses a metric or label Forge does not export (the rules).

The default docker compose up does not start it. See docs/OBSERVABILITY.md for how logs reach Loki and what the dashboard shows.

Supply chain: fuzzing, SBOM, signed images and binaries

Every fuzz target runs on each pull request and nightly for longer; SBOMs describe the binaries and images; release images and the release checksums are signed keyless with Sigstore cosign from v* tags only, and every release archive has a GitHub build provenance attestation (Install shows the commands); actions and images are pinned by SHA and digest (docs/SUPPLY_CHAIN.md, ADR 0044).

# fuzz locally: every target 10s (what CI runs), one package longer, one target
scripts/fuzz.sh
scripts/fuzz.sh -t 2m ./i18n
go test -run '^$' -fuzz '^FuzzDatabaseURL$' -fuzztime 1m ./config
scripts/fuzz.sh -l                       # list the targets
# a failing input lands in <package>/testdata/fuzz/<target>/: fix the bug, commit the input

# verify a released image (cosign v3+), then deploy it by digest
cosign verify ghcr.io/rocketinsidecorp/forge-proxy:v0.1.0 \
  --certificate-identity-regexp '^https://github\.com/rocketinsidecorp/forge-go/\.github/workflows/release\.yml@refs/tags/v' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com
docker buildx imagetools inspect ghcr.io/rocketinsidecorp/forge-proxy:v0.1.0 --format '{{ .Manifest.Digest }}'

# read the SBOMs: an image's (SPDX attestation), the binaries' (CycloneDX)
docker buildx imagetools inspect ghcr.io/rocketinsidecorp/forge-proxy:v0.1.0 \
  --format '{{ json (index .SBOM "linux/amd64").SPDX }}' | jq -r '.packages[] | "\(.name) \(.versionInfo)"'
scripts/sbom.sh dist/sbom                # builds forge and forge-proxy; Syft runs in Docker
jq -r '.components[] | "\(.name) \(.version)"' dist/sbom/forge.cdx.json

Applications from forge new keep an SBOM of their image as the sbom artifact of every CI run, and get an opt-in .github/workflows/release.yml (enable it with gh variable set FORGE_RELEASE_IMAGES --body true, then push a v* tag); their README shows how to verify the result. Their Dockerfile pins both base images by digest, the same ones Forge builds with (TestDockerfilesPinBaseImagesByDigest), and Dependabot proposes new digests. To move to another Go release, replace the whole reference:

docker buildx imagetools inspect golang:1.27.4-alpine --format '{{ .Manifest.Digest }}'
# then in Dockerfile: FROM --platform=$BUILDPLATFORM golang:1.27.4-alpine@sha256:<that digest> AS build

Benchmarks

Micro-benchmarks of the hot paths (router, middleware chain, JSON and HTML rendering, CSRF, session cookie, password hashing, UUIDv7, i18n, attribute encryption, rate limiting) and an end-to-end HTTP benchmark of a generated application on PostgreSQL, with a load generator written with the standard library only. Methodology, scenarios, caveats and a dated results table: docs/BENCHMARKS.md.

# micro-benchmarks, one package at a time so they do not compete for CPU
go test -p 1 -run '^$' -bench . -benchmem -count 3 ./...
go test -run '^$' -bench 'RouterMatch|MiddlewareChain' -benchmem ./router .   # a subset

# end to end: builds a generated app against this checkout, runs it on PostgreSQL,
# prints requests/s and p50/p95/p99 of /health, welcome, index, JSON show, create
export FORGE_TEST_POSTGRES_URL='postgres://postgres:postgres@127.0.0.1:5432/postgres?sslmode=disable'
scripts/bench.sh                                  # 16 workers, 10s per scenario, 3 runs
BENCH_DURATION=3s BENCH_RUNS=1 scripts/bench.sh   # a quick check
scripts/bench.sh -format json                     # the same figures as JSON

CI runs every benchmark once (go test -run '^$' -bench . -benchtime=1x ./...) so they keep compiling and running; it never compares numbers.

Example

examples/basic-api is the reference service: a task API on PostgreSQL laid out in hexagonal layers (app/domain, app/application, app/adapters/persistence, app/adapters/httpapi, app/bootstrap) with SQL migrations in db/migrations. It demonstrates repositories on DBTX, a transaction, optimistic locking with a version column, keyset pagination, and the JSON error format. Endpoints: POST /v1/tasks, GET /v1/tasks, GET /v1/tasks/{id}, and POST /v1/tasks/{id}/complete.

The repository's compose.yaml runs it with PostgreSQL:

docker compose up --build
curl -i -X POST -H 'Content-Type: application/json' -d '{"title":"try forge"}' \
  http://127.0.0.1:8080/v1/tasks

Developing the framework

go vet ./...
golangci-lint run
go test -race ./...
govulncheck ./...

SQLite tests always run. PostgreSQL and MySQL/MariaDB tests are skipped unless a server is configured whose user can create databases:

export FORGE_TEST_POSTGRES_URL='postgres://forge:forge@127.0.0.1:5432/postgres?sslmode=disable'
export FORGE_TEST_MYSQL_URL='mysql://root:forge@127.0.0.1:3306/mysql'   # MySQL or MariaDB

Set FORGE_TEST_REQUIRE_DATABASE=true to make a missing server a failure.

Coverage: scripts/coverage.sh measures statement coverage (go test -coverpkg=./..., with the database tests when the servers above are set), prints the gross total and the gated total (production code, without the contract test kits) with a per-package table, and fails when the gated total is below .coverage-floor, the gross total is below 95.0%, or one of the security-relevant packages (auth, auth/postgres, auth/mysql, auth/sqlite, auth/authsql, credentials, csrf, encryption, postgres/internal/pgurl, postgres) is below 95% of its own statements (docs/TESTING.md).

CI (.github/workflows/ci.yml):

  • quality: gofmt, vet, golangci-lint, race tests against PostgreSQL 18 and MySQL 8.4 services and a real sshd (every test run with an explicit -timeout 30m, TestGeneratedApplication skipped), benchmarks, and govulncheck;
  • generated: TestGeneratedApplication, the applications forge new generates for each database, vetted, built and tested against PostgreSQL 18 and MySQL 8.4, then the README quickstarts (SQLite --auth, MySQL --auth, PostgreSQL --tenants, the welcome page) driven over HTTP (a job of its own: these workflows are serial and slow);
  • coverage: scripts/coverage.sh against PostgreSQL 18 and 16 and MariaDB, gated by .coverage-floor, the gross floor and the per-package floors;
  • fuzz: every fuzz target for 10 seconds (scripts/fuzz.sh);
  • sbom: CycloneDX SBOMs of the forge and forge-proxy binaries;
  • mariadb: the mysql and auth/mysql tests, the generated MySQL applications and the MySQL quickstart against MariaDB 11.8;
  • docker: for each of PostgreSQL, MySQL, MariaDB, and SQLite, generates an application, builds its image, and smoke-tests its Compose stack with docker compose up --wait (the PostgreSQL run also builds the example image and an SBOM of the generated one).

Other workflows: fuzz.yml fuzzes each target for 10 minutes every night, scorecard.yml runs OpenSSF Scorecard weekly, and release.yml publishes signed images for v* tags only.

Documentation

Documentation

Overview

Package forge composes the framework runtime.

Index

Constants

View Source
const OpenAPIPath = "/openapi.json"

OpenAPIPath is where the application serves its OpenAPI document when FORGE_OPENAPI_ENABLED allows it (config.Config.ServesOpenAPI). The route is mounted when the first request arrives, and not at all when the application registered a route for this path itself.

Variables

View Source
var ErrUsage = command.ErrUsage

ErrUsage marks a Command error caused by invalid arguments; Execute exits with status 2 for it instead of 1. Wrap it: fmt.Errorf("%w: ...", ErrUsage). It is command.ErrUsage, the same error value.

Functions

func Execute

func Execute(ctx context.Context, args []string, lookup func(string) (string, bool), stdout, stderr io.Writer, configure Configure, commands ...Command) int

Execute runs an application command and returns its exit code: 0 on success, 1 on failure, 2 on misuse. Cancelling ctx triggers a graceful shutdown of "serve". It exists so the full startup path can be tested without touching process state.

Commands run by Execute receive no standard input (CommandEnv.Stdin is nil); Main passes os.Stdin.

func Main

func Main(configure Configure, commands ...Command)

Main is the conventional entrypoint for Forge applications:

func main() { forge.Main(bootstrap.Configure, postgres.Commands(db.Migrations())...) }

The binary serves HTTP by default ("serve") and also answers "healthcheck", which lets minimal container images (no shell, no curl) run a Docker HEALTHCHECK against the process itself, "openapi", which prints the OpenAPI document of the configured routes without serving them, "routes", which prints the table of those routes, plus any extra commands. Main exits the process.

Types

type App

type App struct {
	// contains filtered or unexported fields
}

App is a Forge application: the router, the health registry, the HTTP server with its fixed security chain, telemetry and the shutdown hooks. Register routes, middleware and readiness checks on it before it serves its first request, then call Run.

func New

func New(opts ...Option) (*App, error)

New builds an App from the options, which default to config.Default and a discarding logger. It validates the configuration, registers the built-in health routes (GET /health, /health/live and /health/ready) and constructs the HTTP server without listening; Run starts it.

func (*App) Close

func (a *App) Close(ctx context.Context) error

Close runs the shutdown hooks once. Run calls it; call it directly only when the application is discarded without running.

func (*App) Config

func (a *App) Config() config.Config

Config returns the validated configuration the application was built with.

func (*App) Handle

func (a *App) Handle(pattern string, handler http.Handler) error

Handle registers handler for pattern, a net/http.ServeMux pattern such as "GET /posts/{id}". It returns an error for an invalid or conflicting pattern, or once the application has served its first request.

func (*App) HandleFunc

func (a *App) HandleFunc(pattern string, handler http.HandlerFunc) error

HandleFunc is Handle for a handler function.

func (*App) Handler

func (a *App) Handler() http.Handler

Handler returns the application's full HTTP handler, including the security headers and the middleware, without listening. Use it to test the application with httptest.

func (*App) Logger

func (a *App) Logger() *slog.Logger

Logger returns the application logger.

func (*App) OnShutdown

func (a *App) OnShutdown(hook ShutdownHook) error

OnShutdown registers a hook that runs after the HTTP server stops, in reverse registration order, like deferred calls.

func (*App) OpenAPI

func (a *App) OpenAPI() (*openapi.Document, error)

OpenAPI builds the OpenAPI 3.1 document of the routes registered so far whose handlers are described (openapi.Describe, openapi.Typed); the others are listed by Document.Undocumented. It works whether or not the document is served, which is how `forge openapi` and tests use it.

func (*App) Readiness

func (a *App) Readiness(name string, check health.Check) error

Readiness registers a named readiness check. GET /health/ready and GET /health run every check and answer 503 while any of them fails. Names must be unique.

func (*App) Resources

func (a *App) Resources(path string, handler any, middleware ...router.ResourceMiddleware) error

Resources registers the conventional resource routes for the actions handler implements; see router.Router.Resources.

func (*App) Routes

func (a *App) Routes() []router.Route

Routes returns the registered routes sorted by path, then method.

func (*App) Run

func (a *App) Run(ctx context.Context) error

Run serves HTTP until ctx is cancelled, then drains in-flight requests and runs the shutdown hooks. Draining and hooks share one budget, FORGE_HTTP_SHUTDOWN_TIMEOUT, counted from cancellation, so the whole shutdown fits inside an orchestrator grace period set slightly above it.

func (*App) Serving

func (a *App) Serving() bool

Serving reports whether this process composes the application to serve it. It is false while the "routes" and "openapi" commands run Configure only to describe the routes, and true otherwise ("serve", and an App built with New). Configure starts background work (a jobs.Scheduler, a queue.Worker, a recovery queue) only when it is true, so describing the application never runs or claims a job, and it can open its database pool lazily when it is false (postgres.Lazy(!app.Serving()), mysql.Lazy, sqlite.Lazy), so those commands need no reachable database.

func (*App) SetAPIInfo

func (a *App) SetAPIInfo(info openapi.Info) error

SetAPIInfo sets the title, version and description of the application's OpenAPI document; the title defaults to "Forge application" and the version to "0.0.0".

func (*App) Telemetry

func (a *App) Telemetry() *telemetry.Telemetry

Telemetry returns the application's telemetry, or nil when it is disabled. Pass it to the database adapters, queue workers, outbox dispatchers, schedulers and mail senders so they are instrumented; every Forge component accepts nil.

func (*App) Use

func (a *App) Use(middleware httpserver.Middleware) error

Use adds middleware that runs before routing, inside the middleware given to WithHTTPMiddleware, in the order Use is called (the first is outermost). Configure uses it to install request-wide behavior that must see the request before a route is chosen, such as web.MethodOverride, csrf.Protection.Handler and web.Flash. The chain is rebuilt on every call, so a middleware constructor may run more than once. A middleware whose handler is an httpserver.Rewriter (i18n.Translator.Middleware with PathPrefix) has its request rewrite applied before every middleware, including those given to WithHTTPMiddleware and those added with Use before it, so a path guard anywhere in the chain sees the path the router uses. After the first request has reached the application middleware, Use returns an error wrapping router.ErrFrozen.

type Command

type Command = command.Command

Command is an additional subcommand of the application binary, such as the migration commands contributed by the postgres package. It is command.Command: packages that contribute commands import the leaf package command, never this one (ADR 0052).

type CommandEnv

type CommandEnv = command.Env

CommandEnv is what a Command receives from the runtime. It is command.Env.

type Configure

type Configure func(ctx context.Context, app *App) error

Configure registers an application's routes, checks and dependencies on a freshly built App. It is the composition root between the framework runtime and application code. ctx is cancelled when the process is asked to stop, so slow startup work such as connecting to a database can be interrupted.

type Option

type Option func(*options) error

Option configures an App built by New.

func WithConfig

func WithConfig(cfg config.Config) Option

WithConfig sets the configuration, for example one from config.Load. New validates it.

func WithHTTPMiddleware

func WithHTTPMiddleware(middleware httpserver.Middleware) Option

WithHTTPMiddleware adds middleware to the application's HTTP handler chain. Request rewrites of middleware added here or with App.Use (see httpserver.Rewriter, such as the locale path prefix of i18n.Translator.Middleware) are applied before the first of these middleware runs, so they see the path the router uses. Options are applied in declaration order, outermost first among custom middleware. Forge's request ID and security headers remain outermost; body limits and panic recovery still protect the custom middleware chain.

func WithLogger

func WithLogger(logger *slog.Logger) Option

WithLogger sets the application logger; it must not be nil.

func WithTelemetry

func WithTelemetry(t *telemetry.Telemetry) Option

WithTelemetry makes the application use t instead of building one from the FORGE_TELEMETRY_* configuration; nil disables telemetry. The caller owns t and must shut it down. Without this option, New builds the Telemetry from configuration and shuts it down with the application.

type ShutdownHook

type ShutdownHook func(ctx context.Context) error

ShutdownHook releases a resource, such as a database pool, after the HTTP server has drained. ctx is bounded by the configured shutdown timeout.

type ViewRenderer

type ViewRenderer = htmlviews.Renderer

ViewRenderer executes server-side HTML templates with contextual escaping. It is an alias for views.Renderer, exposed here for generated apps that vendor the main Forge package.

func NewViewRenderer

func NewViewRenderer(templates fs.FS) (*ViewRenderer, error)

NewViewRenderer parses *.gohtml templates from templates.

Directories

Path Synopsis
Package agent provides a provider-neutral, fail-closed execution boundary for actions selected by an AI agent.
Package agent provides a provider-neutral, fail-closed execution boundary for actions selected by an AI agent.
postgres
Package postgres persists agent execution attempts and their outcomes.
Package postgres persists agent execution attempts and their outcomes.
Package assets serves an application's embedded static files (stylesheets, scripts, images, fonts) under content-digested, immutable URLs, like the Rails asset pipeline without a build step: app.css is served as /assets/app-<digest>.css, so the response can be cached forever and a changed file gets a new URL.
Package assets serves an application's embedded static files (stylesheets, scripts, images, fonts) under content-digested, immutable URLs, like the Rails asset pipeline without a build step: app.css is served as /assets/app-<digest>.css, so the response can be cached forever and a changed file gets a new URL.
Package auth is Forge's authentication and authorization domain: opaque session tokens, principals and permissions, password hashing, sign-in, two-factor authentication, password recovery, throttling and the security audit, with the ports identity stores implement.
Package auth is Forge's authentication and authorization domain: opaque session tokens, principals and permissions, password hashing, sign-in, two-factor authentication, password recovery, throttling and the security audit, with the ports identity stores implement.
authhttp
Package authhttp is the HTTP adapter of package auth: opaque session authentication over explicit bearer or secure-cookie transports (Middleware), deny-by-default permission checks (Require, RequireAudited), CSRF protection for cookie sessions (RequireCSRF), the JSON sign-in, sign-out, two-factor and password-recovery handlers with their OpenAPI operations, and Browser, the security-critical half of server-rendered authentication forms.
Package authhttp is the HTTP adapter of package auth: opaque session authentication over explicit bearer or secure-cookie transports (Middleware), deny-by-default permission checks (Require, RequireAudited), CSRF protection for cookie sessions (RequireCSRF), the JSON sign-in, sign-out, two-factor and password-recovery handlers with their OpenAPI operations, and Browser, the security-critical half of server-rendered authentication forms.
authsetup
Package authsetup is the command line of the "setup" subcommand every Forge identity store ships (auth/postgres, auth/sqlite, auth/mysql): its flags, how it reads the owner's password and what it prints.
Package authsetup is the command line of the "setup" subcommand every Forge identity store ships (auth/postgres, auth/sqlite, auth/mysql): its flags, how it reads the owner's password and what it prints.
authsql
Package authsql is the database/sql identity store that auth/sqlite and auth/mysql share: the same queries as auth/postgres, written once for the SQL both databases accept, with the few constructs they spell differently (the clock, locking reads, upserts, the setup lock) supplied by a Dialect.
Package authsql is the database/sql identity store that auth/sqlite and auth/mysql share: the same queries as auth/postgres, written once for the SQL both databases accept, with the few constructs they spell differently (the clock, locking reads, upserts, the setup lock) supplied by a Dialect.
authtest
Package authtest is the contract suite every Forge identity store must pass: auth/postgres, auth/sqlite and auth/mysql run it against a real database (decision D30).
Package authtest is the contract suite every Forge identity store must pass: auth/postgres, auth/sqlite and auth/mysql run it against a real database (decision D30).
internal/backupcode
Package backupcode is the format of two-factor backup codes: 15 random bytes, written in unpadded base32 in groups of four, stored only as a domain-separated SHA-256 digest.
Package backupcode is the format of two-factor backup codes: 15 random bytes, written in unpadded base32 in groups of four, stored only as a domain-separated SHA-256 digest.
mysql
Package mysql provides MySQL and MariaDB persistence for Forge authentication and identity (decision D30): the same tables, rules and results as auth/postgres, over the forge-go/mysql adapter.
Package mysql provides MySQL and MariaDB persistence for Forge authentication and identity (decision D30): the same tables, rules and results as auth/postgres, over the forge-go/mysql adapter.
postgres
Package postgres provides PostgreSQL persistence for Forge authentication and identity.
Package postgres provides PostgreSQL persistence for Forge authentication and identity.
recoverymail
Package recoverymail delivers password-recovery emails: an adapter that implements auth.RecoverySender on top of mail.Sender.
Package recoverymail delivers password-recovery emails: an adapter that implements auth.RecoverySender on top of mail.Sender.
recoveryqueue
Package recoveryqueue moves password-recovery requests onto Forge's persistent job queue (package queue): an adapter between auth's RecoveryDispatcher and queue's Registry, so the request page answers without looking an account up, issuing a token or sending mail, whatever the mail server's latency, and a failed delivery is retried with backoff.
Package recoveryqueue moves password-recovery requests onto Forge's persistent job queue (package queue): an adapter between auth's RecoveryDispatcher and queue's Registry, so the request page answers without looking an account up, issuing a token or sending mail, whatever the mail server's latency, and a failed delivery is retried with backoff.
sqlite
Package sqlite provides SQLite persistence for Forge authentication and identity (decision D30): the same tables, rules and results as auth/postgres, over the forge-go/sqlite adapter.
Package sqlite provides SQLite persistence for Forge authentication and identity (decision D30): the same tables, rules and results as auth/postgres, over the forge-go/sqlite adapter.
Package broadcast pushes Turbo Streams to browsers in real time over Server-Sent Events: Rails' Turbo::StreamsChannel / Action Cable for the common case, without WebSockets.
Package broadcast pushes Turbo Streams to browsers in real time over Server-Sent Events: Rails' Turbo::StreamsChannel / Action Cable for the common case, without WebSockets.
postgres
Package postgres is the PostgreSQL adapter of package broadcast: it fans messages out across application replicas with LISTEN/NOTIFY, so a change made through one replica reaches browsers connected to another.
Package postgres is the PostgreSQL adapter of package broadcast: it fans messages out across application replicas with LISTEN/NOTIFY, so a change made through one replica reaches browsers connected to another.
Package cache is Forge's application cache, the equivalent of Rails' Solid Cache (ADR 0032): byte values under string keys with a time to live, a bounded in-memory implementation, and database-backed stores (cache/postgres, cache/mysql, cache/sqlite) that every replica shares.
Package cache is Forge's application cache, the equivalent of Rails' Solid Cache (ADR 0032): byte values under string keys with a time to live, a bounded in-memory implementation, and database-backed stores (cache/postgres, cache/mysql, cache/sqlite) that every replica shares.
cachetest
Package cachetest is the conformance suite every cache.Cache must pass.
Package cachetest is the conformance suite every cache.Cache must pass.
mysql
Package mysql is the MySQL and MariaDB adapter of Forge's shared cache (ADR 0032): entries live in a table addressed by the SHA-256 of the cache key, all expiry arithmetic uses the database clock (UTC_TIMESTAMP), and pruning removes expired entries and, above MaxEntries, the oldest writes, in bounded batches.
Package mysql is the MySQL and MariaDB adapter of Forge's shared cache (ADR 0032): entries live in a table addressed by the SHA-256 of the cache key, all expiry arithmetic uses the database clock (UTC_TIMESTAMP), and pruning removes expired entries and, above MaxEntries, the oldest writes, in bounded batches.
postgres
Package postgres is the PostgreSQL adapter of Forge's shared cache (ADR 0032), the equivalent of Solid Cache: entries live in a table keyed by the SHA-256 of the cache key, all expiry arithmetic uses the database clock, and pruning removes expired entries and, above MaxEntries, the oldest writes, in bounded batches.
Package postgres is the PostgreSQL adapter of Forge's shared cache (ADR 0032), the equivalent of Solid Cache: entries live in a table keyed by the SHA-256 of the cache key, all expiry arithmetic uses the database clock, and pruning removes expired entries and, above MaxEntries, the oldest writes, in bounded batches.
sqlite
Package sqlite is the SQLite adapter of Forge's shared cache (ADR 0032).
Package sqlite is the SQLite adapter of Forge's shared cache (ADR 0032).
cmd
forge command
forge-proxy command
Command forge-proxy is the reverse proxy that `forge deploy` runs on each web host.
Command forge-proxy is the reverse proxy that `forge deploy` runs on each web host.
Package command holds the contract between an application binary's runtime and the extra subcommands packages contribute to it, such as the migration commands of postgres, mysql and sqlite or the "jobs" command of the queue adapters (ADR 0052).
Package command holds the contract between an application binary's runtime and the extra subcommands packages contribute to it, such as the migration commands of postgres, mysql and sqlite or the "jobs" command of the queue adapters (ADR 0052).
Package config loads and validates Forge runtime configuration.
Package config loads and validates Forge runtime configuration.
Package credentials reads Forge's encrypted per-environment credentials, the equivalent of Rails' credentials:edit.
Package credentials reads Forge's encrypted per-environment credentials, the equivalent of Rails' credentials:edit.
Package csrf protects browser requests against cross-site request forgery with two independent checks: a double-submit token bound to the __Host-forge_csrf cookie, and cross-origin rejection based on the browser's Sec-Fetch-Site and Origin headers (net/http.CrossOriginProtection).
Package csrf protects browser requests against cross-site request forgery with two independent checks: a double-submit token bound to the __Host-forge_csrf cookie, and cross-origin rejection based on the browser's Sec-Fetch-Site and Origin headers (net/http.CrossOriginProtection).
Package dbtest is the adapter-independent part of Forge's database test helpers (postgrestest, mysqltest, sqlitetest): finding the test server, naming throwaway databases and applying migrations.
Package dbtest is the adapter-independent part of Forge's database test helpers (postgrestest, mysqltest, sqlitetest): finding the test server, naming throwaway databases and applying migrations.
Package encryption provides authenticated attribute encryption with key rotation, built only from the standard library (AES-256-GCM, HKDF-SHA256 and HMAC-SHA256).
Package encryption provides authenticated attribute encryption with key rotation, built only from the standard library (AES-256-GCM, HKDF-SHA256 and HMAC-SHA256).
Package events defines the portable envelope used for application events.
Package events defines the portable envelope used for application events.
examples
basic-api command
Command basic-api is the reference Forge service: a small task API backed by PostgreSQL and laid out as ports and adapters (see docs/adr/0004).
Command basic-api is the reference Forge service: a small task API backed by PostgreSQL and laid out as ports and adapters (see docs/adr/0004).
basic-api/app/adapters/httpapi
Package httpapi is the HTTP adapter for the task use cases.
Package httpapi is the HTTP adapter for the task use cases.
basic-api/app/adapters/persistence
Package persistence implements the application's storage ports on PostgreSQL with explicit SQL.
Package persistence implements the application's storage ports on PostgreSQL with explicit SQL.
basic-api/app/application
Package application implements the task use cases on top of the domain and the ports (interfaces) that adapters implement.
Package application implements the task use cases on top of the domain and the ports (interfaces) that adapters implement.
basic-api/app/bootstrap
Package bootstrap is the composition root: it acquires infrastructure and wires repositories, use cases and handlers into a forge.App.
Package bootstrap is the composition root: it acquires infrastructure and wires repositories, use cases and handlers into a forge.App.
basic-api/app/domain
Package domain holds the task entity and its rules.
Package domain holds the task entity and its rules.
basic-api/db
Package db holds the service's SQL migrations, embedded in the binary so the production image can migrate its own database ("basic-api migrate").
Package db holds the service's SQL migrations, embedded in the binary so the production image can migrate its own database ("basic-api migrate").
Package fault defines stable application errors without exposing internal causes.
Package fault defines stable application errors without exposing internal causes.
Package health provides liveness and readiness checks.
Package health provides liveness and readiness checks.
Package hotwire embeds Turbo and Stimulus, the JavaScript half of Forge's Rails-style front end, so an application needs no Node toolchain and no CDN.
Package hotwire embeds Turbo and Stimulus, the JavaScript half of Forge's Rails-style front end, so an application needs no Node toolchain and no CDN.
Package httpserver owns Forge's secure HTTP server defaults and lifecycle.
Package httpserver owns Forge's secure HTTP server defaults and lifecycle.
Package i18n translates an application's user-visible text, like Rails I18n: translations live in JSON files (config/locales/LOCALE.json), keys are dotted paths ("users.welcome"), values interpolate named arguments ("Welcome, %{name}!") and choose CLDR plural forms by count.
Package i18n translates an application's user-visible text, like Rails I18n: translations live in JSON files (config/locales/LOCALE.json), keys are dotted paths ("users.welcome"), values interpolate named arguments ("Welcome, %{name}!") and choose CLDR plural forms by count.
internal
cli
dbtasks
Package dbtasks implements the database lifecycle behind `forge db`: creating and dropping the application's database, dumping and loading its schema, and opening the database's own console (ADR 0035).
Package dbtasks implements the database lifecycle behind `forge db`: creating and dropping the application's database, dumping and loading its schema, and opening the database's own console (ADR 0035).
deploy
Package deploy implements `forge deploy`, `forge logs` and `forge app`: container deployment to hosts over SSH with zero-downtime switches through forge-proxy (ADR 0030).
Package deploy implements `forge deploy`, `forge logs` and `forge app`: container deployment to hosts over SSH with zero-downtime switches through forge-proxy (ADR 0030).
deploy/internal/fakedocker command
Command fakedocker stands in for the docker CLI in the SSH integration tests of internal/deploy, where no Docker daemon is available.
Command fakedocker stands in for the docker CLI in the SSH integration tests of internal/deploy, where no Docker daemon is available.
devloop
Package devloop is the engine behind `forge dev`: a polling file watcher, a supervisor that rebuilds and gracefully restarts the application, and a development-only reverse proxy that reloads the browser.
Package devloop is the engine behind `forge dev`: a polling file watcher, a supervisor that rebuilds and gracefully restarts the application, and a development-only reverse proxy that reloads the browser.
exectest
Package exectest creates the executable stand-ins (fake ssh, fake database clients, hook scripts) that tests run as separate processes.
Package exectest creates the executable stand-ins (fake ssh, fake database clients, hook scripts) that tests run as separate processes.
frontend
Package frontend compiles a generated application's TypeScript (app/javascript/**/*.ts) to the JavaScript the browser runs, with esbuild's Go API (ADR 0049).
Package frontend compiles a generated application's TypeScript (app/javascript/**/*.ts) to the JavaScript the browser runs, with esbuild's Go API (ADR 0049).
langtag
Package langtag validates and canonicalizes the subset of BCP 47 locale names Forge supports.
Package langtag validates and canonicalizes the subset of BCP 47 locale names Forge supports.
loadgen command
Command loadgen is the HTTP load generator of scripts/bench.sh: a closed loop of a fixed number of workers that send the requests of the benchmark scenarios (docs/BENCHMARKS.md) to a running generated application for a fixed time, and report requests per second and latency percentiles.
Command loadgen is the HTTP load generator of scripts/bench.sh: a closed loop of a fixed number of workers that send the requests of the benchmark scenarios (docs/BENCHMARKS.md) to a running generated application for a fixed time, and report requests per second and latency percentiles.
principalctx
Package principalctx reads the authenticated principal from a request context.
Package principalctx reads the authenticated principal from a request context.
principalctx/internal/key
Package key is the request-context key of the authenticated principal.
Package key is the request-context key of the authenticated principal.
principalctx/principalwrite
Package principalwrite puts the authenticated principal in a request context, where principalctx.Value and auth.FromContext read it.
Package principalwrite puts the authenticated principal in a request context, where principalctx.Value and auth.FromContext read it.
proxy
Package proxy implements forge-proxy, the reverse proxy that `forge deploy` runs on every web host (ADR 0030).
Package proxy implements forge-proxy, the reverse proxy that `forge deploy` runs on every web host (ADR 0030).
ptytest
Package ptytest opens pseudo-terminals for tests that drive an interactive terminal: secret prompts, colored logs.
Package ptytest opens pseudo-terminals for tests that drive an interactive terminal: secret prompts, colored logs.
scaffold
Package scaffold generates new Forge applications.
Package scaffold generates new Forge applications.
terminal
Package terminal reads a secret typed at an interactive terminal without echoing it.
Package terminal reads a secret typed at an interactive terminal without echoing it.
version
Package version holds the build information of the forge and forge-proxy commands and renders it for `forge version` and `forge-proxy version`.
Package version holds the build information of the forge and forge-proxy commands and renders it for `forge version` and `forge-proxy version`.
waittest
Package waittest waits for a condition that another goroutine, process or server makes true: the one place the tests poll the real clock.
Package waittest waits for a condition that another goroutine, process or server makes true: the one place the tests poll the real clock.
Package jobs provides an in-process scheduler for recurring background work.
Package jobs provides an in-process scheduler for recurring background work.
mysql
Package mysql records the ticks of package jobs' scheduler in MySQL or MariaDB, so that each tick of a scheduled job runs on exactly one replica (ADR 0047).
Package mysql records the ticks of package jobs' scheduler in MySQL or MariaDB, so that each tick of a scheduled job runs on exactly one replica (ADR 0047).
postgres
Package postgres records the ticks of package jobs' scheduler in PostgreSQL, so that each tick of a scheduled job runs on exactly one replica (ADR 0047).
Package postgres records the ticks of package jobs' scheduler in PostgreSQL, so that each tick of a scheduled job runs on exactly one replica (ADR 0047).
ticktest
Package ticktest is the conformance suite every jobs.TickClaimer must pass.
Package ticktest is the conformance suite every jobs.TickClaimer must pass.
Package mail builds and delivers email.
Package mail builds and delivers email.
mailtest
Package mailtest provides an in-memory mail.Sender for application tests, the equivalent of ActionMailer::Base.deliveries.
Package mailtest provides an in-memory mail.Sender for application tests, the equivalent of ActionMailer::Base.deliveries.
Package migrate is Forge's database-independent migration engine: SQL files embedded in the application binary, applied in version order under a cross-process lock, recorded with checksums, and refused whenever the database and the build disagree (ADR 0008).
Package migrate is Forge's database-independent migration engine: SQL files embedded in the application binary, applied in version order under a cross-process lock, recorded with checksums, and refused whenever the database and the build disagree (ADR 0008).
migratetest
Package migratetest is the conformance suite for the migration engine's framework-migration ownership rules (ADR 0039), run by every database adapter against a real database:
Package migratetest is the conformance suite for the migration engine's framework-migration ownership rules (ADR 0039), run by every database adapter against a real database:
Package mysql is Forge's MySQL and MariaDB adapter: a database/sql pool configured for safe defaults, error translation into fault errors and SQL-file migrations.
Package mysql is Forge's MySQL and MariaDB adapter: a database/sql pool configured for safe defaults, error translation into fault errors and SQL-file migrations.
internal/integration
Package integration holds tests of core packages (encryption, migrate, sqldb, the root forge package, auth and its helpers) that need the MySQL and MariaDB driver.
Package integration holds tests of core packages (encryption, migrate, sqldb, the root forge package, auth and its helpers) that need the MySQL and MariaDB driver.
mysqltest
Package mysqltest gives each test its own empty MySQL or MariaDB database.
Package mysqltest gives each test its own empty MySQL or MariaDB database.
Package openapi describes Forge JSON routes as an OpenAPI 3.1 document (ADR 0043).
Package openapi describes Forge JSON routes as an OpenAPI 3.1 document (ADR 0043).
outbox
postgres
Package postgres provides PostgreSQL persistence for the transactional outbox.
Package postgres provides PostgreSQL persistence for the transactional outbox.
Package pagination implements keyset (cursor) pagination over UUIDv7 identifiers.
Package pagination implements keyset (cursor) pagination over UUIDv7 identifiers.
Package postgres is Forge's PostgreSQL adapter: a configured pgx connection pool, explicit transactions, error translation into fault errors, advisory locks and SQL-file migrations.
Package postgres is Forge's PostgreSQL adapter: a configured pgx connection pool, explicit transactions, error translation into fault errors, advisory locks and SQL-file migrations.
internal/argon2cost
Package argon2cost tests, with the real golang.org/x/crypto/argon2, that auth refuses an Argon2id implementation that changes Forge's default cost (ADR 0053).
Package argon2cost tests, with the real golang.org/x/crypto/argon2, that auth refuses an Argon2id implementation that changes Forge's default cost (ADR 0053).
internal/integration
Package integration holds tests of core packages (encryption, migrate, sqldb, the root forge package, auth and its helpers) that need the PostgreSQL driver.
Package integration holds tests of core packages (encryption, migrate, sqldb, the root forge package, auth and its helpers) that need the PostgreSQL driver.
internal/pgurl
Package pgurl makes a PostgreSQL URL independent of the process environment before pgx parses it.
Package pgurl makes a PostgreSQL URL independent of the process environment before pgx parses it.
postgrestest
Package postgrestest gives each test its own empty PostgreSQL database.
Package postgrestest gives each test its own empty PostgreSQL database.
Package queue is Forge's persistent background job queue: typed JSON payloads, transactional enqueue, leases with token-fenced acknowledgements, retries with exponential backoff and jitter, dead jobs that operators can retry or discard, idempotency keys and graceful shutdown (ADR 0023).
Package queue is Forge's persistent background job queue: typed JSON payloads, transactional enqueue, leases with token-fenced acknowledgements, retries with exponential backoff and jitter, dead jobs that operators can retry or discard, idempotency keys and graceful shutdown (ADR 0023).
mysql
Package mysql is the MySQL and MariaDB adapter of Forge's job queue (ADR 0023).
Package mysql is the MySQL and MariaDB adapter of Forge's job queue (ADR 0023).
postgres
Package postgres is the PostgreSQL adapter of Forge's job queue (ADR 0023).
Package postgres is the PostgreSQL adapter of Forge's job queue (ADR 0023).
queuecmd
Package queuecmd provides the "jobs" subcommand of an application binary for any queue.Admin.
Package queuecmd provides the "jobs" subcommand of an application binary for any queue.Admin.
sqlite
Package sqlite is the SQLite adapter of Forge's job queue (ADR 0023).
Package sqlite is the SQLite adapter of Forge's job queue (ADR 0023).
storetest
Package storetest is the conformance suite every queue.Store must pass.
Package storetest is the conformance suite every queue.Store must pass.
Package ratelimit limits how often something may happen per key: Rails' rate_limit, with fail-closed defaults (ADR 0031).
Package ratelimit limits how often something may happen per key: Rails' rate_limit, with fail-closed defaults (ADR 0031).
limitertest
Package limitertest is the conformance suite every ratelimit.Limiter must pass.
Package limitertest is the conformance suite every ratelimit.Limiter must pass.
mysql
Package mysql is the MySQL and MariaDB adapter of Forge's shared rate limiter (ADR 0031).
Package mysql is the MySQL and MariaDB adapter of Forge's shared rate limiter (ADR 0031).
postgres
Package postgres is the PostgreSQL adapter of Forge's shared rate limiter (ADR 0031): one atomic INSERT ...
Package postgres is the PostgreSQL adapter of Forge's shared rate limiter (ADR 0031): one atomic INSERT ...
sqlite
Package sqlite is the SQLite adapter of Forge's shared rate limiter (ADR 0031).
Package sqlite is the SQLite adapter of Forge's shared rate limiter (ADR 0031).
Package router provides Forge's route registry on top of net/http semantics.
Package router provides Forge's route registry on top of net/http semantics.
Package sqldb holds what Forge's database adapters share: one translation of database errors into fault errors, transaction retries, and a database/sql wrapper used by the MySQL and SQLite adapters.
Package sqldb holds what Forge's database adapters share: one translation of database errors into fault errors, transaction retries, and a database/sql wrapper used by the MySQL and SQLite adapters.
Package sqlite is Forge's SQLite adapter: a database/sql pool on the pure-Go modernc.org/sqlite driver (so binaries keep CGO_ENABLED=0 and the static distroless image), error translation into fault errors and SQL-file migrations.
Package sqlite is Forge's SQLite adapter: a database/sql pool on the pure-Go modernc.org/sqlite driver (so binaries keep CGO_ENABLED=0 and the static distroless image), error translation into fault errors and SQL-file migrations.
internal/integration
Package integration holds tests of core packages (encryption, migrate, sqldb, the root forge package, auth and its helpers) that need the SQLite driver.
Package integration holds tests of core packages (encryption, migrate, sqldb, the root forge package, auth and its helpers) that need the SQLite driver.
sqlitetest
Package sqlitetest gives each test its own empty SQLite database: a fresh file in the test's temporary directory, opened with the same settings as production (foreign keys, WAL, busy timeout, immediate transactions).
Package sqlitetest gives each test its own empty SQLite database: a fresh file in the test's temporary directory, opened with the same settings as production (foreign keys, WAL, busy timeout, immediate transactions).
Package storage is Forge's file storage, the equivalent of Rails' Active Storage (ADR 0033): a Storage port, an adapter for local disk (storage/disk), HMAC-signed time-limited links, an HTTP handler that serves them with safe headers, and a strict multipart upload helper.
Package storage is Forge's file storage, the equivalent of Rails' Active Storage (ADR 0033): a Storage port, an adapter for local disk (storage/disk), HMAC-signed time-limited links, an HTTP handler that serves them with safe headers, and a strict multipart upload helper.
disk
Package disk is the local-disk adapter of Forge's file storage (ADR 0033).
Package disk is the local-disk adapter of Forge's file storage (ADR 0033).
storagetest
Package storagetest is the conformance suite every storage.Storage must pass.
Package storagetest is the conformance suite every storage.Storage must pass.
Package telemetry is Forge's dependency-free observability core: W3C Trace Context propagation, a minimal tracer with parent-based ratio sampling, a bounded span batcher, metric instruments with a cardinality cap, a Prometheus text handler, an OTLP/HTTP JSON exporter and a log handler that correlates log records with traces (ADR 0034, docs/OBSERVABILITY.md).
Package telemetry is Forge's dependency-free observability core: W3C Trace Context propagation, a minimal tracer with parent-based ratio sampling, a bounded span batcher, metric instruments with a cardinality cap, a Prometheus text handler, an OTLP/HTTP JSON exporter and a log handler that correlates log records with traces (ADR 0034, docs/OBSERVABILITY.md).
telemetrytest
Package telemetrytest records spans and reads metrics in tests of instrumented code.
Package telemetrytest records spans and reads metrics in tests of instrumented code.
Package tenancy carries the current tenant through request and database boundaries.
Package tenancy carries the current tenant through request and database boundaries.
Package turbo builds Turbo Streams: <turbo-stream> elements that tell the browser to append, prepend, replace, update, remove or insert beside an element identified by its DOM id.
Package turbo builds Turbo Streams: <turbo-stream> elements that tell the browser to append, prepend, replace, update, remove or insert beside an element identified by its DOM id.
Package uuid provides Forge's default UUIDv7 identifier.
Package uuid provides Forge's default UUIDv7 identifier.
Package views renders server-side HTML templates with contextual escaping.
Package views renders server-side HTML templates with contextual escaping.
Package web provides the HTTP adapter toolkit for Forge handlers: JSON responses, a single error response format, strict request decoding and the request-scoped values (request ID, logger) set by the HTTP server.
Package web provides the HTTP adapter toolkit for Forge handlers: JSON responses, a single error response format, strict request decoding and the request-scoped values (request ID, logger) set by the HTTP server.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL