Skip to content

The composable stack for building with Go

One cohesive application model. Explicit dependency wiring. Local-first drivers. Production-ready primitives across the application stack.

App-owned compositionGenerated code keeps application boundaries and extension points explicit.
Explicit RuntimesRun together locally, then split into production processes.
Swap drivers, not business logicMove between local and production infrastructure behind the same APIs.
The GoForj application forgeOne GoForj App composes frontend choices, libraries, infrastructure drivers, Atlas coding-agent support, and explicit Runtimes into Go source you own.BACKENDFRONTENDCORE LIBRARIESQUEUEEVENTSATLASSTORAGEDATABASECACHE

Start

A real application in two commands

forj new renders a complete Go project - the components you choose, nothing more. forj dev brings it alive. Built for Go developers shipping services, workers, CLIs, and full products.

A focused CLIAn API serviceWorkers and schedulesA full product with auth and Vue, React, or templ
  • Choose what you need. Add database access, cache, events, file storage, background jobs, auth, a frontend, and observability at forj new or later as the app grows.
  • The structure is already there. Routes, wiring, lifecycle, configuration, and tests have a place before you write a line.
  • It runs before you configure anything. Local drivers back the database, cache, queue, events, storage, and mail integrations, so day one needs no cloud account and no docker-compose archaeology.
Agent-aware from the first renderforj new can install Atlas support for Codex, Claude Code, GitHub Copilot, and Gemini CLI, including project guidance, skills, and MCP context.

Infrastructure

Swap drivers, not business logic

Services depend on stable contracts. Configuration selects among the drivers compiled into the app, so a supported backend change leaves business logic alone.

Your service · the same file in every environment

go
// internal/photos/service.go
type Service struct {
	disk storage.Storage
}

func NewService(disk storage.Storage) *Service {
	return &Service{disk: disk}
}

func (s *Service) Store(ctx context.Context, in UploadInput) (Photo, error) {
	path := photoPath(in)
	if err := s.disk.WithContext(ctx).Put(path, in.Body); err != nil {
		return Photo{}, fmt.Errorf("store photo: %w", err)
	}
	return Photo{Path: path}, nil
}

Your environment · the only thing that changes

STORAGE_PHOTOS_DRIVER=local
DB_DRIVER=sqlite
CACHE_DRIVER=memory
QUEUE_DRIVER=workerpool
EVENTS_DRIVER=inproc
MAIL_DRIVER=log
0lines of Go changed
$ forj buildDriver support is compiled in, selection happens at runtime, and misconfiguration fails fast instead of failing quietly.

The same rule applies across the app. Cache, storage, queues, events, databases, and mail run on in-process or local drivers, then move to production infrastructure without changing service code.

Generators

Generated code you own

Make commands create the file and the wiring: providers, routes, schedules, subscriptions. No annotations, no reflection container, no hidden registration.

  • Organized by package, not by file type. A feature's HTTP, CLI, queue, scheduler, and event entry points live beside the service that owns the work.
  • Reversible. --remove deletes the file and undoes the wiring the generator manages. --dry-run shows you first.
  • Readable output. Generated wiring is ordinary Go you can read, debug, and step through. If it would be embarrassing to look at, it does not ship.
one feature · four entry points
$ forj make:controller photos
$ forj make:job photos:thumbnail --queue media
$ forj make:schedule photos:digest --every 24h
$ forj make:subscriber photos:photo-uploaded

internal/photos/
├── controller.go                 # HTTP entry point
├── thumbnail_job.go              # queue entry point
├── digest_schedule.go            # scheduler entry point
├── photo_uploaded_subscriber.go  # event entry point
└── service.go                    # your workflow code

Operations

Run it your way. See everything it does

One binary hosts everything locally, or splits into explicit processes when production needs to scale. Build one artifact, then run the entry point your environment needs.

Your entire app is one file

Whatever you choose to include, forj build compiles it into one static binary, with nothing extra to install beside it.

Ships inside

HTTP serverQueue workersSchedulerCLI commandsMigrationsDriversHealth and metricsLighthouse UI
bin/app · everything
$ forj new  # components · all of them
$ forj build
$ ls -lh bin/app
-rwxr-xr-x  1 you  staff    bin/app

$ ./bin/app
23:51:32.256 Scheduler  Scheduler started
23:51:32.256 Jobs       Queue worker started → workers=30
23:51:32.257 HTTP       Listening → addr=0.0.0.0:3000

Standalone

$ ./bin/app
one process: http + jobs + scheduler

Distributed

$ ./bin/app api
$ ./bin/app worker --queue media
$ ./bin/app scheduler

Route lists

forj route:list lists every registered HTTP route, so startup logs do not have to serve as route documentation.

Health and readiness

/-/health and /-/ready are included, with token-gated structured diagnostics.

Metrics

Prometheus-compatible series with bounded labels: route patterns, queue names, job names, schedule names.

Inspects and Lighthouse

Execution records for every request, job, schedule run, and command, browsable in a first-party operator UI.

Scale

Start with one app. Grow into many

Most products stay in one app. When a Project needs another deployment or scaling boundary, one command adds another runnable app in the same repo: shared code, separate wiring, and separate binaries.

  • Apps are runnable boundaries, not automatic microservices. Apps in a multi-app Project share one repo, one Go module, and everything under internal/. No RPC ceremony, no duplicated plumbing.
  • Each app deploys on its own terms. Its own binary, ports, wiring, and runtime identity in logs, metrics, and Lighthouse - scale admin without touching the rest.
  • Nothing changes until you need it. A single-app Project never pays for this. Add another app only when larger systems, teams, or monorepos need separate runnable boundaries.
one project · many apps
$ forj make:app admin
$ forj admin make:controller users
$ forj admin route:list
$ forj dev  # manages apps listed in dev.apps

photodrop/
├── cmd/app/         # default app
├── cmd/admin/ # additional app binary
├── app/admin/ # routes, commands, wiring
└── internal/        # shared behavior, one module

$ forj api
$ forj admin worker

Tested foundation

Drivers tested against real backends

A driver should not only compile - it should prove its behavior against the backend it claims to support.

3,400+test functions across the first-party libraries
950+integration test runs against real backends in containers
48interchangeable drivers across queue, events, cache, storage, database, and mail
18standalone libraries, each useful without the framework

Driver suites run against Redis, Postgres, MySQL, NATS, Kafka, MinIO, SQS, and more through testcontainers and emulators. These numbers are generated from the repositories, not written by hand: see how they are counted →

Verified scenarios

Learn it by building it

Seven scenarios grow one small app from a single route to a fully observable system. Each ships only after it executes against the current templates, keeping the tutorial aligned with the framework.

  1. 1JSON API route
  2. 2Cached profile
  3. 3File upload
  4. 4users.created event
  5. 5reports:generate job
  6. 6reports:daily schedule
  7. 7Runtime observability

Fit

Is GoForj for you?

A framework should say who it serves and who it does not. Here is the honest version.

Reach for GoForj when

  • You are building services, APIs, workers, schedulers, CLIs, or full products in Go.
  • You want the foundation every service repeats, wiring, queues, cache, auth, observability, built and tested before day one.
  • You want infrastructure to be a configuration decision instead of an architecture rewrite.

Reach for something else when

  • You want a thin router and nothing more. A minimal mux and hand-picked libraries will be lighter.
  • Your team rules out code generation. GoForj's model is rendered code you own, and that is not negotiable.
  • You are building a library, not an application. Use the standalone libraries instead.

If you outgrow it, you keep everything

A GoForj app is ordinary Go: explicit wiring, readable files, standard modules. Stop running forj tomorrow and your application still builds, tests, and deploys. The framework earns its place in your workflow, not in your lock-in.

I love building in Go. I love how direct it feels, how simple it is to ship, and how long production services can stay understandable. But I got tired of rebuilding the same application foundation every time: commands, queues, schedules, cache, storage, mail, metrics, wiring, local dev. GoForj is the stack I wanted for complete Go applications: cohesive, explicit, compiled, and still recognizably Go.

Chris MilesCreator of GoForj