Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/core/app-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ flowchart LR
run --> stop["BeforeShutdown → Shutdown → AfterShutdown"]
```

The generated `App.Run` starts the lifecycle before executing a parsed command and defers shutdown with the App shutdown timeout. Startup phases run in registration order. Shutdown phases run in reverse registration order, so dependent resources can stop before what they rely on.
`App.Run` starts the lifecycle before executing a parsed command and defers shutdown with the App shutdown timeout. Startup phases run in registration order. Shutdown phases run in reverse registration order, so dependent resources can stop before what they rely on.

## Add an App-Owned Hook

Expand Down
29 changes: 27 additions & 2 deletions docs/core/code-generation.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,36 @@ The output can include managers, accessors, configuration types, driver manifest

See [Configuration Reference](/reference/configuration) for Project inputs and [Environment Reference](/reference/env-vars#resolution-and-naming) for driver and named-resource inputs.

## One Resource from Input to App API

For example, adding a named queue starts with configuration:

```dotenv
QUEUE_SUPPORTED_DRIVERS=workerpool,redis
QUEUE_CRITICAL_DRIVER=redis
```

The next `forj build` turns that input into concrete source and a compiled App contract:

```text
.env
└── QUEUE_CRITICAL_DRIVER=redis
↓ forj build
internal/queues/manager_gen.go supported driver construction
internal/queues/accessors_gen.go Critical() accessor
↓ Wire
app.Queues().Critical() stable App API
```

Application code depends on `Critical()`, not a Redis constructor. Runtime configuration may switch that queue to another already-supported driver; changing the supported set or adding another named queue regenerates the contract.

This is the useful test for generation: an input that changes compile-time capability should produce readable code, a stable typed API, and an early build failure when the graph cannot be satisfied.

<span id="choose-a-safe-extension-point"></span>

## Ownership Models

Generated Projects contain three practical ownership models:
GoForj Projects contain three practical ownership models:

| Ownership | How to work with it |
| --- | --- |
Expand Down Expand Up @@ -167,6 +192,6 @@ A normal application implementation change also needs a new binary, but it does

- [Apps](/core/apps) explains app composition and ownership.
- [Dependency Injection](/core/dependency-injection) explains providers and Wire.
- [Generated Files](/reference/generated-files) lists important generated locations.
- [File Ownership](/reference/generated-files) lists important generated locations.
- [Generation Commands](/reference/generation-commands) is the command lookup.
- [Make Command Reference](/reference/make-commands) lists resource scaffolding and registration changes.
65 changes: 65 additions & 0 deletions docs/core/dependency-injection.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,36 @@ app/routes.go App-owned route composition

Additional apps use the same shape under `app/<name>/wire/` and `app/<name>/routes.go`.

This is where application services belong. GoForj supplies the framework providers, but your App-owned `_app.go` files are the normal place to add constructors for services, repositories, gateways, clients, and adapters used by that App.

## Share a Service Between Apps

Sharing an `internal` package does not share a runtime singleton. Each App has its own entrypoint and Wire graph, so each binary constructs the service for itself:

::: code-group

<!-- go-example: illustrative-fragment -->
```go [app/wire/inject_services_app.go]
var appSet = wire.NewSet(
reports.NewService,
app.NewLifecycleRegistry,
runtime.NewTimeouts,
)
```

<!-- go-example: illustrative-fragment -->
```go [app/admin/wire/inject_services_app.go]
var appSet = wire.NewSet(
reports.NewService,
admin.NewLifecycleRegistry,
runtime.NewTimeouts,
)
```

:::

Both Apps reuse `internal/reports.Service`, but the default App might expose it through a public controller while `admin` exposes staff-only routes and commands. Adding the constructor to one App does not silently add it to the other.

## Providers

A provider is an ordinary Go constructor or function that Wire calls while constructing an App. Its parameters declare dependencies and its return type supplies a value to another constructor.
Expand Down Expand Up @@ -119,6 +149,41 @@ func ProvideGateway(cfg GatewayConfig) (*Gateway, error) {

Wire propagates that error through App construction. Resolve configuration near the root and pass typed values down so malformed configuration fails during construction rather than during a later request or job.

### Organize your own injector sets

When one service area has several providers, keep them in another App-owned `_app.go` file and include the set from `appSet`:

::: code-group

<!-- go-example: illustrative-fragment -->
```go [app/wire/inject_billing_app.go]
package wire

import (
"github.com/goforj/wire"
"example.com/acme/internal/billing"
)

var billingSet = wire.NewSet(
billing.NewGateway,
billing.NewRepository,
billing.NewService,
)
```

<!-- go-example: illustrative-fragment -->
```go [app/wire/inject_services_app.go]
var appSet = wire.NewSet(
billingSet,
app.NewLifecycleRegistry,
runtime.NewTimeouts,
)
```

:::

The custom injector remains ordinary App-owned Go code. Nesting it in `appSet` connects it to the root graph without editing `wire_gen.go` or hiding dependencies behind a registry.

### Provider boundaries

Providers may construct services, repositories, controllers, commands, job handlers, typed configuration, adapters, drivers, managers, and runtime registries. Keep their responsibility narrow: construct dependencies, select implementations, and validate construction inputs.
Expand Down
18 changes: 14 additions & 4 deletions docs/core/local-first-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,23 @@ Local-first does not mean local-only. It means the first working path is small,

Start with:

```bash
forj dev
```

The development loop prepares dependencies, builds each configured SPA and App, starts the selected runtimes, and watches their inputs. A failed build leaves the last healthy App running, so ordinary edits do not turn the local loop into a sequence of process crashes.

New Projects describe that workflow under `dev.apps` in `.goforj.yml`. Each App can own its build, runtime, and one or more SPA builds while independent tooling remains under `dev.watches`. See [forj dev](/developer-tools/forj-dev) for the lifecycle graph and customization options.

## Run One App Directly

Use the App command when you want to inspect the combined runtime without the development watcher:

```bash
forj app
```

The generated `app` command hosts enabled runtimes together in one process. Topology comes from the command you launch, not an environment mode switch.
The `app` command hosts enabled runtimes together in one process. Topology comes from the command you launch, not an environment mode switch.

For an additional app, add the app name:

Expand Down Expand Up @@ -76,8 +88,6 @@ This should be a configuration and provider-support change, not a business-logic

## Development Workflow

Use `forj dev` for watcher-driven local development. Each entry under `dev.apps` controls that App's managed build and runtime participation; sibling `dev.watches` remain independent.

Use `forj build` before relying on generated code or binaries:

```bash
Expand Down Expand Up @@ -120,4 +130,4 @@ Local-first docs should avoid:

- [Runtime Topology](/core/runtime-topology) explains combined and split process shapes.
- [Drivers and Adapters](/core/drivers-and-adapters) explains driver selection.
- [Generated Components](/core/code-generation) explains how driver support is compiled into the App.
- [Code Generation](/core/code-generation) explains how driver support is compiled into the App.
72 changes: 67 additions & 5 deletions docs/core/named-resources.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,34 @@ description: How GoForj Apps expose named caches, disks, queues, event buses, me

# Named Resources

A named resource is an operational object the App can use, discover, or expose by a stable name.
A named resource gives application code a stable, typed handle such as `uploads`, `critical`, or `audit` while configuration chooses the backing driver.

Names make runtime behavior visible. They also let application code switch infrastructure without changing business logic.
That separation is one of GoForj's main configuration strengths: the same service can use an in-process queue locally and Redis in production without changing the queue name or dispatch code.

```mermaid
flowchart LR
service[Application service] --> accessor[Queues().Critical()]
dev[Local config<br/>workerpool] --> driver[Selected queue driver]
prod[Production config<br/>redis] --> driver
driver --> accessor
accessor --> queue[critical queue]
```

The accessor is compiled from the Project's named resource configuration. The active driver is selected at startup from the drivers already compiled into the App.

## Common Named Resources

Examples include:
Resource families with generated accessors include:

- cache accessors
- caches
- storage disks
- queues
- event buses
- mailers
- database connections

Other operational objects also have stable names, but they are registered rather than exposed as infrastructure accessors:

- jobs
- schedules
- routes
Expand Down Expand Up @@ -78,6 +94,52 @@ app.Mail().Transactional()

Accessors come from configuration. After adding or renaming named resources, run `forj build`; `forj dev` does this automatically for apps listed in `dev.apps`.

## Use a Named Resource in a Service

Inject the owning manager once, then choose the named resource where the workflow needs it:

<!-- go-example: illustrative-fragment -->
```go
type AlertService struct {
queues *queues.Manager
}

func NewAlertService(queueManager *queues.Manager) *AlertService {
return &AlertService{queues: queueManager}
}

func (s *AlertService) Dispatch(ctx context.Context, payload []byte) error {
critical := s.queues.Critical()
_, err := critical.WithContext(ctx).Dispatch(
queue.NewJob(AlertJobTypeName).Payload(payload),
)
return err
}
```

The service asks for `critical`; it does not know whether that queue is backed by workerpool, Redis, NATS, SQS, or another supported driver. Add `NewAlertService` to the App's service provider set and let Wire supply the manager.

## Change a Driver Without Changing the Service

Keep the named contract and change only runtime selection:

::: code-group

```dotenv [Local]
QUEUE_SUPPORTED_DRIVERS=workerpool,redis
QUEUE_CRITICAL_DRIVER=workerpool
```

```dotenv [Production]
QUEUE_SUPPORTED_DRIVERS=workerpool,redis
QUEUE_CRITICAL_DRIVER=redis
QUEUE_ADDR=redis:6379
```

:::

Because both drivers are already in `QUEUE_SUPPORTED_DRIVERS`, this switch needs a restart, not regeneration. Adding a new supported driver or a new named accessor requires `forj build`.

## Fail-Fast Invariants

Named accessors represent generated invariants.
Expand Down Expand Up @@ -135,7 +197,7 @@ Avoid raw paths, raw SQL, user IDs, emails, or arbitrary payload values.

## Next Steps

- [Generated Components](/core/code-generation) explains regeneration.
- [Code Generation](/core/code-generation) explains regeneration.
- [Drivers and Adapters](/core/drivers-and-adapters) explains backend selection.
- [Naming Conventions](/reference/naming-conventions) defines stable resource names.
- [Libraries](/libraries/) contains package-level resource behavior.
2 changes: 1 addition & 1 deletion docs/getting-started/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ Use configuration for deployment policy and infrastructure choices. Keep busines

## Edit Local Environment

A generated Project can include:
A Project can include:

- `.env` for the main local configuration
- `.env.local` for local environment overrides
Expand Down
Loading