Documentation previewThese docs are actively being built. Some pages may change as the framework and examples are finalized.
Skip to content

The composable stack for building with Go

One cohesive runtime. Explicit dependency wiring. Local-first drivers. Production-ready primitives for everything an application needs.

Driver swappableChange infrastructure without rewriting application code.
Explicit runtimeGenerated wiring and lifecycle behavior stay inspectable.
Local firstStart locally, then move to production topology when needed.

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 a Vue frontend
  • Components are choices. Auth, mail, database, metrics, Docker, frontend - picked at forj new, added 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 every primitive, so day one needs no cloud account and no docker-compose archaeology.

Infrastructure

Swap drivers, not business logic

Services depend on contracts. Configuration selects the backend. When infrastructure changes, your code does not.

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.

Every primitive works this way. Cache, storage, queue, events, database, mail - each runs on in-process or local drivers in a standalone binary, then swaps to real infrastructure in production. No code changes.

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. Deployment is a file you copy. Your code never knows the difference.

Standalone

$ forj app  # → ./bin/app run
one process: http + jobs + scheduler

Distributed

$ forj api  # → ./bin/app api
$ forj worker --queue media
$ forj scheduler

Route lists

forj route:list is the source of truth for the HTTP surface, not a scroll through startup logs.

Health and readiness

/-/health and /-/ready ship generated, 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 live their whole life as a single App - and that is the golden path. When a Project outgrows it, one command adds another runnable app in the same repo: shared code, separate wiring, separate binaries, separate scaling.

  • Apps are boundaries, not microservices. Named apps 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 billing without touching the rest.
  • Nothing changes until you need it. A single-App Project never pays for this. Multi-app is a fan-out path for larger systems, teams, and monorepos - not a new architecture to learn on day one.
one project · many apps
$ forj make:app billing
$ forj billing route:list
$ forj dev  # orchestrates app + billing

photodrop/
├── cmd/app/         # default app
├── cmd/billing/     # named app
├── app/billing/     # its routes, commands, wiring
└── internal/        # shared behavior, one module

$ forj api             # → ./bin/app api
$ forj billing worker   # → ./bin/billing worker

Tested foundation

Primitives that prove themselves

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

2,100+test functions across the first-party libraries
870+integration test runs against real backends in containers
49interchangeable drivers across queue, events, cache, storage, database, and mail
16standalone 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 - the tutorial cannot drift from 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 rendered 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 got tired of rebuilding the same foundation every time I started a Go service. I did not want magic, and I did not want a framework that fights the language. So GoForj is built from things that still feel like Go: explicit wiring, compiled binaries, small interfaces, readable control flow. It is the stack I always wanted.

Chris MilesCreator of GoForj