From 6e8fa02928d2688bcc188567a24fe86e9475c68a Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Sat, 1 Aug 2026 03:06:24 +0000 Subject: [PATCH 1/6] docs: improve documentation quality --- Makefile | 15 +- .../internal/docs/docs_generate_command.go | 16 +- backend/internal/docs/readme_transform.go | 2 +- .../internal/docs/readme_transform_test.go | 6 +- backend/internal/docs/repo.go | 2 +- docs/.vitepress/config.mts | 5 +- .../scripts/audit-content-value.mjs | 3 + .../components/LighthouseProductView.vue | 108 ++++++ docs/.vitepress/theme/custom.css | 12 + docs/.vitepress/theme/index.js | 2 + docs/applications/api-index.md | 12 +- docs/applications/controllers.md | 2 +- docs/applications/http-clients.md | 2 +- docs/applications/http-services.md | 2 +- docs/applications/middleware.md | 14 + docs/applications/requests-validation.md | 314 +++++++++++++++--- docs/applications/routes.md | 1 + docs/async/event-subscribers.md | 1 + docs/async/events.md | 1 + docs/async/jobs.md | 16 +- docs/async/queues.md | 1 + docs/async/scheduler.md | 11 +- docs/cookbook.md | 4 +- docs/core/app-lifecycle.md | 2 +- docs/core/code-generation.md | 29 +- docs/core/dependency-injection.md | 65 ++++ docs/core/local-first-development.md | 18 +- docs/core/named-resources.md | 72 +++- docs/data/database-strategy.md | 14 +- docs/data/index.md | 2 +- docs/data/migrations.md | 16 +- docs/data/repositories.md | 2 +- docs/developer-tools/atlas.md | 31 +- docs/developer-tools/forj-dev.md | 15 +- docs/developer-tools/wiring-recipes.md | 40 ++- docs/drivers.md | 2 +- docs/frontend/react-starter-kit.md | 3 +- docs/frontend/templ-htmx-starter-kit.md | 3 + docs/frontend/vue-starter-kit.md | 1 + docs/getting-started/configuration.md | 2 +- docs/getting-started/project-structure.md | 105 ++++-- docs/getting-started/quickstart.md | 2 +- docs/index.md | 2 +- docs/libraries/atlas.md | 7 +- docs/libraries/cache.md | 10 +- docs/libraries/collection.md | 4 + docs/libraries/console.md | 47 ++- docs/libraries/crypt.md | 4 + docs/libraries/env.md | 4 + docs/libraries/events.md | 14 +- docs/libraries/execx.md | 10 +- docs/libraries/godump.md | 4 + docs/libraries/httpx.md | 6 +- docs/libraries/index.md | 4 +- docs/libraries/mail.md | 10 +- docs/libraries/metrics.md | 24 +- docs/libraries/queue.md | 30 +- docs/libraries/scheduler.md | 68 ++-- docs/libraries/storage.md | 30 +- docs/libraries/strings.md | 10 +- docs/libraries/web.md | 14 +- docs/libraries/wire.md | 6 +- docs/operations/deployment-basics.md | 76 ++++- docs/operations/http-server.md | 14 + docs/operations/index.md | 1 + docs/operations/lighthouse.md | 16 +- docs/operations/logging.md | 27 +- docs/operations/metrics.md | 19 ++ docs/operations/performance-benchmarks.md | 98 ++++++ docs/operations/runtime-processes.md | 2 +- docs/operations/scheduler-processes.md | 4 +- docs/public/design-system.css | 12 + docs/reference/configuration.md | 18 +- docs/reference/env-vars.md | 15 +- docs/reference/errors.md | 4 +- docs/reference/generated-files.md | 118 ++++--- docs/reference/generation-commands.md | 2 +- docs/reference/index.md | 2 +- docs/reference/make-commands.md | 15 +- docs/scenarios/cached-user-profile.md | 4 +- docs/scenarios/file-upload-storage.md | 4 +- docs/scenarios/json-api-route.md | 8 +- docs/scenarios/reports-daily-schedule.md | 94 +++--- docs/scenarios/reports-generate-job.md | 4 +- docs/scenarios/runtime-observability.md | 10 +- docs/scenarios/users-created-event.md | 4 +- docs/starter-kits.md | 2 +- docs/testing/index.md | 2 +- docs/testing/integration-tests.md | 2 +- docs/testing/rendered-app-smoke-tests.md | 8 +- 90 files changed, 1389 insertions(+), 480 deletions(-) create mode 100644 docs/.vitepress/theme/components/LighthouseProductView.vue create mode 100644 docs/operations/performance-benchmarks.md diff --git a/Makefile b/Makefile index 0ea098c..70d8bb9 100644 --- a/Makefile +++ b/Makefile @@ -47,20 +47,7 @@ RESET := $(shell tput -Txterm sgr0) .PHONY: build test -HELP_FUN = \ - %help; \ - while(<>) { \ - push @{$$help{$$2 // 'options'}}, [$$1, $$3] if /^([a-zA-Z\-]+)\s*:.*\#\#(?:@([a-zA-Z\-]+))?\s(.*)$$/ }; \ - print "\n"; \ - for (sort keys %help) { \ - print "${WHITE}$$_${RESET \ - }\n"; \ - for (@{$$help{$$_}}) { \ - $$sep = " " x (32 - length $$_->[0]); \ - print " ${YELLOW}$$_->[0]${RESET}$$sep${GREEN}$$_->[1]${RESET}\n"; \ - }; \ - print ""; \ - } +HELP_FUN = %help; while(<>) { if (/^([A-Za-z0-9_-]+)\s*:.*\#\#(?:@([A-Za-z0-9_-]+))?\s(.*)$$/) { push @{$$help{$$2 || "other"}}, [$$1, $$3]; $$width = length($$1) if length($$1) > $$width } } print "\n"; for $$category (sort keys %help) { print "${WHITE}$$category${RESET}\n"; for $$entry (@{$$help{$$category}}) { printf " ${YELLOW}%-*s${RESET} ${GREEN}%s${RESET}\n", $$width, $$entry->[0], $$entry->[1] } } help: ##@other Show this help. @perl -e '$(HELP_FUN)' $(MAKEFILE_LIST) diff --git a/backend/internal/docs/docs_generate_command.go b/backend/internal/docs/docs_generate_command.go index 61e0c34..142a880 100644 --- a/backend/internal/docs/docs_generate_command.go +++ b/backend/internal/docs/docs_generate_command.go @@ -69,7 +69,7 @@ func (c *GenerateCommand) Run() error { FrameworkGuide: FrameworkGuide{ Title: "HTTP Services", Path: "/applications/http-services", - Summary: "Generated Apps register web routes and controllers through the HTTP runtime. Keep server wiring in framework providers and inject application services into controllers.", + Summary: "GoForj Apps register web routes and controllers through the HTTP runtime. Keep server wiring in framework providers and inject application services into controllers.", }, }, { @@ -114,7 +114,7 @@ func (c *GenerateCommand) Run() error { FrameworkGuide: FrameworkGuide{ Title: "Scheduler", Path: "/async/scheduler", - Summary: "Generated Apps register schedules in the scheduler runtime and inject the jobs they run. Keep recurring business work in jobs instead of the schedule registry.", + Summary: "GoForj Apps register schedules in the scheduler runtime and inject the jobs they run. Keep recurring business work in jobs instead of the schedule registry.", }, }, { @@ -127,7 +127,7 @@ func (c *GenerateCommand) Run() error { FrameworkGuide: FrameworkGuide{ Title: "Queues", Path: "/async/queues", - Summary: "Generated Apps expose named queues through generated accessors. Dispatch jobs through those accessors and keep backend selection in queue configuration.", + Summary: "GoForj Apps expose named queues through generated accessors. Dispatch jobs through those accessors and keep backend selection in queue configuration.", }, }, { @@ -140,7 +140,7 @@ func (c *GenerateCommand) Run() error { FrameworkGuide: FrameworkGuide{ Title: "Events", Path: "/async/events", - Summary: "Generated Apps expose named event buses through generated accessors. Publish through those accessors and keep driver selection in event configuration.", + Summary: "GoForj Apps expose named event buses through generated accessors. Publish through those accessors and keep driver selection in event configuration.", }, }, { @@ -153,7 +153,7 @@ func (c *GenerateCommand) Run() error { FrameworkGuide: FrameworkGuide{ Title: "Mail", Path: "/applications/mail", - Summary: "Generated Apps expose named mailers through generated accessors. Send through those accessors and keep transport selection and credentials in configuration.", + Summary: "GoForj Apps expose named mailers through generated accessors. Send through those accessors and keep transport selection and credentials in configuration.", }, }, { @@ -166,7 +166,7 @@ func (c *GenerateCommand) Run() error { FrameworkGuide: FrameworkGuide{ Title: "Cache Patterns", Path: "/data/cache-patterns", - Summary: "Generated Apps expose named caches through generated accessors. Use those accessors in application services and keep backend selection in cache configuration.", + Summary: "GoForj Apps expose named caches through generated accessors. Use those accessors in application services and keep backend selection in cache configuration.", }, }, { @@ -187,7 +187,7 @@ func (c *GenerateCommand) Run() error { FrameworkGuide: FrameworkGuide{ Title: "Storage Patterns", Path: "/data/storage-patterns", - Summary: "Generated Apps expose named disks through generated accessors. Use those accessors in application services and keep backend selection in storage configuration.", + Summary: "GoForj Apps expose named disks through generated accessors. Use those accessors in application services and keep backend selection in storage configuration.", }, }, { @@ -200,7 +200,7 @@ func (c *GenerateCommand) Run() error { FrameworkGuide: FrameworkGuide{ Title: "Metrics", Path: "/operations/metrics", - Summary: "Generated Apps expose metrics through the observability and HTTP runtime. Keep registration close to the behavior being measured and configure scrape exposure through the App runtime.", + Summary: "GoForj Apps expose metrics through the observability and HTTP runtime. Keep registration close to the behavior being measured and configure scrape exposure through the App runtime.", }, }, { diff --git a/backend/internal/docs/readme_transform.go b/backend/internal/docs/readme_transform.go index 372c256..44d2487 100644 --- a/backend/internal/docs/readme_transform.go +++ b/backend/internal/docs/readme_transform.go @@ -34,7 +34,7 @@ func appendFrameworkGuide(content string, guide FrameworkGuide) string { content = strings.TrimRight(content, "\n") return fmt.Sprintf( - "%s\n\n## Using with GoForj\n\n%s\n\nFor generated App integration, see [%s](%s).\n", + "%s\n\n## Using with GoForj\n\n%s\n\nFor the App workflow, see [%s](%s).\n", content, guide.Summary, guide.Title, diff --git a/backend/internal/docs/readme_transform_test.go b/backend/internal/docs/readme_transform_test.go index b5016ac..d52f1e7 100644 --- a/backend/internal/docs/readme_transform_test.go +++ b/backend/internal/docs/readme_transform_test.go @@ -68,7 +68,7 @@ func TestTransformReadmeAppendsFrameworkGuide(t *testing.T) { FrameworkGuide: FrameworkGuide{ Title: "Queues", Path: "/async/queues", - Summary: "Generated Apps expose named queues through generated accessors.", + Summary: "GoForj Apps expose named queues through generated accessors.", }, } @@ -76,8 +76,8 @@ func TestTransformReadmeAppendsFrameworkGuide(t *testing.T) { wants := []string{ `description: "Queued work with pluggable backend drivers."`, "## Using with GoForj {#using-with-goforj}", - "Generated Apps expose named queues through generated accessors.", - "For generated App integration, see [Queues](/async/queues).", + "GoForj Apps expose named queues through generated accessors.", + "For the App workflow, see [Queues](/async/queues).", } for _, want := range wants { if !strings.Contains(got, want) { diff --git a/backend/internal/docs/repo.go b/backend/internal/docs/repo.go index eaeb0b3..a527ce4 100644 --- a/backend/internal/docs/repo.go +++ b/backend/internal/docs/repo.go @@ -15,7 +15,7 @@ type RepoConfig struct { FrameworkGuide FrameworkGuide } -// FrameworkGuide links a standalone library page to its canonical generated App guide. +// FrameworkGuide links a standalone library page to its canonical App guide. type FrameworkGuide struct { Title string Path string diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 1c513bf..d5bffa9 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -933,7 +933,7 @@ const frontendSidebar = sectionSidebar('Frontend', [ const dataSidebar = sectionSidebar('Data and Persistence', [ { text: 'Overview', link: '/data/' }, - { text: 'Database Strategy', link: '/data/database-strategy' }, + { text: 'Database Connections', link: '/data/database-strategy' }, { text: 'Migrations', link: '/data/migrations' }, { text: 'Repositories', link: '/data/repositories' }, { text: 'Transactions', link: '/data/transactions' }, @@ -989,6 +989,7 @@ const operationsSidebar = sectionSidebar('Operations', [ { text: 'Metrics', link: '/operations/metrics' }, { text: 'Inspects', link: '/operations/inspects' }, { text: 'Lighthouse', link: '/operations/lighthouse' }, + { text: 'Performance Benchmarks', link: '/operations/performance-benchmarks' }, { text: 'Backup and Restore', link: '/operations/backups' } ]) @@ -1029,7 +1030,7 @@ const referenceSidebar = sectionSidebar('Reference', [ { text: 'CLI Reference', link: '/reference/cli' }, { text: 'Environment Reference', link: '/reference/env-vars' }, { text: 'Configuration Reference', link: '/reference/configuration' }, - { text: 'Generated Files', link: '/reference/generated-files' }, + { text: 'File Ownership', link: '/reference/generated-files' }, { text: 'Generation Commands', link: '/reference/generation-commands' }, { text: 'Make Commands', link: '/reference/make-commands' }, { text: 'Naming Conventions', link: '/reference/naming-conventions' }, diff --git a/docs/.vitepress/scripts/audit-content-value.mjs b/docs/.vitepress/scripts/audit-content-value.mjs index c857b48..8ba9902 100644 --- a/docs/.vitepress/scripts/audit-content-value.mjs +++ b/docs/.vitepress/scripts/audit-content-value.mjs @@ -208,6 +208,9 @@ function auditForbiddenPatterns() { if (relativePath.startsWith('operations/') && /\|\s*Development alias\s*\|/i.test(source)) { editorialFailures.push(`${relativePath}: operations pages must lead with supervised binary commands; link to development guidance instead of adding an alias column`) } + if (/\bgenerated\s+(?:Apps?|applications?)\b/i.test(source)) { + editorialFailures.push(`${relativePath}: call the runnable boundary an App; attach generated to the specific file, accessor, provider, or output instead`) + } } } diff --git a/docs/.vitepress/theme/components/LighthouseProductView.vue b/docs/.vitepress/theme/components/LighthouseProductView.vue new file mode 100644 index 0000000..bc06eea --- /dev/null +++ b/docs/.vitepress/theme/components/LighthouseProductView.vue @@ -0,0 +1,108 @@ + + + diff --git a/docs/.vitepress/theme/custom.css b/docs/.vitepress/theme/custom.css index f500244..fdaa91b 100644 --- a/docs/.vitepress/theme/custom.css +++ b/docs/.vitepress/theme/custom.css @@ -7358,3 +7358,15 @@ html:not(.dark) .gf-blog-visual { --gf-ink: #FFFFFF; --gf-ink-2: #A9A1B3; } +/* Atlas uses its source-owned product banner while the framework guide remains the preferred user path. */ +.vp-doc .gf-atlas-banner { + margin: 1.5rem 0 1.75rem; +} + +.vp-doc .gf-atlas-banner img { + display: block; + width: 100%; + border: 1px solid var(--vp-c-divider); + border-radius: 16px; + box-shadow: 0 18px 48px rgb(0 0 0 / 18%); +} diff --git a/docs/.vitepress/theme/index.js b/docs/.vitepress/theme/index.js index f18d53b..cab8aaf 100644 --- a/docs/.vitepress/theme/index.js +++ b/docs/.vitepress/theme/index.js @@ -8,6 +8,7 @@ import StarterKitOptions from './components/StarterKitOptions.vue' import SitePreview from './components/SitePreview.vue' import CodeFile from './components/CodeFile.vue' import MakeCommandTabs from './components/MakeCommandTabs.vue' +import LighthouseProductView from './components/LighthouseProductView.vue' import './custom.css' /* The hero is imported STATICALLY on purpose. As an async component it @@ -683,6 +684,7 @@ export default { ctx.app.component('GoForjLiveTerminal', GoForjLiveTerminal) ctx.app.component('CodeFile', CodeFile) ctx.app.component('MakeCommandTabs', MakeCommandTabs) + ctx.app.component('LighthouseProductView', LighthouseProductView) }, Layout: () => { const { theme } = useData() diff --git a/docs/applications/api-index.md b/docs/applications/api-index.md index 021efbc..55188a3 100644 --- a/docs/applications/api-index.md +++ b/docs/applications/api-index.md @@ -148,9 +148,9 @@ The three artifacts form one generation: This means a failed compile or process-start attempt cannot replace the last working API contract with candidate output from a broken App. -## Serve OpenAPI +## Serve the API reference {#serve-openapi} -When HTTP and Swagger support are enabled, the generated runtime serves: +When HTTP and API reference support are enabled, the generated runtime serves these compatibility routes: ```text GET /swagger @@ -158,20 +158,20 @@ GET /swagger/ GET /swagger/doc.json ``` -`/swagger` and `/swagger/` serve the Scalar UI. `/swagger/doc.json` serves the active App's OpenAPI JSON: +`/swagger` and `/swagger/` are the established URLs for the Scalar API reference. `/swagger/doc.json` serves the active App's OpenAPI JSON: - default App: `build/openapi.json` - additional app: `build//openapi.json` The selected app never falls back to the default App document. If its artifact is missing, `/swagger/doc.json` returns a JSON `404` with the exact `forj build:api-index` command needed to create it. -Enable these routes with: +Enable these routes with the established configuration key: ```text API_SWAGGER_ENABLED=true ``` -`SWAGGER_ENABLED` remains a legacy fallback. Use `OPENAPI_SPEC_PATH` only as an explicit serving override for an arbitrary document: +The `/swagger` paths and `API_SWAGGER_ENABLED` name remain for compatibility; they do not mean the UI is Swagger UI. `SWAGGER_ENABLED` remains a legacy fallback. Use `OPENAPI_SPEC_PATH` only as an explicit serving override for an arbitrary document: ```text OPENAPI_SPEC_PATH=build/contracts/public.json @@ -216,6 +216,6 @@ The first two artifacts must be non-empty. Review the diagnostics file before pu - [Routes](/applications/routes) - [HTTP Services](/applications/http-services) -- [Generated Files](/reference/generated-files) +- [File Ownership](/reference/generated-files) - [HTTP Tests](/testing/http-tests) - [Web](/web) diff --git a/docs/applications/controllers.md b/docs/applications/controllers.md index 7ef7d73..07b0013 100644 --- a/docs/applications/controllers.md +++ b/docs/applications/controllers.md @@ -207,7 +207,7 @@ Use `web.Context` for HTTP-specific behavior such as params, binding, response h ## Next Steps - [JSON API Route](/scenarios/json-api-route) follows a complete controller, service, test, build, route-list, and request workflow. -- [Make Command Reference](/reference/make-commands) explains grouped package placement and generated wiring updates. +- [`make:controller` Reference](/reference/make-commands#make-controller) explains grouped package placement and generated wiring updates. - [Wiring Recipes](/developer-tools/wiring-recipes) shows the controller wiring flow. - [Requests and Validation](/applications/requests-validation) explains request input boundaries. - [Responses and Errors](/applications/responses-errors) explains response shape. diff --git a/docs/applications/http-clients.md b/docs/applications/http-clients.md index e0be280..b538c0f 100644 --- a/docs/applications/http-clients.md +++ b/docs/applications/http-clients.md @@ -72,7 +72,7 @@ func (c *Client) FindInvoice(ctx context.Context, id string) (Invoice, error) { } ``` -Generated GoForj Apps currently pin `github.com/goforj/httpx` v1. Use that module path unless the App's `go.mod` has intentionally been upgraded. +GoForj Apps currently pin `github.com/goforj/httpx` v1. Use that module path unless the App's `go.mod` has intentionally been upgraded. ## Configure and Provide the Client diff --git a/docs/applications/http-services.md b/docs/applications/http-services.md index 6e372e1..e4dec39 100644 --- a/docs/applications/http-services.md +++ b/docs/applications/http-services.md @@ -5,7 +5,7 @@ description: Orient an HTTP service around its runtime, route composition, contr # HTTP Services -An HTTP service in GoForj combines an app-owned route registry and controllers with the generated HTTP runtime. Application code owns endpoint behavior; the framework owns server composition, startup, shutdown, health, readiness, and supported observability. +An HTTP service in GoForj combines an App-owned route registry and controllers with the framework-managed HTTP runtime. Application code owns endpoint behavior; the Framework owns server composition, startup, shutdown, health, readiness, and supported observability. This page is the map through that system. Follow [JSON API Route](/scenarios/json-api-route) for the canonical runnable implementation. diff --git a/docs/applications/middleware.md b/docs/applications/middleware.md index d05933e..9448b72 100644 --- a/docs/applications/middleware.md +++ b/docs/applications/middleware.md @@ -94,6 +94,20 @@ Middleware is a good fit for: Middleware is not a good place for business workflows. +## Common Middleware Needs + +The Web library provides reusable middleware; App route setup decides where each policy applies: + +- [CORS](/web#webmiddleware-cors) for explicit browser origins +- [body limits](/web#webmiddleware-bodylimit) before binding large request payloads +- [timeouts](/web#webmiddleware-timeout) around bounded request work +- [trusted proxy and real-IP handling](/web#webmiddleware-proxy) before using client IP for security policy +- [secure response headers](/web#webmiddleware-secure) and [CSRF protection](/web#webmiddleware-csrf) for browser-facing routes +- [compression](/web#compression-middleware) for suitable response bodies +- [rate limiting](/web#rate-limiting-middleware) for request-level admission control + +The built-in rate-limiter memory store is process-local. It is appropriate for one process or deliberately per-instance limits, but replicas do not share its counters. A deployment that requires one limit across replicas needs a shared backend and an application-owned adapter, such as a generated cache store using its [rate-limit operation](/cache#rate-limiting). Configure trusted proxies before keying limits by `Context.RealIP`, and keep the limiter store alive for the lifetime of the App rather than constructing it per request. + ## Testing Middleware Use the `webtest` helpers from [Web](/web) to prove both the rejected and accepted paths. Create `internal/reports/middleware_test.go`: diff --git a/docs/applications/requests-validation.md b/docs/applications/requests-validation.md index cc8b238..b8eac0c 100644 --- a/docs/applications/requests-validation.md +++ b/docs/applications/requests-validation.md @@ -5,40 +5,139 @@ description: How controllers bind, normalize, validate, and pass request input i # Requests and Validation -Request handling should make invalid input visible at the HTTP boundary before application services perform business behavior. +Request handling should make invalid input visible at the HTTP boundary before application services perform business behavior. Controllers own HTTP translation; services own application behavior. -Controllers own request translation. Services own application behavior. +## Map Binding and Validation Separately -## Golden Path +`web.Context.Bind` asks the active Web adapter to decode the request into the target value. A bind error means the payload could not be decoded. It is different from a decoded payload that fails application input rules. -The [JSON API Route](/scenarios/json-api-route) demonstrates the complete HTTP path with a service-backed `GET` request. For a mutating route, preserve that same ownership: controller binds and normalizes HTTP input, a typed service input crosses into application behavior, and the controller maps known validation errors to an explicit client response. +Keep those failures distinct in the public response: -## Bind Input +| Failure | Status | Stable error code | Field errors | +| --- | ---: | --- | --- | +| `ctx.Bind` cannot decode the payload | `400 Bad Request` | `invalid_payload` | none | +| Decoded input fails request validation | `422 Unprocessable Entity` | `validation_failed` | stable field name-to-code map | +| The service fails | application error policy | service-owned | service-owned | -Use `web.Context` to bind request payloads: +Do not return `err.Error()` from `Bind`. Its text belongs to the active binder, can expose parsing details, and is not a stable API contract. + +## Define the Service Input + +Keep the service input independent from JSON field names. In `internal/users/service.go`: ```go -type CreateUserRequest struct { +// Package users owns user application behavior and its HTTP adapter. +package users + +import "context" + +// CreateUserInput is the normalized input accepted by the user service. +type CreateUserInput struct { + DisplayName string + Email string + Password string +} + +// User is the public result of creating a user. +type User struct { + ID string `json:"id"` + DisplayName string `json:"display_name"` + Email string `json:"email"` +} + +// Service owns user application behavior. +type Service struct{} + +// NewService constructs the user service. +func NewService() *Service { + return &Service{} +} + +// Create creates a user from normalized input. +func (*Service) Create(_ context.Context, input CreateUserInput) (User, error) { + return User{ + ID: "user_123", + DisplayName: input.DisplayName, + Email: input.Email, + }, nil +} +``` + +The fixed result keeps this example focused on the request boundary. A real service can enforce business rules and call its injected repository without changing the controller contract. + +## Bind, Normalize, and Validate + +Use a request-only type for JSON binding and a stable response type for validation failures. In `internal/users/controller.go`: + + +```go +package users + +import ( + "context" + "net/http" + "net/mail" + "strings" + "unicode/utf8" + + "github.com/goforj/web" +) + +const ( + errorInvalidPayload = "invalid_payload" + errorValidationFailed = "validation_failed" + fieldInvalidFormat = "invalid_format" + fieldRequired = "required" + fieldTooShort = "too_short" +) + +type createUserRequest struct { DisplayName string `json:"display_name"` Email string `json:"email"` Password string `json:"password"` } +type errorResponse struct { + Error string `json:"error"` + Fields map[string]string `json:"fields,omitempty"` +} + +type userCreator interface { + Create(context.Context, CreateUserInput) (User, error) +} + +// Controller translates user HTTP requests into service calls. +type Controller struct { + service userCreator +} + +// NewController constructs the user HTTP adapter. +func NewController(service *Service) *Controller { + return &Controller{service: service} +} + +// Routes declares the user endpoints owned by this controller. +func (c *Controller) Routes() []web.Route { + return []web.Route{ + web.NewRoute(http.MethodPost, "/users", c.Store), + } +} + +// Store validates a create-user request before calling the service. func (c *Controller) Store(ctx web.Context) error { - var req CreateUserRequest - if err := ctx.Bind(&req); err != nil { - return ctx.JSON(http.StatusBadRequest, map[string]any{ - "ok": false, - "error": "invalid payload", + var request createUserRequest + if err := ctx.Bind(&request); err != nil { + return ctx.JSON(http.StatusBadRequest, errorResponse{ + Error: errorInvalidPayload, }) } - input, err := req.Input() - if err != nil { - return ctx.JSON(http.StatusBadRequest, map[string]any{ - "ok": false, - "error": err.Error(), + input, fields := request.input() + if len(fields) != 0 { + return ctx.JSON(http.StatusUnprocessableEntity, errorResponse{ + Error: errorValidationFailed, + Fields: fields, }) } @@ -49,58 +148,181 @@ func (c *Controller) Store(ctx web.Context) error { return ctx.JSON(http.StatusCreated, user) } -``` - -## Normalize Before Validation -Normalize request input before validation: - - -```go -func (r CreateUserRequest) Input() (CreateUserInput, error) { - email := strings.TrimSpace(strings.ToLower(r.Email)) +// input normalizes user-facing identifiers and reports stable validation codes. +func (r createUserRequest) input() (CreateUserInput, map[string]string) { displayName := strings.TrimSpace(r.DisplayName) + email := strings.ToLower(strings.TrimSpace(r.Email)) + fields := make(map[string]string) - if displayName == "" || email == "" || r.Password == "" { - return CreateUserInput{}, errors.New("display_name, email, and password are required") + if displayName == "" { + fields["display_name"] = fieldRequired + } + if email == "" { + fields["email"] = fieldRequired + } else if !validEmail(email) { + fields["email"] = fieldInvalidFormat + } + if r.Password == "" { + fields["password"] = fieldRequired + } else if utf8.RuneCountInString(r.Password) < 12 { + fields["password"] = fieldTooShort } return CreateUserInput{ DisplayName: displayName, Email: email, Password: r.Password, - }, nil + }, fields +} + +// validEmail accepts a plain mailbox while rejecting display-name forms. +func validEmail(value string) bool { + address, err := mail.ParseAddress(value) + return err == nil && address.Address == value } ``` -This keeps controller code readable and gives the service a typed input. +Display names and email addresses are normalized because surrounding whitespace is not meaningful for those fields. The password is intentionally not trimmed: whitespace can be part of a credential, and silently changing it can make the password a user supplied differ from the password the service stores. This example counts every password rune, including whitespace, toward the minimum. If an application rejects all-whitespace passwords or applies another password policy, make that a separate explicit rule while still passing the original value onward. -## Validation Boundary +The field keys match the JSON request fields, and the values are machine-readable codes rather than prose. Clients can translate those codes without coupling themselves to server wording. Each field receives one deterministic code because the checks prioritize `required` before format or length rules. + +## Test Invalid and Valid Requests -Validate: +Controller tests should prove malformed JSON, stable field errors, normalization, and password preservation. Create `internal/users/controller_test.go`: -- required fields -- basic shape -- allowed values -- path and query parameter presence -- payload size through middleware when relevant + +```go +package users -Leave business rules to services when those rules require persistence, permissions, workflows, or domain decisions. +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "reflect" + "strings" + "testing" -## Context Propagation + "github.com/goforj/web/webtest" +) -Pass `ctx.Context()` into services: +type recordingCreator struct { + input CreateUserInput +} - -```go -user, err := c.service.Create(ctx.Context(), input) +// Create records the service input received from the controller. +func (c *recordingCreator) Create(_ context.Context, input CreateUserInput) (User, error) { + c.input = input + return User{ + ID: "user_123", + DisplayName: input.DisplayName, + Email: input.Email, + }, nil +} + +// TestControllerStoreRejectsMalformedPayload verifies bind failures use the public payload error. +func TestControllerStoreRejectsMalformedPayload(t *testing.T) { + controller := &Controller{service: &recordingCreator{}} + req := httptest.NewRequest(http.MethodPost, "/users", strings.NewReader(`{"email":`)) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + ctx := webtest.NewContext(req, rec, "/users", nil) + + if err := controller.Store(ctx); err != nil { + t.Fatalf("store user: %v", err) + } + + var response errorResponse + if err := json.NewDecoder(rec.Body).Decode(&response); err != nil { + t.Fatalf("decode response: %v", err) + } + if rec.Code != http.StatusBadRequest { + t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest) + } + if response.Error != errorInvalidPayload || len(response.Fields) != 0 { + t.Fatalf("response = %#v", response) + } +} + +// TestControllerStoreReportsStableFieldErrors verifies decoded invalid input maps to field codes. +func TestControllerStoreReportsStableFieldErrors(t *testing.T) { + controller := &Controller{service: &recordingCreator{}} + body := `{"display_name":" ","email":"not-an-email","password":"short"}` + req := httptest.NewRequest(http.MethodPost, "/users", strings.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + ctx := webtest.NewContext(req, rec, "/users", nil) + + if err := controller.Store(ctx); err != nil { + t.Fatalf("store user: %v", err) + } + + var response errorResponse + if err := json.NewDecoder(rec.Body).Decode(&response); err != nil { + t.Fatalf("decode response: %v", err) + } + wantFields := map[string]string{ + "display_name": fieldRequired, + "email": fieldInvalidFormat, + "password": fieldTooShort, + } + if rec.Code != http.StatusUnprocessableEntity { + t.Fatalf("status = %d, want %d", rec.Code, http.StatusUnprocessableEntity) + } + if response.Error != errorValidationFailed || !reflect.DeepEqual(response.Fields, wantFields) { + t.Fatalf("response = %#v, want fields %#v", response, wantFields) + } +} + +// TestControllerStoreNormalizesInputWithoutChangingPassword verifies the successful boundary. +func TestControllerStoreNormalizesInputWithoutChangingPassword(t *testing.T) { + creator := &recordingCreator{} + controller := &Controller{service: creator} + password := " correct horse battery staple " + body := `{"display_name":" Ada Lovelace ","email":" ADA@EXAMPLE.TEST ","password":"` + password + `"}` + req := httptest.NewRequest(http.MethodPost, "/users", strings.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + ctx := webtest.NewContext(req, rec, "/users", nil) + + if err := controller.Store(ctx); err != nil { + t.Fatalf("store user: %v", err) + } + + wantInput := CreateUserInput{ + DisplayName: "Ada Lovelace", + Email: "ada@example.test", + Password: password, + } + if rec.Code != http.StatusCreated { + t.Fatalf("status = %d, want %d", rec.Code, http.StatusCreated) + } + if !reflect.DeepEqual(creator.input, wantInput) { + t.Fatalf("service input = %#v, want %#v", creator.input, wantInput) + } +} +``` + +Run the App test suite from its root: + +```bash +go test ./... ``` -This preserves request cancellation and deadlines across service, repository, queue, cache, storage, and event calls. +Expected result: all three controller paths pass, and the service receives normalized display and email fields with the password unchanged. + +## Validation Boundary + +Validate transport-level requirements such as required fields, basic shapes, allowed values, and path or query parameter presence before calling the service. Leave rules that require persistence, permissions, workflows, or domain decisions to the service. + +Limit payload size before binding by following the [body-limit middleware guidance](/applications/middleware#common-middleware-needs). Keeping admission control with route setup avoids duplicating body-reading policy in controllers. + +Always pass `ctx.Context()` into the service so request cancellation and deadlines continue through repositories, queues, caches, storage, and events. ## Next Steps -- [JSON API Route](/scenarios/json-api-route) provides the complete generate/build/test/curl workflow. +- [JSON API Route](/scenarios/json-api-route) provides a complete generate, build, test, and request workflow. - [Controllers](/applications/controllers) explains request handler structure. -- [Responses and Errors](/applications/responses-errors) explains error response policy. +- [Responses and Errors](/applications/responses-errors) explains application error policy. - [Application Services](/applications/services) explains service inputs. diff --git a/docs/applications/routes.md b/docs/applications/routes.md index 28f6e47..ebbf1d4 100644 --- a/docs/applications/routes.md +++ b/docs/applications/routes.md @@ -138,4 +138,5 @@ Do not add application behavior by editing framework route registration. - [Controllers](/applications/controllers) explains handler structure. - [Middleware](/applications/middleware) explains route and group policy. - [Naming Conventions](/reference/naming-conventions) defines route naming. +- [`make:controller` Reference](/reference/make-commands#make-controller) lists generation, removal, and shared options. - [Web](/web) covers standalone route primitives. diff --git a/docs/async/event-subscribers.md b/docs/async/event-subscribers.md index 5adf2ee..a476ecb 100644 --- a/docs/async/event-subscribers.md +++ b/docs/async/event-subscribers.md @@ -160,3 +160,4 @@ Use queues for durable, retryable, worker-managed work. - [Events](/async/events) explains event publishing. - [Jobs](/async/jobs) explains durable background work. - [Retries and Idempotency](/async/retries-idempotency) explains safe retry design. +- [`make:subscriber` Reference](/reference/make-commands#make-subscriber) lists bus selection, removal, and generated registration. diff --git a/docs/async/events.md b/docs/async/events.md index 8d19a19..f019d64 100644 --- a/docs/async/events.md +++ b/docs/async/events.md @@ -149,4 +149,5 @@ During `forj dev`, an app listed in `dev.apps` rebuilds automatically. [Generati - [Events versus Queues](/async/events-vs-queues) explains boundary decisions. - [Environment Reference](/reference/env-vars#events) lists driver settings. - [Naming Conventions](/reference/naming-conventions) defines stable event topics. +- [`make:event` Reference](/reference/make-commands#make-event) lists generation, removal, and shared options. - [Events](/events) covers standalone package details. diff --git a/docs/async/jobs.md b/docs/async/jobs.md index deb7d1f..867d254 100644 --- a/docs/async/jobs.md +++ b/docs/async/jobs.md @@ -80,19 +80,14 @@ func NewGenerateJob(queues *queues.Manager) *GenerateJob { return &GenerateJob{queues: queues} } -// Queue creates a task and dispatches it to the selected queue. -// Add application inputs as arguments when defining the payload contract. -func (t *GenerateJob) Queue(ctx context.Context, name string) error { - var p GenerateJobPayload - // add your payload fields here - // p.User = name - - payload, err := json.Marshal(p) +// Queue dispatches the typed payload to the selected queue. +func (t *GenerateJob) Queue(ctx context.Context, payload GenerateJobPayload) error { + data, err := json.Marshal(payload) if err != nil { return err } _, err = t.queues.WithContext(ctx).Dispatch( - queue.NewJob(GenerateJobTypeName).Payload(payload).OnQueue("reports"), + queue.NewJob(GenerateJobTypeName).Payload(data).OnQueue("reports"), ) return err } @@ -141,6 +136,8 @@ func registerJobHandlers( The scaffold supplies dispatch and handler seams. Replace its placeholder payload with the smallest source-of-truth references the worker needs. +`GenerateJobTypeName` remains an explicit constant because queue registration and transport use a stable string identifier. Go cannot derive a package-level constant from the payload struct type, and reflection would make that operational contract less visible rather than cleaner. + ### Payload and Dependencies @@ -261,3 +258,4 @@ Do not register handlers after workers are already running. - [Workers](/async/workers) explains execution lifecycle. - [Retries and Idempotency](/async/retries-idempotency) explains safe retry behavior. - [Naming Conventions](/reference/naming-conventions) defines stable job names. +- [`make:job` Reference](/reference/make-commands#make-job) lists generation, queue selection, removal, and shared options. diff --git a/docs/async/queues.md b/docs/async/queues.md index 9dd488a..b773408 100644 --- a/docs/async/queues.md +++ b/docs/async/queues.md @@ -334,4 +334,5 @@ During `forj dev`, an app listed in `dev.apps` rebuilds automatically. [Generati - [Jobs](/async/jobs) explains job definitions. - [Workers](/async/workers) explains worker lifecycle. - [Environment Reference](/reference/env-vars#queue) lists queue and driver settings. +- [`make:queue` Reference](/reference/make-commands#make-queue) lists resource generation, removal, and exact output. - [Queue](/queue) covers standalone package details. diff --git a/docs/async/scheduler.md b/docs/async/scheduler.md index 8bedf49..e72011f 100644 --- a/docs/async/scheduler.md +++ b/docs/async/scheduler.md @@ -139,6 +139,12 @@ func (r *ScheduleRegistry) Register(s *schedules.Scheduler) error { `AppSchedules.Register` iterates the collection and registers each task using its `Name`, `Interval`, and `Handle` methods. Manually defined fluent schedules can still follow that call when they need cron expressions, calendar helpers, or other custom registration. +::: info Why the generated schedule owns its interval +`make:schedule` keeps a simple recurring task's name, cadence, and handler in one file while Wire only adds that object to the App collection. This lets the generator add and remove schedules without rewriting the App-owned `app/schedules.go` registry. + +Use the generated form when one task has one interval. If the same workflow must run at two times, keep the workflow in a service or job and register two named fluent schedules in `app/schedules.go`; do not reuse one generated schedule object as two operational schedules. +::: + @@ -184,7 +190,7 @@ Good shape: ```go s.Every(30).Seconds(). Name("monitor:poll"). - Do(s.inspectTask("monitor:poll", s.monitorCheckJob.RunScheduledPoll)) + Do(s.InspectTask("monitor:poll", s.monitorCheckJob.RunScheduledPoll)) ``` Avoid growing scheduler runtime files into business-logic buckets. @@ -208,7 +214,7 @@ Stable schedule names make scheduler behavior understandable, but they do not pr s.EveryFiveMinutes(). WithoutOverlapping(). Name("reports:daily"). - Do(s.inspectTask("reports:daily", s.reports.GenerateDaily)) + Do(s.InspectTask("reports:daily", s.reports.GenerateDaily)) ``` Use `WithoutOverlapping()` for same-process overlap control. Use `WithoutOverlappingWithLocker(...)` with a shared locker when multiple scheduler processes could run the same schedule. @@ -240,4 +246,5 @@ Expected startup includes `Scheduler started`. Let one safe schedule become due - [Runtime Topology](/core/runtime-topology) explains process boundaries. - [Environment Reference](/reference/env-vars#scheduler-and-process-shutdown) lists scheduler timeouts. - [Naming Conventions](/reference/naming-conventions) defines stable schedule names. +- [`make:schedule` Reference](/reference/make-commands#make-schedule) lists cadence flags, removal, and generated registration. - [Scheduler](/scheduler) covers standalone package details. diff --git a/docs/cookbook.md b/docs/cookbook.md index bc038dc..166ba99 100644 --- a/docs/cookbook.md +++ b/docs/cookbook.md @@ -25,7 +25,7 @@ Every entry answers one "how do I" question and links to the page that owns the - Shape JSON responses and errors. [Responses and Errors](/applications/responses-errors) - Add middleware to a route or group. [Middleware](/applications/middleware) - See every route the App serves: `forj route:list`. [Routes](/applications/routes) -- Serve Swagger and OpenAPI. [OpenAPI](/applications/api-index#serve-openapi) +- Serve the OpenAPI document and browser reference. [OpenAPI](/applications/api-index#serve-openapi) - Call an external API with a typed client. [HTTP Clients](/applications/http-clients) - Add health and readiness checks. [Health and Readiness](/operations/health-readiness) @@ -42,7 +42,7 @@ Every entry answers one "how do I" question and links to the page that owns the ## Data -- Choose a database and driver. [Database Strategy](/data/database-strategy) +- Choose a database and driver. [Database Connections](/data/database-strategy) - Decide local versus production drivers for any primitive. [Driver Selection](/data/driver-selection) - Write and run migrations: `forj migrate`. [Migrations](/data/migrations) - Put reads and writes behind a boundary. [Repositories](/data/repositories) diff --git a/docs/core/app-lifecycle.md b/docs/core/app-lifecycle.md index d5dbf6b..a1119f2 100644 --- a/docs/core/app-lifecycle.md +++ b/docs/core/app-lifecycle.md @@ -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 diff --git a/docs/core/code-generation.md b/docs/core/code-generation.md index ef5f899..b56eaa4 100644 --- a/docs/core/code-generation.md +++ b/docs/core/code-generation.md @@ -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. + ## Ownership Models -Generated Projects contain three practical ownership models: +GoForj Projects contain three practical ownership models: | Ownership | How to work with it | | --- | --- | @@ -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. diff --git a/docs/core/dependency-injection.md b/docs/core/dependency-injection.md index ee07132..6ab27f5 100644 --- a/docs/core/dependency-injection.md +++ b/docs/core/dependency-injection.md @@ -77,6 +77,36 @@ app/routes.go App-owned route composition Additional apps use the same shape under `app//wire/` and `app//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 [app/wire/inject_services_app.go] +var appSet = wire.NewSet( + reports.NewService, + app.NewLifecycleRegistry, + runtime.NewTimeouts, +) +``` + + +```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. @@ -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 [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 [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. diff --git a/docs/core/local-first-development.md b/docs/core/local-first-development.md index b760921..71e4fe1 100644 --- a/docs/core/local-first-development.md +++ b/docs/core/local-first-development.md @@ -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: @@ -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 @@ -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. diff --git a/docs/core/named-resources.md b/docs/core/named-resources.md index d6a47e7..b6b1b52 100644 --- a/docs/core/named-resources.md +++ b/docs/core/named-resources.md @@ -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
workerpool] --> driver[Selected queue driver] + prod[Production config
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 @@ -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 +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. @@ -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. diff --git a/docs/data/database-strategy.md b/docs/data/database-strategy.md index cd16e62..b560fcf 100644 --- a/docs/data/database-strategy.md +++ b/docs/data/database-strategy.md @@ -1,13 +1,13 @@ --- -title: Database Strategy +title: Database Connections description: How GoForj Apps model database connections, driver support, and durable data ownership. --- -# Database Strategy +# Database Connections Database connections are the source-of-truth path for durable relational data in a GoForj App. -GoForj keeps database configuration explicit and generated. The generated database package opens and caches connections on first access through its connection registry. +GoForj keeps database configuration explicit. The Framework-managed `internal/database` connection registry opens and caches connections on first access. ## Open the Default Connection @@ -34,7 +34,7 @@ db, err := conns.Default() The connection opens on first access and is then cached by name. -## Generated Package +## Database Package Database connection behavior lives in: @@ -42,12 +42,12 @@ Database connection behavior lives in: internal/database ``` -The generated package owns: +The database package owns: - database connection configuration - first-access connection opening - default and named connection access -- driver-specific generated support +- driver-specific support produced during the build - local database README guidance ## Default Connection @@ -102,8 +102,6 @@ Use health and readiness checks to make required database availability visible f ## Shell Options -Database-enabled Apps also expose the canonical command name: - Use the canonical command when you want the full name: ```bash diff --git a/docs/data/index.md b/docs/data/index.md index 79b43ef..eaba500 100644 --- a/docs/data/index.md +++ b/docs/data/index.md @@ -13,7 +13,7 @@ Use these guides to keep source-of-truth records, derived data, and files separa | Task | Read | | --- | --- | -| Select, configure, or inspect a database | [Database Strategy](/data/database-strategy) | +| Select, configure, or inspect a database | [Database Connections](/data/database-strategy) | | Change schema safely | [Migrations](/data/migrations) | | Own and test database queries | [Repositories](/data/repositories) | | Coordinate durable writes | [Transactions](/data/transactions) | diff --git a/docs/data/migrations.md b/docs/data/migrations.md index 1fb1d81..f405e7e 100644 --- a/docs/data/migrations.md +++ b/docs/data/migrations.md @@ -106,7 +106,18 @@ forj admin migrate --connection archive The first command runs every migration stream under `migrations/admin/*`. The second runs only `migrations/admin/archive`. -If two apps share one physical database, pick one app to own that database's migration stream. Do not duplicate the same schema history under two app directories. The `analytics` connection directory maps to `DB_ANALYTICS_*`. +Migration streams map to the generated flat connection registry: + +| Migration stream | Database configuration | +| --- | --- | +| `migrations/app/default/*` | Default `DB_*` connection | +| `migrations/app/analytics/*` | `DB_ANALYTICS_*` | +| `migrations/admin/default/*` | `DB_ADMIN_*` | +| `migrations/admin/archive/*` | `DB_ADMIN_ARCHIVE_*` | + +Adding another App expands the original App's streams beneath `migrations/app/` so every migration has an explicit App and connection owner. The additional App's name becomes part of its database connection name even when the migration stream is named `default`. + +If two apps share one physical database, pick one app to own that database's migration stream. Do not duplicate the same schema history under two app directories. Migration records use a unique migration name within each physical database, so migration filenames across App streams that intentionally share one database must also remain unique. ## Migration Table @@ -159,6 +170,7 @@ Do not use rollback as the first production recovery test. Exercise each new dow ## Next Steps -- [Database Strategy](/data/database-strategy) explains connection configuration. +- [Database Connections](/data/database-strategy) explains connection configuration. - [Repositories](/data/repositories) explains where query code should live. - [Testing Overview](/testing/#choose-a-test-layer) explains GoForj App testing direction. +- [`make:migration` Reference](/reference/make-commands#make-migration) lists connection selection, removal, and shared options. diff --git a/docs/data/repositories.md b/docs/data/repositories.md index 6c86afe..cf3e889 100644 --- a/docs/data/repositories.md +++ b/docs/data/repositories.md @@ -87,5 +87,5 @@ Success means the focused repository behavior and every package using its servic - [Transactions](/data/transactions) explains consistency boundaries. - [Application Services](/applications/services) explains service orchestration. -- [Database Strategy](/data/database-strategy) explains generated connections. +- [Database Connections](/data/database-strategy) explains generated connections. - [Integration Tests](/testing/integration-tests) covers Driver-specific persistence behavior. diff --git a/docs/developer-tools/atlas.md b/docs/developer-tools/atlas.md index e935e87..5af1f30 100644 --- a/docs/developer-tools/atlas.md +++ b/docs/developer-tools/atlas.md @@ -7,8 +7,14 @@ description: Agent support for GoForj projects, including local guidance, skills Atlas gives AI coding agents enough local project context to work inside a GoForj App without guessing at framework conventions. +

GoForj Atlas — a map for your coding agent

+ It is optional, but first-class. During `forj new`, the `Atlas - Agent Support` step can install agent guidance for the tools you use. Atlas can also be added later from an existing project. +If you are building a GoForj Project, use this guide and the `forj atlas:*` commands. That CLI path installs the right project files and runs Atlas without requiring a separate Atlas binary. Use the [standalone Atlas library reference](/atlas) only when embedding Atlas in another Go program or contributing to its Go module. + +Atlas starts with read-only project inspection. GoForj make commands remain the write path for framework scaffolding, so agent context does not become an arbitrary shell or file-mutation API. + ## Install and Verify During project creation, run the wizard: @@ -35,7 +41,7 @@ forj atlas:install --dry-run forj atlas:update --dry-run ``` -## What Atlas adds +## What Atlas Adds Atlas installs lightweight project files that teach agents the GoForj way to build: @@ -47,7 +53,14 @@ Atlas installs lightweight project files that teach agents the GoForj way to bui The goal is not to make agents louder. The goal is to make them less surprising. -## Project-owned skills +| Layer | What It Gives the Agent | +| --- | --- | +| Guidance | Project-wide GoForj conventions in the agent's native instruction file. | +| Skills | Focused workflows selected for the Project's components, Apps, and starter kit. | +| MCP context | Read-only project layout, ownership, docs, routes, resources, runtime evidence, and validation plans. | +| Project skills | Team-specific rules under `.ai/skills` that Atlas synchronizes into each selected agent's native format. | + +## Project-Owned Skills Atlas ships with built-in GoForj skills, but your project can add its own. @@ -113,7 +126,7 @@ Choose individual agents and surfaces when you want a smaller or more explicit i forj atlas:install --agent codex --agent copilot --agent gemini --guidelines --skills --mcp ``` -## Supported agents +## Supported Agents Atlas is designed around local project files and editor-readable instructions, so the same project can support multiple agents: @@ -133,7 +146,7 @@ Atlas writes each agent's native project files: | GitHub Copilot | `.github/copilot-instructions.md` | `.github/instructions/*.instructions.md` | `.vscode/mcp.json` | | Gemini CLI | `GEMINI.md` | `.gemini/skills/*/GEMINI.md` | `.gemini/settings.json` | -## MCP context +## MCP Context Atlas can expose GoForj context through an MCP server. The MCP server loads docs and project metadata locally, then serves focused slices of context to the agent. @@ -163,7 +176,7 @@ Atlas reports the active docs version and revision through `application-info` an Use `version-alignment` when an agent needs to compare the project GoForj version, Atlas version, and active docs bundle before following docs from a branch or release. -## Workflow skills +## Workflow Skills Atlas installs workflow skills for high-leverage GoForj changes. They are short, task-focused guides that tell an agent which app owns the change, which `forj make:*` command to prefer, which generated files not to edit by hand, which docs sections to read, and which validation commands prove the work. @@ -182,7 +195,9 @@ Built-in workflow skills cover: Atlas also includes starter-kit overlays for Vue, React, and templ/htmx projects. When a frontend task touches pages, screens, dashboards, login, auth, or UI behavior, the workflow plan can point the agent at the matching starter-kit skill so edits stay in the owning app's frontend tree. -## Agent workflow examples +Skills are entry points into verified workflows, not a second copy of the documentation. They direct the agent to bounded docs sections, current project evidence, file-ownership policy, and validation appropriate to the task. + +## Agent Workflow Examples Agents should use Atlas tools together instead of reading the whole docs site or guessing from filenames. @@ -238,13 +253,15 @@ browser-logs app="app" limit=50 That gives the agent app/runtime identity, local URLs, recent logs, metrics labels, browser errors, and known operator resources before code changes begin. +The runtime evidence tools do not require the Lighthouse browser UI to be open. When Lighthouse-backed local evidence exists, Atlas can include its links or records; when logs, browser entries, routes, URLs, or metrics targets are absent, it reports the missing evidence instead of inventing it. + For human-readable versions of common evidence loops, see [Atlas Debug Recipes](/developer-tools/atlas-debug-recipes). `runtime-snapshot` and `debug-plan` are evidence tools. They report missing logs, URLs, routes, browser entries, metrics targets, or resource links instead of inventing values. `generated-file-policy` reports classification, preferred action, and ownership for generated files, app-owned files, app-specific files, migrations, frontend files, config, docs, and unknown paths. Projects can override ownership rules in `.goforj/atlas.json`. -## Daily use +## Daily Use Most users do not need to run Atlas commands every day. Once installed, your agent reads the local guidance files and, when configured, asks the MCP server for focused docs context. diff --git a/docs/developer-tools/forj-dev.md b/docs/developer-tools/forj-dev.md index ffbc3d3..41e6006 100644 --- a/docs/developer-tools/forj-dev.md +++ b/docs/developer-tools/forj-dev.md @@ -24,6 +24,10 @@ flowchart LR In a GoForj Project, this can bring up local dependencies, prepare the database, build the frontend, compile the App, start its Runtime, and watch the files that feed each step. You do not need to keep separate build, frontend, and server commands synchronized in different terminals. +::: warning Development only +`forj dev` is a local development supervisor. Do not run it as a production process manager or deploy its watcher lifecycle. Production supervisors should execute the built artifact, such as `./bin/app`, `./bin/app api`, `./bin/app worker`, or `./bin/app scheduler`. +::: + When a file changes, `forj dev` reruns only the affected work. It replaces the running App after a successful build. If the build fails, the last working Runtime stays up while the error remains visible in the transcript. Fix the error, save again, and the loop continues. ## Start the Development Loop @@ -237,7 +241,16 @@ dev: down_on_exit: true ``` -Startup first runs configured App bootstrap builds so pre-tasks can call built App commands. It then runs `dev.pre`, performs configured database setup and auto-migration, and runs any generated tasks deliberately ordered after migration. Finally, it builds App-owned SPAs, rebuilds their Apps, and starts persistent watcher and runtime processes. +For a modern `dev.apps` configuration whose setup tasks match GoForj's generated conventions, startup follows this order: + +1. Run conventional setup tasks that do not require an App binary, such as starting Docker Compose and waiting for its database. +2. Build each App-owned SPA. +3. Build the participating Apps once. +4. Prepare configured development databases and run auto-migration. +5. Run generated tasks that must follow migration, then rebuild when those tasks changed generated source. +6. Start the persistent watchers and App runtime processes. + +This ordering gives the migration step a current App binary while ensuring that embedded frontend assets are already present in that binary. If custom or legacy `dev.pre` tasks do not match the generated setup phases, GoForj preserves their historical ordering and may perform an earlier bootstrap build plus a post-setup rebuild. Keep binary-dependent custom setup explicit instead of relying on a generated task name to change its phase. For npm-backed starter kits, new Projects generate this dependency setup task: diff --git a/docs/developer-tools/wiring-recipes.md b/docs/developer-tools/wiring-recipes.md index fbf8d8f..696a9e6 100644 --- a/docs/developer-tools/wiring-recipes.md +++ b/docs/developer-tools/wiring-recipes.md @@ -27,6 +27,8 @@ For an additional app, replace `app/...` with the owning app's `app//...`. Use the most specific generated set that owns the surface. If a generated file is not present, the app probably does not have that component enabled. +The `inject_*_app.go` files are App-owned extension points. GoForj creates them with the App and make commands can update them, but normal Project regeneration preserves your edits. In contrast, `app/wire/app.go` and `app/wire/wire_gen.go` are framework-generated output and should not be edited. + ## Generated Resources When a resource has a make command, use it before editing provider sets by hand. The command creates the resource and updates the active App's generated wiring boundaries. @@ -75,6 +77,23 @@ The [controller verification workflow](/applications/controllers#verify-the-resu The controller can depend on an application service already provided by the app service set. If Wire cannot provide that service, add the service constructor to `app/wire/inject_services_app.go`. +The controller injector uses its own set rather than `appSet`: + + +```go +package wire + +import ( + "github.com/goforj/wire" + + "myapp/internal/users" +) + +var appHttpControllerSet = wire.NewSet( + users.NewController, +) +``` + Verify the result: ```bash @@ -88,7 +107,25 @@ The [command creation workflow](/applications/commands#create-a-command) shows t Command constructors should receive application services as parameters. They should not create repositories, managers, clients, or services themselves. -Commands also need to be exposed through the generated command collection. See [Commands](/applications/commands) for the command-specific registration path. +Commands need both an App-owned provider and an App-owned CLI field. `forj make:command` updates both locations. For a manually written command, edit both files: + + +```go +// app/wire/inject_cmd_app.go +var appCommandSet = wire.NewSet( + reports.NewReconcileCmd, +) +``` + + +```go +// app/commands.go +type Commands struct { + ReconcileCmd reports.ReconcileCmd `cmd:""` +} +``` + +The existing `NewCommands` constructor in `app/commands.go` must also accept the injected pointer and copy it into the collection. See [Commands](/applications/commands) for the complete registration path. ## Named Resource @@ -132,6 +169,7 @@ forj build ::: warning Common mistakes - Do not add constructors to `app/wire/wire_gen.go`; it is generated output. +- Do not edit `app/wire/app.go`; it is Framework-managed App assembly. - Do not register a controller in the service set when it belongs in the HTTP controller set. - Do not create dependencies inside commands or controllers when they should be constructor parameters. - Do not use package globals to avoid wiring a provider. diff --git a/docs/drivers.md b/docs/drivers.md index 0ed50aa..6b25e6b 100644 --- a/docs/drivers.md +++ b/docs/drivers.md @@ -113,7 +113,7 @@ Three drivers behind generated connections and the ORM. Dialect differences live | `postgres` | Production relational database | | `mysql` | Production relational database and the current `forj new` default | -Details: [database strategy](/data/database-strategy) · [migrations](/data/migrations) +Details: [database connections](/data/database-strategy) · [migrations](/data/migrations) ## How Selection Works diff --git a/docs/frontend/react-starter-kit.md b/docs/frontend/react-starter-kit.md index 64e7a96..16ed60b 100644 --- a/docs/frontend/react-starter-kit.md +++ b/docs/frontend/react-starter-kit.md @@ -5,7 +5,7 @@ description: How the React starter kit is generated, owned, built, and served in # React Starter Kit -The React starter kit is a generated client-side application shell for Apps with Web UI enabled. +The React starter kit gives Apps with Web UI enabled an App-owned client-side shell. It uses React 19, Vite, TypeScript, Tailwind CSS, shadcn/ui, and React Router. Its product surface matches the Vue kit so choosing a frontend framework does not create a different GoForj application model. @@ -113,3 +113,4 @@ Its frontend lives in `cmd/admin/frontend/`. App-specific frontend variables use - [HTTP Services](/applications/http-services) explains the backend API boundary. - [Auth](/security/auth) explains generated browser authentication. - [forj dev](/developer-tools/forj-dev) explains the coordinated development loop. +- [`make:app` Reference](/reference/make-commands#make-app) lists starter-kit and component options. diff --git a/docs/frontend/templ-htmx-starter-kit.md b/docs/frontend/templ-htmx-starter-kit.md index 936964a..fee520b 100644 --- a/docs/frontend/templ-htmx-starter-kit.md +++ b/docs/frontend/templ-htmx-starter-kit.md @@ -307,9 +307,12 @@ forj make:app admin --components web-api,web-ui --starter-kit templ_htmx Expected result: the additional app receives its own assets under `cmd/admin/frontend/`. The server-rendered starter remains the shared `internal/starterui` package; the command does not create a second app-local copy of those views and controllers. +That shared package is wired into each App that selects templ + htmx. The first scaffold creates it, and adding another App does not produce an independently configurable server-rendered starter or overwrite the existing views. This is suitable when the Apps intentionally share the same page/controller implementation while owning separate compiled frontend assets. If the Apps need different server-rendered UIs, move or copy the relevant controllers and views into explicitly App-owned packages and wire each App to its own package. + ## Next Steps - [Controllers](/applications/controllers) explains route registration and dependency injection. - [Requests and Validation](/applications/requests-validation) covers form input. - [Auth](/security/auth) explains session and browser authentication. - [forj dev](/developer-tools/forj-dev) explains the coordinated development loop. +- [`make:app` Reference](/reference/make-commands#make-app) lists starter-kit and component options. diff --git a/docs/frontend/vue-starter-kit.md b/docs/frontend/vue-starter-kit.md index 569ce0d..e9f8f66 100644 --- a/docs/frontend/vue-starter-kit.md +++ b/docs/frontend/vue-starter-kit.md @@ -111,3 +111,4 @@ Its frontend lives in `cmd/admin/frontend/`. - [Choose a Starter Kit](/getting-started/starter-kits) - [forj dev](/developer-tools/forj-dev) - [HTTP Server](/operations/http-server) +- [`make:app` Reference](/reference/make-commands#make-app) lists starter-kit and component options. diff --git a/docs/getting-started/configuration.md b/docs/getting-started/configuration.md index 5b9b348..0d68cf6 100644 --- a/docs/getting-started/configuration.md +++ b/docs/getting-started/configuration.md @@ -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 diff --git a/docs/getting-started/project-structure.md b/docs/getting-started/project-structure.md index 24e7385..34298ca 100644 --- a/docs/getting-started/project-structure.md +++ b/docs/getting-started/project-structure.md @@ -77,7 +77,7 @@ The exact tree depends on the components selected when the Project is created. S └── bin/ Compiled app binaries [generated] ``` -```text [Multi-App] +```diff [Multi-App — highlighted lines differ] . ├── .goforj.yml Project shape, apps, and local dev lifecycles ├── .env, .env.host Shared and App-prefixed runtime configuration @@ -88,9 +88,9 @@ The exact tree depends on the components selected when the Project is created. S │ ├── app/ │ │ ├── main.go Default App binary entrypoint │ │ └── frontend/ Default App starter kit [Web UI] -│ └── admin/ -│ ├── main.go Admin App binary entrypoint -│ └── frontend/ Admin App starter kit [Web UI] ++│ └── admin/ ++│ ├── main.go Admin App binary entrypoint ++│ └── frontend/ Admin App starter kit [Web UI] │ ├── app/ │ ├── commands.go Default App command exposure @@ -104,17 +104,17 @@ The exact tree depends on the components selected when the Project is created. S │ │ ├── wire.go Framework-managed Wire declaration │ │ └── wire_gen.go Generated dependency graph │ │ -│ └── admin/ -│ ├── commands.go Admin App command exposure -│ ├── lifecycle.go Admin App lifecycle hooks -│ ├── routes.go Admin App HTTP exposure [Web API or UI] -│ ├── schedules.go Admin App schedule registry [Scheduler] -│ ├── root_cmd.go Admin App command assembly -│ └── wire/ Admin App dependency graph -│ ├── inject_*_app.go Admin App-owned providers -│ ├── inject_*.go Framework-managed provider assembly -│ ├── wire.go Framework-managed Wire declaration -│ └── wire_gen.go Generated dependency graph ++│ └── admin/ ++│ ├── commands.go Admin App command exposure ++│ ├── lifecycle.go Admin App lifecycle hooks ++│ ├── routes.go Admin App HTTP exposure [Web API or UI] ++│ ├── schedules.go Admin App schedule registry [Scheduler] ++│ ├── root_cmd.go Admin App command assembly ++│ └── wire/ Admin App dependency graph ++│ ├── inject_*_app.go Admin App-owned providers ++│ ├── inject_*.go Framework-managed provider assembly ++│ ├── wire.go Framework-managed Wire declaration ++│ └── wire_gen.go Generated dependency graph │ ├── internal/ │ ├── reports/ Domain behavior shared by either App @@ -125,18 +125,55 @@ The exact tree depends on the components selected when the Project is created. S │ ├── schedules/ Scheduler runtime and scheduled work │ └── caches, queues, storages, ... Generated resource support [by component] │ -├── migrations/ Shared database migrations [Database] +├── migrations/ ++│ ├── app/default/ Default App migration stream [Database] ++│ └── admin/default/ Admin App migration stream [Database] ├── build/ Per-App API and OpenAPI output [generated] └── bin/ ├── app Compiled default App binary [generated] - └── admin Compiled admin App binary [generated] ++ └── admin Compiled admin App binary [generated] ``` ::: Paths marked with a component appear only when that component is enabled. Generated output appears after the relevant render, generation, frontend, or build step. -Both layouts keep behavior under `internal/`. Each App has its own entrypoint, registration files, lifecycle hooks, and Wire graph, so it can expose a different subset of that shared behavior. +The highlighted lines are the additional ownership boundaries introduced by `admin`. Both layouts keep behavior under `internal/`. Each App has its own entrypoint, registration files, lifecycle hooks, Wire graph, API artifacts, and migration streams, so it can expose a different subset of that shared behavior. + +## Multiple Apps and SPAs + +A common multi-App Project gives its public and administrative Apps separate frontends: + +```text +shared internal packages +├── default App → cmd/app/frontend/ public SPA +└── admin App → cmd/admin/frontend/ administrative SPA +``` + +Each SPA builds before its owning App is rebuilt, and each App embeds and deploys its own frontend output. A change under `cmd/admin/frontend/` does not need to rebuild the default App. + +One App can also coordinate more than one SPA during development: + +```yaml +dev: + apps: + app: + spas: + storefront: + path: ./cmd/app/frontend + build: npm run build + documentation: + path: ./ui/documentation + build: npm run build +``` + +`forj dev` waits for successful SPA builds before replacing the owning App. Additional SPA entries are development build relationships; configure how their output is served or deployed as part of the App's own frontend architecture. See the [default App lifecycle](/developer-tools/forj-dev#default-app-lifecycle). + +## Growing the Project + +Keeping reusable workflows in `internal/` makes growth incremental. If both the default and `admin` Apps need reports, they can inject the same `reports.Service` constructor into separate Wire graphs without duplicating the workflow. Each binary receives its own service instance and chooses its own controllers, commands, or schedules. + +That boundary also makes a later service split less disruptive: move the domain package behind a new App first, make its inputs and outputs explicit, then extract it into another module or repository only when deployment ownership requires it. The `internal` rule prevents packages outside the Project's module tree from importing the code directly, so extraction is still an intentional move rather than an accidental distributed dependency. ## Where Common Changes Go @@ -144,18 +181,18 @@ Use the owning package for implementation and the app layer for exposure or depe | Change | Behavior belongs in | Exposure or wiring | | --- | --- | --- | -| HTTP controller | `internal//controller.go` | `app/routes.go`, `app/wire/inject_http_controllers_app.go` | -| Application service | `internal//service.go` | `app/wire/inject_services_app.go` | -| Repository or model | `internal/models/`, or `internal//` when grouped | `app/wire/inject_repositories_app.go` | -| App command | `internal/cmd/`, or `internal//` when grouped | `app/commands.go`, `app/wire/inject_cmd_app.go` | -| Queue job | `internal/jobs/`, or `internal//` when grouped | `app/wire/inject_jobs_app.go` | -| Generated schedule | `internal/schedules/`, or `internal//` when grouped | `app/wire/inject_schedules_app.go` | -| Custom schedule | The domain method that performs the work | `app/schedules.go` | -| Startup or shutdown hook | The owning service when behavior is reusable | `app/lifecycle.go` | -| Starter-kit frontend | `cmd/app/frontend/` | Embedded by `cmd/app/main.go`; review the ownership note below before rerendering | -| Migration | `migrations/` | Run through the app's migration commands | - -The `forj make:*` commands create the common files and update their registration points together. Use the [Make Command Reference](/reference/make-commands) for exact output and registration changes, then use the linked application, data, or async guide to implement the behavior itself. +| [HTTP controller](/applications/controllers) | `internal//controller.go` | `app/routes.go`, `app/wire/inject_http_controllers_app.go` | +| [Application service](/core/dependency-injection) | `internal//service.go` | `app/wire/inject_services_app.go` | +| [Repository or model](/data/repositories) | `internal/models/`, or `internal//` when grouped | `app/wire/inject_repositories_app.go` | +| [App command](/applications/commands) | `internal/cmd/`, or `internal//` when grouped | `app/commands.go`, `app/wire/inject_cmd_app.go` | +| [Queue job](/async/jobs) | `internal/jobs/`, or `internal//` when grouped | `app/wire/inject_jobs_app.go` | +| [Generated schedule](/async/scheduler) | `internal/schedules/`, or `internal//` when grouped | `app/wire/inject_schedules_app.go` | +| [Custom schedule](/async/scheduler) | The domain method that performs the work | `app/schedules.go` | +| [Startup or shutdown hook](/core/app-lifecycle) | The owning service when behavior is reusable | `app/lifecycle.go` | +| [Starter-kit frontend](/starter-kits) | `cmd/app/frontend/`; `cmd//frontend/` for an additional App | Embedded by that App's `cmd//main.go`; review the ownership note below before rerendering | +| [Migration](/data/migrations) | `migrations/`; `migrations///` after multi-App expansion | Run through the owning App's migration commands | + +The `forj make:*` commands create the common files and update their registration points together. Use the [Make Command Reference](/reference/make-commands) for exact output and registration changes, including how `--open` and `FORJ_EDITOR` open created source files. The paths above describe files in your Project, while each change type links to the guide that explains how to use it. Framework contributors can inspect the [authoritative GoForj templates](https://github.com/goforj/goforj/tree/main/templates) separately without confusing template source with application-owned files. ## Which Files Can I Edit? @@ -193,7 +230,7 @@ Starter-kit and demo source has a different lifecycle from ordinary application Review the [Starter Kit Guide](/getting-started/starter-kits) before customizing these paths or running `forj render`. Move durable application behavior into your own packages instead of relying on demo scaffold ownership. -### Generated and Build Output +### Tool-Owned and Build Output Do not edit derived output by hand: @@ -203,7 +240,7 @@ Do not edit derived output by hand: - `bin/` - frontend `dist/` -Change the source, provider, or configuration and rebuild. [Generated Files](/reference/generated-files) lists the important generated paths and the command that owns each one. +Change the source, provider, or configuration and rebuild. [File Ownership](/reference/generated-files) lists the important generated paths and the command that owns each one. ## Configuration at the Project Root @@ -230,7 +267,7 @@ Selected components add focused packages under `internal/`: | --- | --- | --- | | Web API or Web UI | `internal/http`, `app/routes.go` | [HTTP Services](/applications/http-services) | | Web UI starter kit | `cmd/app/frontend/` | [Starter Kit Guide](/getting-started/starter-kits) | -| Database | `internal/database`, `migrations/` | [Database Strategy](/data/database-strategy) | +| Database | `internal/database`, `migrations/` | [Database Connections](/data/database-strategy) | | Cache | `internal/caches` | [Cache Patterns](/data/cache-patterns) | | File Storage | `internal/storages` | [Storage Patterns](/data/storage-patterns) | | Background Jobs | `internal/queues`, `internal/jobs` | [Queues](/async/queues) and [Jobs](/async/jobs) | @@ -273,5 +310,5 @@ This refreshes generated accessors, runs Wire, updates the API index, and compil - [JSON API Route](/scenarios/json-api-route) applies this structure to a controller, service, provider, route, and test. - [Configuration](/getting-started/configuration) explains Project and runtime settings. - [Apps](/core/apps) covers additional runnable apps. -- [Generated Files](/reference/generated-files) identifies generated ownership and regeneration commands. +- [File Ownership](/reference/generated-files) identifies ownership and regeneration commands. - [App Lifecycle](/core/app-lifecycle) explains startup and shutdown. diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md index fda465b..fb6d08d 100644 --- a/docs/getting-started/quickstart.md +++ b/docs/getting-started/quickstart.md @@ -87,7 +87,7 @@ go test ./... A successful run ends with `ok` lines or `[no test files]` for each generated package and no `FAIL` line. -You now have a generated Project, a running app, a verified HTTP endpoint, and a passing test suite. +You now have a GoForj Project, a running App, a verified HTTP endpoint, and a passing test suite. ## Next Steps diff --git a/docs/index.md b/docs/index.md index 72297a2..90c4e27 100644 --- a/docs/index.md +++ b/docs/index.md @@ -87,7 +87,7 @@ function setBinTab(id) { } const CAPABILITIES = [ - { title: 'HTTP services', icon: 'globe', copy: 'Thin controllers, route groups, and middleware over the web abstraction. Health, readiness, and Swagger included.', href: '/applications/http-services' }, + { title: 'HTTP services', icon: 'globe', copy: 'Thin controllers, route groups, and middleware over the web abstraction. Health, readiness, and an OpenAPI reference included.', href: '/applications/http-services' }, { title: 'Commands', icon: 'terminal', copy: 'First-class CLI entry points with injected dependencies, not shell scripts around your binary.', href: '/applications/commands' }, { title: 'Queues and jobs', icon: 'rows-3', copy: 'Named, durable background work with typed payloads, retries, timeouts, and worker processes.', href: '/async/queues' }, { title: 'Events', icon: 'radio', copy: 'Typed facts with local-first fan-out. In-process today, NATS or Kafka when you need it.', href: '/async/events' }, diff --git a/docs/libraries/atlas.md b/docs/libraries/atlas.md index 1220aad..b8a3a95 100644 --- a/docs/libraries/atlas.md +++ b/docs/libraries/atlas.md @@ -8,7 +8,7 @@ repoUrl: https://github.com/goforj/atlas # GoForj Atlas {#goforj-atlas}

- Atlas Banner + GoForj Atlas — a map for your coding agent

Agent-native project navigation and MCP tooling for GoForj. @@ -27,7 +27,8 @@ forj atlas:mcp This repository contains the reusable Atlas library. The GoForj CLI exposes it through `forj atlas:*` commands so projects do not need to install a separate -binary. +binary. For installation and daily use in a rendered GoForj Project, use the +[Atlas framework guide](https://goforj.dev/developer-tools/atlas). ## What Atlas Provides {#what-atlas-provides} @@ -49,7 +50,7 @@ When source scaffolding is needed, agents should use normal GoForj commands: ```bash forj make:controller users -forj marketplace make:job sync-catalog +forj admin make:job reconcile-refunds ``` ## Development {#development} diff --git a/docs/libraries/cache.md b/docs/libraries/cache.md index bbb5d71..86a8551 100644 --- a/docs/libraries/cache.md +++ b/docs/libraries/cache.md @@ -49,8 +49,6 @@ go get github.com/goforj/cache/driver/mysqlcache ## Drivers {#drivers} -Each driver is thoroughly tested against the shared test suite using [testcontainers](https://testcontainers.com/) or emulators where appropriate. - | Driver / Backend | Mode | Shared | Durable | TTL | Counters | Locks | RateLimit | Prefix | Batch | Shaping | Notes | |-------------------------------------------------------------------------------------------------------------:| :--- | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :--- | | Null | No-op | - | - | - | - | No-op | No-op | ✓ | ✓ | ✓ | Great for tests: cache calls are no-ops and never persist. | @@ -1399,7 +1397,11 @@ fmt.Println(c.SetString("user:42:name", "Ada", time.Minute) == nil) // true Default integration runs cover the contract suite above. Fault/recovery restart tests run automatically when the selected integration suite includes container-backed fixtures. -## Contributing (README updates) {#contributing-(readme-updates)} +## Development {#development} + +Use `make test` for root-module tests, `make vet` for static checks, `make generate` to refresh generated documentation, and `make integration` for the separate integration module. Integration runs may require local services. Driver, docs, examples, and integration directories are independent Go modules; test each changed module from its directory. + +### README updates {#readme-updates} README content is a mix of generated sections and manual sections. @@ -1446,4 +1448,4 @@ Notes: GoForj Apps expose named caches through generated accessors. Use those accessors in application services and keep backend selection in cache configuration. -For the GoForj integration, see [Cache Patterns](/data/cache-patterns). +For the App workflow, see [Cache Patterns](/data/cache-patterns). diff --git a/docs/libraries/collection.md b/docs/libraries/collection.md index 2f0fb43..045158a 100644 --- a/docs/libraries/collection.md +++ b/docs/libraries/collection.md @@ -4336,3 +4336,7 @@ collection.Dump(out3.Items()) // ] ``` + +## Development {#development} + +Use `make test` for the root module, `make vet` for static checks, and `make generate` to refresh the generated README API reference. The `docs` and `examples` directories are separate Go modules and can be tested from their own directories when changed. diff --git a/docs/libraries/console.md b/docs/libraries/console.md index 3d7ed8a..33b79e5 100644 --- a/docs/libraries/console.md +++ b/docs/libraries/console.md @@ -579,7 +579,7 @@ if err := loader.Start(); err != nil { } // · Uploading release loader.Fail("Registry refused upload") -// ✖ Registry refused upload +// ERROR Registry refused upload ``` #### Loader.Start {#loader-start} @@ -712,7 +712,7 @@ ErrorMark returns the default console's error indicator. ```go fmt.Println(console.ErrorMark()) -// ✖ +// ERROR ``` #### InfoMark {#infomark} @@ -791,7 +791,7 @@ Error prints an error message through the default console. ```go console.Error("deployment failed") -// ✖ deployment failed +// ERROR deployment failed ``` #### Errorf {#errorf} @@ -800,7 +800,7 @@ Errorf prints a formatted error message through the default console. ```go console.Errorf("deployment failed: %s", "timeout") -// ✖ deployment failed: timeout +// ERROR deployment failed: timeout ``` #### Fatal {#fatal} @@ -812,7 +812,7 @@ console.SetDefault(console.New(console.Config{ Exit: func(code int) { fmt.Println("exit", code) }, })) console.Fatal("invalid configuration") -// ✖ invalid configuration +// ERROR invalid configuration // exit 1 ``` @@ -825,7 +825,7 @@ console.SetDefault(console.New(console.Config{ Exit: func(code int) { fmt.Println("exit", code) }, })) console.Fatalf("invalid port: %d", 0) -// ✖ invalid port: 0 +// ERROR invalid port: 0 // exit 1 ``` @@ -1065,7 +1065,7 @@ if err := progress.Start(); err != nil { } // · Publishing release progress.Fail("Registry refused upload") -// ✖ Registry refused upload +// ERROR Registry refused upload ``` #### Progress.Set {#progress-set} @@ -1329,12 +1329,12 @@ fmt.Println(errors.Is(err, console.ErrNonInteractive)) #### ASCIIMarks {#asciimarks} -ASCIIMarks returns symbols suitable for constrained terminals and plain logs. +ASCIIMarks returns marks suitable for constrained terminals and plain logs. ```go marks := console.ASCIIMarks() fmt.Println(marks.Success, marks.Warn, marks.Error) -// + ! x +// + ! ERROR ``` #### Config {#config} @@ -1373,12 +1373,12 @@ fmt.Println(console.Default() != nil) #### DefaultMarks {#defaultmarks} -DefaultMarks returns the Unicode symbols used by a default console. +DefaultMarks returns the semantic marks used by a default console. ```go marks := console.DefaultMarks() fmt.Println(marks.Success, marks.Warn, marks.Error) -// ✔ ! ✖ +// ✔ ! ERROR ``` #### Marks {#marks} @@ -1962,7 +1962,7 @@ console.Success("API ready\nWorker ready") console.Warn("Configuration is incomplete") // ! Configuration is incomplete console.Error("Port already in use") -// ✖ Port already in use +// ERROR Port already in use ``` ### Plain output and coordinated writers {#plain-output-and-coordinated-writers} @@ -1980,7 +1980,7 @@ fmt.Fprintln(console.StderrWriter(), "diagnostic output") ```go fmt.Println(console.ActionMark(), console.SuccessMark(), console.ErrorMark()) -// · ✔ ✖ +// · ✔ ERROR fmt.Println(console.Style("release ready", console.StyleBold, console.ColorGreen)) // release ready ``` @@ -2111,7 +2111,7 @@ if err := publish.Start(); err != nil { // · Publishing release defer publish.Stop() publish.Fail("Registry refused upload") -// ✖ Registry refused upload +// ERROR Registry refused upload ``` ### Determinate progress {#determinate-progress} @@ -2269,7 +2269,7 @@ console.List("DATABASE_URL is missing", "PORT must be between 1 and 65535") // • DATABASE_URL is missing // • PORT must be between 1 and 65535 console.Error("Validation failed") -// ✖ Validation failed +// ERROR Validation failed ``` ### Recipe: machine stdout and status stderr {#recipe:-machine-stdout-and-status-stderr} @@ -2318,18 +2318,9 @@ fmt.Print(output.String()) ## Development {#development} -```sh -go test ./... -go test -race ./... -go -C docs test ./... -go -C examples test ./... -go generate . -go vet ./... -go -C docs vet ./... -go -C examples vet ./... -``` +`docs` and `examples` are separate Go modules so release archives contain only the library. -The docs and examples are separate Go modules so release archives contain only the library. The README API index uses a generator-owned grouping manifest, while each local API target and code sample is generated from the declaration's GoDoc `Example:` block. Focused workflow examples come from standard Go example tests that execute and verify their inline output. Generation validates its marker pair and every example target before writing, so malformed documentation fails without partially changing the README. +Use `make test`, `make test-race`, `make vet`, and `make generate`. The test and vet targets cover all three modules; generation rebuilds the README from its verified GoDoc examples. ## Documentation {#documentation} @@ -2341,8 +2332,8 @@ The docs and examples are separate Go modules so release archives contain only t Before tagging a release: -- Run the root, docs, and examples tests and vet commands above, including the race suite. -- Run `go generate .` and confirm the working tree has no generated or module-file diff. +- Run `make test`, `make test-race`, and `make vet`. +- Run `make generate` and confirm the working tree has no generated or module-file diff. - Choose the next semantic version and review the public API and README output one final time. - Create an annotated tag with `git tag -a vX.Y.Z -m "vX.Y.Z"`, then push it with `git push origin vX.Y.Z`. diff --git a/docs/libraries/crypt.md b/docs/libraries/crypt.md index 62bfbbb..0203f39 100644 --- a/docs/libraries/crypt.md +++ b/docs/libraries/crypt.md @@ -401,3 +401,7 @@ godump.Dump(err == nil, newKey != "") // #bool true ``` + +## Development {#development} + +Use `make test` for the root module, `make vet` for static checks, and `make generate` to refresh generated documentation. The `examples` directory is a separate Go module and can be tested from that directory when changed. The docs watcher remains available as `sh docs/watcher.sh`. diff --git a/docs/libraries/env.md b/docs/libraries/env.md index 88d9538..d127420 100644 --- a/docs/libraries/env.md +++ b/docs/libraries/env.md @@ -1093,6 +1093,10 @@ env.Dump( ``` +## Development {#development} + +Use `make test` for the root module, `make vet` for static checks, and `make generate` to refresh generated documentation. The `examples` directory is a separate Go module and can be tested from that directory when changed. + ## License {#license} MIT diff --git a/docs/libraries/events.md b/docs/libraries/events.md index c28cbd7..83644d1 100644 --- a/docs/libraries/events.md +++ b/docs/libraries/events.md @@ -91,8 +91,6 @@ go get github.com/goforj/events/driver/snsevents ## Drivers {#drivers} -Each driver is thoroughly tested against the shared test suite using [testcontainers](https://testcontainers.com/) or emulators where appropriate. - | Driver / Backend | Mode | Fan-out | Durable | Queue Semantics | Notes | |----------------------------------------------------------------------------------------------------------------:| :--- | :---: | :---: | :---: | :--- | | Sync | In-process | ✓ | x | x | Root-backed synchronous dispatch in the caller path. | @@ -646,18 +644,14 @@ fmt.Printf("%T\n", record.Event) ``` -## Docs Tooling {#docs-tooling} - -The repository includes lightweight docs tooling under `docs/`. +## Development {#development} -Run the watcher to auto-regenerate docs on file changes: +Use `make test` for root-module tests, `make vet` for static checks, `make generate` to refresh generated documentation, and `make integration` for the separate integration module. Integration runs may require local services. Driver, docs, examples, and integration directories are independent Go modules; test each changed module from its directory. -```bash -sh docs/watcher.sh -``` +The docs watcher remains available as `sh docs/watcher.sh`. ## Using with GoForj {#using-with-goforj} GoForj Apps expose named event buses through generated accessors. Publish through those accessors and keep driver selection in event configuration. -For the GoForj integration, see [Events](/async/events). +For the App workflow, see [Events](/async/events). diff --git a/docs/libraries/execx.md b/docs/libraries/execx.md index c295289..602ac4f 100644 --- a/docs/libraries/execx.md +++ b/docs/libraries/execx.md @@ -1167,14 +1167,6 @@ fmt.Println(out == dir) `docs` and `examples` are separate Go modules, keeping their tooling and generated programs out of the library module download. -Run the tests and rebuild the generated examples and README with: - -```sh -go test ./... -go -C docs test ./... -go -C examples test ./... -go -C docs run ./examplegen -go -C docs run ./readme -``` +Use `make test`, `make test-race`, `make vet`, and `make generate`. The test and vet targets cover all three modules; generation rebuilds the examples and README. Licensed under the [MIT License](https://github.com/goforj/execx/blob/main/LICENSE). diff --git a/docs/libraries/godump.md b/docs/libraries/godump.md index cdfd46c..18ab4ae 100644 --- a/docs/libraries/godump.md +++ b/docs/libraries/godump.md @@ -763,3 +763,7 @@ d.Dump("hello") // "hello" #string ``` + +## Development {#development} + +Use `make test` for the root module, `make vet` for static checks, and `make modernize-check` to check for supported modernization opportunities. The `examples` directory is a separate Go module and can be tested from that directory when changed. diff --git a/docs/libraries/httpx.md b/docs/libraries/httpx.md index 55f803a..a03f25f 100644 --- a/docs/libraries/httpx.md +++ b/docs/libraries/httpx.md @@ -91,11 +91,9 @@ They are compiled and executed in CI to ensure the documentation stays accurate httpx v1 has been tagged and is now frozen. The current module path is `github.com/goforj/httpx/v2`, and the `main` branch includes intentional breaking changes to improve API clarity and ergonomics (for example, request helpers return `(T, error)`). -## Contributing {#contributing} +## Development {#development} -- Run `go run ./docs/examplegen/main.go` after updating doc examples. -- Run `go run ./docs/readme/main.go` to refresh the API index and test count. -- Run `go test ./...`. +Use `make test` for the test suite, `make vet` for static checks, and `make generate` to refresh generated documentation. Keep examples and documentation accurate with behavior changes. diff --git a/docs/libraries/index.md b/docs/libraries/index.md index 1a4f42d..5aa8cc3 100644 --- a/docs/libraries/index.md +++ b/docs/libraries/index.md @@ -1,11 +1,11 @@ --- title: Libraries -description: Standalone first-party GoForj libraries for Go services, CLIs, workers, and generated GoForj Apps. +description: Standalone first-party GoForj libraries for Go services, CLIs, workers, and GoForj Apps. --- # Libraries -GoForj Libraries are first-party Go packages that can be used on their own or composed inside a generated GoForj App. +GoForj Libraries are first-party Go packages that can be used on their own or composed inside a GoForj App. Each library page remains useful for standalone package users. Framework guides should link here for primitive APIs, driver details, constructors, and direct package usage. diff --git a/docs/libraries/mail.md b/docs/libraries/mail.md index 85212f9..66c01f6 100644 --- a/docs/libraries/mail.md +++ b/docs/libraries/mail.md @@ -19,6 +19,7 @@ repoUrl: https://github.com/goforj/mail CI Go version Latest tag + Go Report Card Codecov Unit tests (executed count) @@ -1066,15 +1067,12 @@ fmt.Println(fake.SentCount()) ``` -## Docs Tooling {#docs-tooling} +## Development {#development} -- `go run ./docs/examplegen/main.go` -- `go run ./docs/readme/main.go` -- `go run ./docs/readme/testcounts/main.go` -- `./docs/watcher.sh` +Use `make test` for root-module tests, `make vet` for static checks, and `make generate` to refresh generated documentation. The `docs`, `examples`, and `mailses` directories are separate Go modules and can be tested from their own directories when changed. The docs watcher remains available as `sh docs/watcher.sh`. ## Using with GoForj {#using-with-goforj} GoForj Apps expose named mailers through generated accessors. Send through those accessors and keep transport selection and credentials in configuration. -For the GoForj integration, see [Mail](/applications/mail). +For the App workflow, see [Mail](/applications/mail). diff --git a/docs/libraries/metrics.md b/docs/libraries/metrics.md index efe946c..77ae9fb 100644 --- a/docs/libraries/metrics.md +++ b/docs/libraries/metrics.md @@ -48,8 +48,10 @@ import ( ) func main() { + // Own the registry explicitly so tests and multiple applications do not share metric state. registry := metrics.NewRegistry() + // Register metrics once during startup, then retain these handles for inexpensive updates. requests := registry.MustCounter(metrics.Descriptor{ Name: "http.requests", Help: "Total HTTP requests served.", @@ -62,6 +64,7 @@ func main() { http.Handle("/metrics", metrics.Handler(registry)) http.HandleFunc("/hello", func(response http.ResponseWriter, _ *http.Request) { start := time.Now() + // A deferred observation records latency on every return path through the handler. defer latency.ObserveSince(start) requests.Inc() @@ -74,6 +77,15 @@ func main() { This exports `http_requests_total` and the `http_request_duration_seconds` histogram family at `/metrics`. +After starting the program, make one request and inspect the counter: + +```console +$ curl --silent http://localhost:8080/hello > /dev/null + +$ curl --silent http://localhost:8080/metrics | grep '^http_requests_total' +http_requests_total 1 +``` + ## Model {#model} Each application creates and wires one or more registries explicitly. There is no package-global registry. @@ -195,10 +207,14 @@ Registration, updates, snapshots, and exposition are safe to use concurrently. K - updates on the returned counter, gauge, or histogram child do not take the registry or vector lock; - snapshots are detached and may be retained or modified without changing live metrics. -Run the race-enabled test suite with: +## Development {#development} -```sh -GOCACHE=/tmp/gocache GOMODCACHE=/tmp/gomodcache go test -race ./... +Run the normal validation targets with: + +```bash +make test +make test-race +make vet ``` ## Upgrading {#upgrading} @@ -209,4 +225,4 @@ The quality-pass contract is stricter than v0.1.0. See [MIGRATING.md](https://gi GoForj Apps expose metrics through the observability and HTTP runtime. Keep registration close to the behavior being measured and configure scrape exposure through the App runtime. -For the GoForj integration, see [Metrics](/operations/metrics). +For the App workflow, see [Metrics](/operations/metrics). diff --git a/docs/libraries/queue.md b/docs/libraries/queue.md index 22ca702..c112ddb 100644 --- a/docs/libraries/queue.md +++ b/docs/libraries/queue.md @@ -76,8 +76,6 @@ func main() { ## Drivers {#drivers} -Each driver is thoroughly tested against the shared test suite using [testcontainers](https://testcontainers.com/) or emulators where appropriate. - | Driver / Backend | Mode | Notes | Durable | Async | Delay | Unique | Backoff | Timeout | Native Stats | Queue Admin | | ---: | :--- | :--- | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | | Null | Drop-only | Discards dispatched jobs; useful for disabled queue modes and smoke tests. | - | - | - | Instance | - | - | - | - | @@ -2474,34 +2472,14 @@ fmt.Println(q != nil) ``` -## Contributing {#contributing} - -### Testing {#testing} - -Unit tests (root module): - -```bash -go test ./... -``` - -Integration tests (separate `integration` module): +## Development {#development} -```bash -go test -tags=integration ./integration/... -``` - -Select specific backends with `INTEGRATION_BACKEND` (comma-separated), for example: - -```bash -INTEGRATION_BACKEND=sqlite go test -tags=integration ./integration/... -INTEGRATION_BACKEND=redis,rabbitmq go test -tags=integration ./integration/... -count=1 -INTEGRATION_BACKEND=all go test -tags=integration ./integration/... -count=1 -``` +Use `make test` for root-module tests, `make vet` for static checks, and `make generate` to refresh generated documentation. `make integration` runs the separate integration module; it honors the comma-separated `INTEGRATION_BACKEND` selector (for example, `INTEGRATION_BACKEND=sqlite make integration`) and may need local services. -Matrix status and backend integration notes are tracked in `docs/integration-scenarios.md`. +Driver, docs, examples, and integration directories are independent Go modules; test each changed module from its directory. Matrix status and backend notes are tracked in `docs/integration-scenarios.md`. ## Using with GoForj {#using-with-goforj} GoForj Apps expose named queues through generated accessors. Dispatch jobs through those accessors and keep backend selection in queue configuration. -For the GoForj integration, see [Queues](/async/queues). +For the App workflow, see [Queues](/async/queues). diff --git a/docs/libraries/scheduler.md b/docs/libraries/scheduler.md index df6a2cc..0f39c43 100644 --- a/docs/libraries/scheduler.md +++ b/docs/libraries/scheduler.md @@ -65,24 +65,24 @@ go get github.com/redis/go-redis/v9 ### Basic {#basic} ```go -s := scheduler.New() -defer s.Stop() +schedule := scheduler.New() +defer schedule.Stop() -s.EveryMinute().Name("cleanup").Do(func(context.Context) error { return runCleanup() }) // run in-process cleanup every minute -s.DailyAt("09:00").Weekdays().Name("reports:morning").Do(func(context.Context) error { return sendMorningReport() }) // weekdays at 09:00 -s.Cron("0 0 * * *").Command("reports:purge", "--force") // run app subcommand nightly -s.Cron("*/15 * * * *").Exec("/usr/bin/env", "echo", "heartbeat") // run external executable every 15 minutes -s.EveryFiveMinutes().WithoutOverlapping().Name("sync:inventory").Do(func(context.Context) error { return syncInventory() }) // prevent overlapping runs -s.Cron("0 * * * *").When(func() bool { return isPrimaryNode() }).Name("rebalance").Do(func(context.Context) error { return rebalance() }) // run only when condition passes +schedule.EveryMinute().Name("cleanup").Do(func(context.Context) error { return runCleanup() }) // run in-process cleanup every minute +schedule.DailyAt("09:00").Weekdays().Name("reports:morning").Do(func(context.Context) error { return sendMorningReport() }) // weekdays at 09:00 +schedule.Cron("0 0 * * *").Command("reports:purge", "--force") // run app subcommand nightly +schedule.Cron("*/15 * * * *").Exec("/usr/bin/env", "echo", "heartbeat") // run external executable every 15 minutes +schedule.EveryFiveMinutes().WithoutOverlapping().Name("sync:inventory").Do(func(context.Context) error { return syncInventory() }) // prevent overlapping runs +schedule.Cron("0 * * * *").When(func() bool { return isPrimaryNode() }).Name("rebalance").Do(func(context.Context) error { return rebalance() }) // run only when condition passes ``` ### Advanced (kitchen sink) {#advanced-(kitchen-sink)} ```go -s := scheduler.New() -defer s.Stop() +schedule := scheduler.New() +defer schedule.Stop() -s. +schedule. Name("reports:generate"). Timezone("America/New_York"). Weekdays(). @@ -94,7 +94,7 @@ s. DailyAt("10:30"). Do(func(context.Context) error { return generateReports() }) -s. +schedule. Name("reconcile:daily"). RunInBackground(). Cron("0 3 * * *"). @@ -108,33 +108,34 @@ package main import ( "context" + "github.com/goforj/scheduler/v2" ) func main() { - s := scheduler.New() - defer s.Stop() + schedule := scheduler.New() + defer schedule.Stop() - s.EveryMinute().Name("cleanup").Do(func(context.Context) error { return nil }) // run cleanup every minute - s.DailyAt("10:30").Weekdays().Name("reports:generate").Do(func(context.Context) error { return nil }) // run reports on weekdays at 10:30 - s.Cron("0 0 * * *").Command("reports:purge", "--force") // run app subcommand nightly at midnight + schedule.EveryMinute().Name("cleanup").Do(func(context.Context) error { return nil }) // run cleanup every minute + schedule.DailyAt("10:30").Weekdays().Name("reports:generate").Do(func(context.Context) error { return nil }) // run reports on weekdays at 10:30 + schedule.Cron("0 0 * * *").Command("reports:purge", "--force") // run app subcommand nightly at midnight - s.PrintJobsList() + schedule.PrintJobsList() } ``` Example output: ``` -+------------------------------------------------------------------------------------------------------------------------+ -| Scheduler Jobs › (3) | -+------------------+----------+----------------+-----------------------+----------------------+--------------------------+ -| Name | Type | Schedule | Handler | Next Run | Tags | -+------------------+----------+----------------+-----------------------+----------------------+--------------------------+ -| cleanup | function | every 1m | main.main (anon func) | in 1m Mar 3 2:16AM | env=local | -| reports:generate | function | cron 30 10 * * * | main.main (anon func) | in 8h Mar 3 10:30AM | env=local | -| reports:purge | command | cron 0 0 * * * | - | in 21h Mar 4 12:00AM | env=local, args="--force" | -+------------------+----------+----------------+-----------------------+----------------------+--------------------------+ ++---------------------------------------------------------------------------------------------------------------------------+ +| Scheduler Jobs › (3) | ++------------------+----------+------------------+-----------------------+----------------------+---------------------------+ +| Name | Type | Schedule | Handler | Next Run | Tags | ++------------------+----------+------------------+-----------------------+----------------------+---------------------------+ +| cleanup | function | every 1m | main.main (anon func) | in 1m Mar 3 2:16AM | env=local | +| reports:generate | function | cron 30 10 * * * | main.main (anon func) | in 8h Mar 3 10:30AM | env=local | +| reports:purge | command | cron 0 0 * * * | - | in 21h Mar 4 12:00AM | env=local, args="--force" | ++------------------+----------+------------------+-----------------------+----------------------+---------------------------+ ``` ## Runnable examples {#runnable-examples} @@ -1112,8 +1113,19 @@ scheduler.New().Name("cleanup").Cron("0 0 * * *").Do(func(context.Context) error ``` +## Development {#development} + +Use the repository targets so the API examples and README stay synchronized: + +```bash +make test +make test-race +make vet +make generate +``` + ## Using with GoForj {#using-with-goforj} GoForj Apps register schedules in the scheduler runtime and inject the jobs they run. Keep recurring business work in jobs instead of the schedule registry. -For the GoForj integration, see [Scheduler](/async/scheduler). +For the App workflow, see [Scheduler](/async/scheduler). diff --git a/docs/libraries/storage.md b/docs/libraries/storage.md index 053be1f..64ff61f 100644 --- a/docs/libraries/storage.md +++ b/docs/libraries/storage.md @@ -1375,36 +1375,12 @@ Current fixture types in the centralized matrix: - emulator: `gcs` - embedded/local fixtures: `local`, `ftp`, `rclone_local` -Common contributor commands: +## Development {#development} -```bash -go test ./... -``` - -```bash -cd integration -go test -tags=integration ./all -count=1 -``` - -Run a single integration backend: - -```bash -cd integration -INTEGRATION_DRIVER=gcs go test -tags=integration ./all -count=1 -``` - -Make targets: - -```bash -make test -make examples-test -make coverage -make integration -make integration-driver gcs -``` +Use `make test` for root-module tests, `make examples-test` for the examples module, and `make coverage` for the Codecov report. `make integration` runs the centralized matrix; use `make integration-driver gcs` to select one backend. Integration may require Docker. `make bench` and `make bench-render` retain the benchmark workflow, while the `check-modules`, `tag-modules`, `release-plan`, and `release-modules` targets support publication. ## Using with GoForj {#using-with-goforj} GoForj Apps expose named disks through generated accessors. Use those accessors in application services and keep backend selection in storage configuration. -For the GoForj integration, see [Storage Patterns](/data/storage-patterns). +For the App workflow, see [Storage Patterns](/data/storage-patterns). diff --git a/docs/libraries/strings.md b/docs/libraries/strings.md index 98f960f..60ac47c 100644 --- a/docs/libraries/strings.md +++ b/docs/libraries/strings.md @@ -1293,14 +1293,6 @@ println(v) `docs` and `examples` are separate Go modules, keeping their tooling and generated programs out of the library module download. -Run the tests and rebuild the generated examples and README with: - -```sh -go test ./... -go -C docs test ./... -go -C examples test ./... -go -C docs run ./examplegen -go -C docs run ./readme -``` +Use `make test`, `make test-race`, `make vet`, and `make generate`. The test and vet targets cover all three modules; generation rebuilds the examples and README. Licensed under the [MIT License](https://github.com/goforj/str/blob/main/LICENSE). diff --git a/docs/libraries/web.md b/docs/libraries/web.md index 241043c..bcbd2ae 100644 --- a/docs/libraries/web.md +++ b/docs/libraries/web.md @@ -90,7 +90,7 @@ The quick start owns the `http.Server` directly to keep the first example small. | Start with | Use it when | | --- | --- | | `echoweb.New()` and `router.GET(...)` | Routes are registered directly and your application owns the `http.Server`. | -| `web.NewRouteGroup(...)` and `web.RegisterRoutes(...)` | Routes should be reusable declarations for reporting, indexing, or App composition. | +| `web.NewRouteGroup(...)` and `web.RegisterRoutes(...)` | Routes should be reusable declarations for reporting, indexing, or framework-managed App composition. | | `echoweb.NewServer(...)` | The adapter should register route groups and own graceful HTTP shutdown. | | `echoweb.Wrap(engine)` | An existing Echo engine needs to expose the app-facing `web.Router` contract. | @@ -1804,8 +1804,18 @@ fmt.Println(ctx.Param("id"), ctx.Query("expand")) ``` +## Development {#development} + +Use the repository targets to validate every module and refresh executable documentation: + +```bash +make test +make vet +make generate +``` + ## Using with GoForj {#using-with-goforj} GoForj Apps register web routes and controllers through the HTTP runtime. Keep server wiring in framework providers and inject application services into controllers. -For the GoForj integration, see [HTTP Services](/applications/http-services). +For the App workflow, see [HTTP Services](/applications/http-services). diff --git a/docs/libraries/wire.md b/docs/libraries/wire.md index a5a2730..68b8d3c 100644 --- a/docs/libraries/wire.md +++ b/docs/libraries/wire.md @@ -150,13 +150,17 @@ runtime dependency on Wire. ## Running generation manually {#running-generation-manually} -Run the default command (alias for `wire gen`) or specify packages: +Use `make generate` to run `wire gen` across the repository. The CLI remains useful for targeted generation: ```sh wire wire gen ./... ``` +## Development {#development} + +Use `make test` for the test suite and `make vet` for static checks. + ## Watching for changes {#watching-for-changes} Wire includes a native watcher that re-runs generation on Go file changes: diff --git a/docs/operations/deployment-basics.md b/docs/operations/deployment-basics.md index 4ad7f36..de6f1f6 100644 --- a/docs/operations/deployment-basics.md +++ b/docs/operations/deployment-basics.md @@ -20,6 +20,8 @@ flowchart LR This page owns that path. The linked operations pages cover process topology, probes, metrics, backups, and security policy in more depth. +This page uses `forj` while preparing source and `./bin/` after an artifact has been built. Production supervisors and release checks should execute the exact binary being deployed, not source-aware development commands. + ## Before You Start Decide which Apps belong in the release and where they will run. Provision the production database, queue, cache, storage, and other external services selected by those Apps. The deployment platform must also own DNS, TLS termination, network policy, process supervision, and secret delivery. @@ -35,7 +37,7 @@ forj build && test -x ./bin/app ``` -Expected result: `bin/app` exists and is executable. The build refreshes generated Project files, runs Wire, prepares the API index, and compiles the default App. +Expected result: `bin/app` exists and is executable. The build refreshes Framework-managed Project files, runs Wire, prepares the API index, and compiles the default App. ### Apps with Web UI @@ -51,17 +53,31 @@ Expected result: `cmd/app/frontend/dist` contains current frontend output and `b ### Additional Apps -Build each independently deployable App: +Build each independently deployable App. If it has frontend source, build that App's frontend first using its path under `cmd//frontend/`: ```bash -forj admin build && +npm --prefix cmd/admin/frontend ci && + npm --prefix cmd/admin/frontend run build && + forj admin build && test -x ./bin/admin ``` -Expected result: `bin/admin` contains the staff-facing App selected by the command prefix. Repeat the build for every App included in the release. +Expected result: `cmd/admin/frontend/dist` contains the current staff frontend and `bin/admin` contains the staff-facing App selected by the command prefix. Omit the npm steps when that App has no frontend source. Repeat the applicable frontend build, App build, and artifact check for every App included in the release. Keep the artifact immutable after it has been tested. If a release is transferred to another host or registry, verify its checksum or image digest before activation. +### Hand Off the Artifact + +The build system should hand the deployment system one identified, immutable release. Record at least: + +- the binary or image digest; +- every App binary included in the release; +- the target operating system and architecture; +- the source revision and build time; and +- any non-secret defaults or overrides compiled with `forj build`. + +Frontend output is already embedded in an App binary after the frontend build and `forj build`; do not deploy an unrelated `dist` directory beside it. Promote the same tested bytes between environments. If configuration or frontend assets require a rebuild, assign the result a new release identity and repeat artifact checks. + ## Keep Configuration Outside the Artifact Supply production configuration through the process environment or the secret and configuration mechanism provided by the deployment platform. @@ -81,6 +97,8 @@ APP_DIAG_TOKEN= The rendered App determines the rest. Configure its selected database, queue, cache, storage, event, mail, and observability drivers with production values. Do not allow a missing production dependency to silently fall back to a process-local driver. +`forj build --env-defaults` and `--env-overrides` are explicit exceptions: they pin non-secret values into the binary. Defaults remain replaceable by deployment configuration; overrides do not. Use defaults only for artifact-level fallbacks and overrides only when every deployment of that artifact must use the same value. A rotated credential, environment endpoint, port, replica-specific identity, or retention setting belongs to the deployment system instead. See [Compiled Environment Values](/reference/configuration#compiled-environment-values) for exact precedence. + Bind to `127.0.0.1` when a reverse proxy on the same host owns public traffic. Bind to `0.0.0.0` only when a container network, firewall, or host network policy controls access. If the App intentionally uses SQLite, local storage, uploads, or another writable filesystem path, place that data outside the versioned release directory and configure an absolute path. Replacing an immutable release must not replace or orphan durable application data. @@ -110,6 +128,16 @@ Changing the command does not change application behavior or make in-memory driv Run one scheduler process unless the schedules use deliberate cross-process locking. See [Runtime Processes](/operations/runtime-processes) before introducing a split topology. +A concrete split deployment might use the same immutable artifact in these supervised process groups: + +| Process group | Replicas | Traffic or work handoff | Scaling signal | +| --- | ---: | --- | --- | +| `./bin/app api` | Two or more | The platform sends HTTP traffic only to ready instances. | Request load, latency, and resource use. | +| `./bin/app worker --queue emails` | One or more | A shared queue backend hands jobs to workers. | Queue depth, job age, failures, and resource use. | +| `./bin/app scheduler` | One | The supervisor maintains singleton ownership unless schedules use a shared lock. | Availability and due-run outcomes, not HTTP load. | + +This is a topology example, not a required replica count. Each group receives its own environment, resource limits, probe configuration, and restart policy. If an App has another binary such as `bin/admin`, model its HTTP, worker, and scheduler roles independently rather than assuming the default App process owns them. + ## Give the Process to a Supervisor The deployment platform should: @@ -125,7 +153,21 @@ The deployment platform should: This contract applies equally to systemd, a container runtime, Kubernetes, Nomad, or another supervisor. The exact service unit or workload manifest belongs to that platform. -Set the supervisor's stop grace period above the longest effective App shutdown path, with additional margin for the supervisor itself. In combined mode, runtime shutdown happens concurrently before the outer App lifecycle finishes. Test shutdown with real in-flight jobs instead of relying only on arithmetic. +### Budget Timeouts as a System + +Timeouts protect different boundaries and should be planned together: + +| Boundary | Example control | What it bounds | +| --- | --- | --- | +| One unit of work | A job timeout or `SCHEDULER_COMMAND_TIMEOUT` | The handler or scheduled command execution. | +| Runtime cleanup | `QUEUE_SHUTDOWN_TIMEOUT` | Queue drain and backend cleanup inside App shutdown. | +| App shutdown | `APP_SHUTDOWN_TIMEOUT` | The HTTP or scheduler graceful-stop path and outer App lifecycle. | +| Readiness request | Probe or `health --timeout-ms` | One operator or platform request, including sequential resource checks. | +| Process supervision | Platform stop grace period | The complete interval before the platform may force termination. | + +The App resolves this policy once at startup. A queue or scheduler subprocess value larger than `APP_SHUTDOWN_TIMEOUT` is capped to the App budget and emits one structured warning with the configured and effective values. Run `./bin/app about` to inspect the effective Runtime settings before changing supervisor limits. + +Do not add every configured duration and assume that sum is the required supervisor value. Some shutdown work is concurrent, some limits are nested, and a handler that ignores cancellation can outlive its intended budget. Set the supervisor's stop grace period above the longest observed graceful-stop path with margin for traffic removal and supervisor overhead. In combined mode, runtime shutdown happens concurrently before the outer App lifecycle finishes. Test `SIGTERM` with real in-flight requests, jobs, and scheduled commands; make interrupted work safe to retry. The long-running command must be the deployed binary: @@ -149,8 +191,30 @@ Expected result: the migration command exits successfully before processes from Run the command once per migration-owning App, not once per HTTP replica. During a rolling deployment, prefer additive schema changes that work with both the old and new binaries. +For an additional migration-owning App, run that App's staged binary independently: + +```bash +/srv/example/releases/2026-07-28/bin/admin migrate +``` + +Expected result: only the `admin` App's migration streams run. Repeat this once for each migration-owning App in the release; do not infer that the default App command migrated additional Apps. + Use stable paths for backup output rather than writing backup sets inside a versioned release directory. [Backup and Restore](/operations/backups) covers discovery, verification, retention, and restore safeguards. +## Hand Off Observability and Retention + +The App emits signals; the deployment platform and operators own their transport, access, and retention. Before activation, assign each signal to an operational destination: + +| Signal or data | App/process responsibility | Deployment responsibility | +| --- | --- | --- | +| Logs | Write structured runtime output to standard output and standard error. | Collect, index, redact, retain, and alert on it. | +| Metrics | Expose the endpoint appropriate to combined or split topology. | Scrape it with bounded labels, retain time series, and define alerts. | +| Health and readiness | Report process liveness and required dependency state. | Route probes correctly and remove unready HTTP instances from traffic. | +| Inspects and Lighthouse | Capture and present bounded recent execution detail when enabled. | Restrict operator access and choose capture, sampling, and recent-window limits. | +| Backups and durable data | Use the configured stable paths and backends. | Schedule backups, apply retention, verify restore, and keep data outside release directories. | + +Preserve the app name, runtime role, release identity, and instance identity in the surrounding platform metadata so an alert can be traced to the exact process and artifact. Set `APP_VERSION` and `APP_REVISION` while building framework-managed metrics discovery, and apply equivalent bounded labels in an external production scraper. Lighthouse's recent Inspect window is not a substitute for retained logs, metrics, or backups. + ## Activate the Release A useful filesystem layout separates immutable releases from mutable state: @@ -246,12 +310,14 @@ Queue payloads are another compatibility boundary. A previous worker binary must - Build every App for the deployment target. - Build frontend assets before `forj build` when the App has Web UI. - Store production configuration and secrets outside the artifact. +- Record any non-secret defaults or overrides intentionally compiled into it. - Use production drivers for state shared across processes or hosts. - Keep writable data and backup sets outside immutable release directories. - Run the staged release's migrations once before it receives traffic. - Supervise each required process with the deployed binary. - Keep the scheduler singleton unless locking makes overlap safe. - Set and test graceful shutdown budgets. +- Assign logs, metrics, Inspects, and backups explicit access and retention owners. - Verify liveness, readiness, metrics, and one application workflow. - Keep the previous artifact available for binary rollback. - Treat database migrations and queued payloads as separate rollback contracts. diff --git a/docs/operations/http-server.md b/docs/operations/http-server.md index ec821e2..3cb8d1d 100644 --- a/docs/operations/http-server.md +++ b/docs/operations/http-server.md @@ -77,6 +77,20 @@ For a staff operations App named `admin`, use its binary: Expected result: a complete, human-readable table of the staff-facing methods, paths, and handlers registered by `admin`. The command constructs the App route surface but does not start the HTTP listener. +A small route table looks like this (terminal output uses color when supported): + +```text ++-------------------+---------+-----------------------+------------+ +| API Routes › (1) ++-------------------+---------+-----------------------+------------+ +| Path | Methods | Handler | Middleware | ++-------------------+---------+-----------------------+------------+ +| /api/v1/users/:id | GET | users.Controller.Show | | ++-------------------+---------+-----------------------+------------+ +``` + +Long middleware names may be replaced by short codes with a `Middleware Legend` above the route title. The columns remain `Path`, `Methods`, `Handler`, and `Middleware`. + ## Health, Readiness, and Verification Use liveness to answer whether the process is responding and readiness to decide whether it should receive traffic: diff --git a/docs/operations/index.md b/docs/operations/index.md index 4da6218..564abe2 100644 --- a/docs/operations/index.md +++ b/docs/operations/index.md @@ -19,6 +19,7 @@ Use these guides when you need to run, split, observe, deploy, or recover an App | Configure probes | [Health and Readiness](/operations/health-readiness) | | Investigate runtime behavior | [Logging](/operations/logging), [Metrics](/operations/metrics), and [Inspects](/operations/inspects) | | Use the operator interface | [Lighthouse](/operations/lighthouse) | +| Compare HTTP and infrastructure performance safely | [Performance Benchmarks](/operations/performance-benchmarks) | | Protect and recover durable state | [Backup and Restore](/operations/backups) | | Build, roll out, verify, and roll back a release | [Deploy an App](/operations/deployment-basics) | diff --git a/docs/operations/lighthouse.md b/docs/operations/lighthouse.md index a9e4004..3d0c6b2 100644 --- a/docs/operations/lighthouse.md +++ b/docs/operations/lighthouse.md @@ -9,6 +9,12 @@ Lighthouse is GoForj's UI for inspecting running applications during development It brings inspects, logs, routes, schedules, queue state, cache, storage, and metrics-backed views into one operator workspace. + + +Instead of reconstructing a failure from several terminals, select the app and Runtime that produced it, follow its recent execution timeline, and move directly to the registered route, queue, schedule, cache, or storage resource involved. The workspace follows the components compiled into each App, so a focused worker and a full web app do not pretend to expose the same controls. + +Use Lighthouse when you need the fast, connected view. Keep logs, metrics, health checks, and Inspects independently useful so production diagnosis never depends on one UI. + ## Role Lighthouse should present already-useful operational data. @@ -90,6 +96,12 @@ Queue actions depend on the selected queue backend. The queue contract defines a Treat operator actions as production changes. Before retrying or deleting queue work, identify the app, process, queue, job, and driver capability; use application logs, metrics, and inspects to establish impact. A control that returns unsupported is a driver capability limit, not an empty queue. +## Browser Benchmarks + +Lighthouse includes a browser workflow for controlled HTTP, cache, queue, storage, and database comparisons. It discovers only the suites and configured resource instances available to the selected App, then reports throughput, latency percentiles, errors, driver details, and the system baseline together. + +Benchmarks create real load and can leave data behind after interruption or backend failure. Read [Performance Benchmarks](/operations/performance-benchmarks) before choosing a target, changing concurrency, or using results for a capacity decision. + ## Operational Workflow Use Lighthouse after the underlying signal has identified a problem: @@ -118,10 +130,12 @@ This order keeps Lighthouse a useful operator view without turning it into the o - Every replica has an unambiguous app, process role, and instance identity. - Connection and authentication warnings are collected with application logs. - A Lighthouse outage has a documented fallback using direct health, logs, metrics, and app commands. +- Browser benchmark access is limited to operators, and production runs require a reviewed target and load budget. ## Next Steps - [Inspects](/operations/inspects) - [Metrics](/operations/metrics) +- [Performance Benchmarks](/operations/performance-benchmarks) - [Environment Reference](/reference/env-vars#lighthouse-and-inspects) -- [Generated Extension Points](/core/code-generation#choose-a-safe-extension-point) +- [App Extension Points](/core/code-generation#choose-a-safe-extension-point) diff --git a/docs/operations/logging.md b/docs/operations/logging.md index 5350812..9869d21 100644 --- a/docs/operations/logging.md +++ b/docs/operations/logging.md @@ -81,6 +81,22 @@ Each request event retains named fields for URI, method, status, latency, and cl Console output uses a compact value-oriented line with status-aware color. JSON output and registered log sinks retain the structured field names, so machine processing does not depend on console formatting. +An App logger can route one structured entry to several destinations. `AddSink` preserves the simple synchronous callback API. Use `AddSinkWithOptions` when a destination needs error isolation or a bounded asynchronous queue: + + +```go +if err := appLogger.AddSinkWithOptions(auditSink, logger.SinkOptions{ + Name: "audit", + Async: true, + QueueCapacity: 256, + Overflow: logger.SinkOverflowDropOldest, +}); err != nil { + return err +} +``` + +Registrations append; they do not replace process output or an earlier sink. Synchronous sinks run in the logging path, so keep them fast. Asynchronous sinks preserve accepted entries in FIFO order and make overload behavior explicit with `block`, `drop-newest`, or `drop-oldest`. Flush managed sinks from an App shutdown hook with `appLogger.FlushSinks(ctx)` so accepted entries get the remaining shutdown budget. + Disable access logs for a runtime where request volume would hide higher-signal events, then rely on metrics and inspects for the intended visibility. Keep access logs enabled during a new deployment until the request path, status, and latency are understood. If you disable them for steady-state volume, retain an explicit route-level metric and a safe Inspect sampling policy; otherwise a 5xx increase has no request-level path back to an operator. @@ -97,7 +113,16 @@ APP_LOG_TIME Use structured fields that answer an operational question: App identity, Runtime source, route pattern, queue or schedule name, status, and latency. The request context can carry the `trace_id` correlation field used by Inspects, but the product surface is still called an Inspect. -Never log authorization headers, cookies, credentials, raw queue payloads, or unredacted request bodies by default. Local HTTP error-response capture is intentionally local-environment behavior; do not rely on it as a production payload-dump mechanism. +Never log authorization headers, cookies, credentials, raw queue payloads, or unredacted request bodies by default. Before deduplication, process output, or sink delivery, the App logger redacts common secret-bearing field names and high-confidence message forms such as bearer tokens and `password=...` assignments. + +Extend that mandatory policy for application-specific data: + +```dotenv +APP_LOG_REDACT_KEYS=session_id,customer_reference +APP_LOG_REDACT_MESSAGE_PATTERNS=["client_secret=[^ ]+","session=[^ ]+"] +``` + +The first value is a comma-separated list of additional case-insensitive field names. The second is a JSON array of regular expressions; invalid JSON or expressions fail during logger construction instead of silently weakening the policy. These controls supplement the framework defaults rather than disabling them. Redaction is a safety net, not permission to log whole payloads. Local HTTP error-response capture remains intentionally local-environment behavior. ## Failure Modes diff --git a/docs/operations/metrics.md b/docs/operations/metrics.md index 7a14e7e..205d637 100644 --- a/docs/operations/metrics.md +++ b/docs/operations/metrics.md @@ -113,6 +113,25 @@ environment=local This label set identifies a background staff-operations job from `admin`. Use `source` for logical runtime attribution and `process` for scrape topology. +## Deployment Markers + +Set release identity in the Project-owned environment used to generate the deployment artifact: + +```dotenv +APP_VERSION=v1.8.0 +APP_REVISION=8d5c20f +``` + +Then build the artifact: + +```bash +forj build +``` + +When present, `APP_VERSION` becomes the bounded `release` target label and `APP_REVISION` becomes `revision`. Blank values are omitted. Keep both values low-cardinality: use one release version and one immutable commit or artifact revision, not a request ID or build timestamp. This lets a dashboard annotation or deployment comparison separate a real regression from ordinary traffic movement. + +Generation intentionally reads `.env.example` and `.env`, not unrelated ambient shell variables, so the discovery file and the artifact are derived from one reviewable Project snapshot. These labels are written into framework-managed local discovery files during generation. In an externally managed production scraper, apply the same release and revision labels in that platform's service-discovery configuration. + Avoid user IDs, emails, raw URLs, raw SQL, cache keys, filenames, request IDs, and arbitrary error strings. ## Proving Path diff --git a/docs/operations/performance-benchmarks.md b/docs/operations/performance-benchmarks.md new file mode 100644 index 0000000..5c5b48d --- /dev/null +++ b/docs/operations/performance-benchmarks.md @@ -0,0 +1,98 @@ +--- +title: Performance Benchmarks +description: Run and interpret Lighthouse browser benchmarks for HTTP, cache, queues, storage, and databases. +--- + +# Performance Benchmarks + +Lighthouse can send controlled workload through a connected App and compare HTTP or configured infrastructure resources from the browser. + +Use this page to establish a reproducible baseline, compare an intentional driver or configuration change, or find the point where higher concurrency stops helping. These benchmarks measure the selected suite under the selected conditions; they do not predict whole-application capacity or define a service-level objective. + +## What Lighthouse Can Measure + +The Benchmarks view asks a connected benchmark-capable agent for its catalog. The available rows follow the components compiled into that App and the resource instances its Runtime discovers. + +| Suite | Target | Work performed | +| --- | --- | --- | +| HTTP | Configured benchmark URL and path | Repeated `GET` requests over the configured duration. | +| Cache | One configured cache instance | Benchmark-owned keys and payloads against that Driver. | +| Queue | One configured queue instance | Benchmark jobs dispatched and drained through the selected queue. | +| Storage | One configured storage disk | Benchmark-owned paths and payloads against that Driver. | +| Database | One configured connection | Operations against a benchmark table created when needed. | + +Lighthouse does not show a cache, storage, or database suite merely because the UI knows its name. The App must compile the component and expose a usable instance. Queue is part of the benchmark runner, but a meaningful queue run also needs workers consuming the selected benchmark queue. + +## Before You Run + +Treat a benchmark as an operator change, not a read-only diagnostic. + +1. Prefer an isolated or representative environment with the same topology you want to compare. +2. Select the exact App, Runtime agent, resource instance, and Driver. +3. Confirm the HTTP URL and path or queue name before sending work. +4. Record the release, agent instance, duration, concurrency, payload size, sweep limit, and surrounding load. +5. Watch normal CPU, memory, network, connection-pool, broker, database, storage, error, and saturation signals during the run. + +::: warning Active workload +The HTTP suite repeatedly sends `GET` requests. Do not point it at a route that mutates data or triggers expensive side effects. Resource suites use run-scoped benchmark keys, paths, jobs, and records and attempt cleanup, but a crash, cancellation, backend error, or unavailable cleanup operation can leave data or queued work behind. The database suite can create its benchmark table. +::: + +## Run a Baseline in Lighthouse + +Open **Benchmarks** in Lighthouse, then: + +1. Confirm the selected agent. Lighthouse prefers a connected jobs Runtime when one is available because the generated benchmark runner lives with App job infrastructure. +2. Select one target first. Each configured cache, queue, storage, or database instance appears as its own target. +3. Keep the initial duration, concurrency, and payload settings unchanged so the first result is a baseline rather than a tuning experiment. +4. Select **Run Baseline** and keep the page attached until the target finishes. +5. Save the target identity, system baseline, result details, and any errors with your change record. + +Expected result: the page reports the suite and Driver, configured duration and concurrency, actual elapsed time, operation count, operations per second, errors, and p50, p95, and p99 latency. Suite-specific detail and system information appear with the report. + +If the target does not appear, confirm that the component and resource instance belong to the selected App. If a queue run stalls, confirm workers are consuming the selected queue before increasing the drain timeout or concurrency. + +## Repeat the Baseline from the Artifact + +The built App exposes the same generated runner for repeatable release checks. For an HTTP-only baseline: + +```bash +./bin/app benchmark:run \ + --suites http \ + --duration-ms 15000 \ + --concurrency 8 \ + --payload-size 512 +``` + +Expected result: the command prints the system baseline, one HTTP result row, and suite details with throughput, operation count, errors, p50, p95, p99, and elapsed time. Add `--json` when a comparison pipeline needs structured output. The command runs the same App-owned suite as Lighthouse; it does not make a production target safer. + +## Compare One Change + +Change one material variable at a time: release, Driver, backend location, pool size, payload, or concurrency. Keep the remaining conditions stable and repeat enough runs to distinguish a durable change from environmental noise. + +Use a concurrency sweep to explore where throughput stops improving or errors and tail latency begin to rise. A sweep is still a synthetic comparison: it does not model production request mixes, think time, cache warmth, or contention from unrelated workloads. + +## Interpret the Report + +Read the result as one evidence bundle: + +- `ops/sec` is achieved throughput for that suite, target, and configuration—not an App-wide total, capacity guarantee, or SLO. +- p50, p95, and p99 describe the observed successful timing distribution. Read them with operation count and errors; a low percentile does not excuse failed work. +- elapsed time can differ from configured duration because setup, drain, cleanup, or backend behavior belongs to the run. +- Driver and instance identity matter. Results from two differently located backends are not interchangeable even when their logical resource name matches. +- the system baseline matters. CPU topology, memory, virtualization, co-located work, and network distance can dominate a small code change. + +Do not add operations-per-second values from different targets and call the sum application throughput. The suites run distinct operations against distinct resources. + +## Confirm with Production-Shaped Evidence + +Use Lighthouse benchmarks to form or reject a focused hypothesis. Before changing capacity, timeouts, or service objectives, confirm the conclusion with production-shaped load testing and the App's ordinary metrics, logs, readiness, and Inspects. + +After a production-authorized run, inspect the selected backend for benchmark records or queued work that cleanup could not remove. + +## Related + +- [Lighthouse](/operations/lighthouse) +- [Metrics](/operations/metrics) +- [Inspects](/operations/inspects) +- [Queue Workers](/operations/queue-workers) +- [Environment Reference](/reference/env-vars#demo-app) diff --git a/docs/operations/runtime-processes.md b/docs/operations/runtime-processes.md index ae1e256..1449d33 100644 --- a/docs/operations/runtime-processes.md +++ b/docs/operations/runtime-processes.md @@ -69,7 +69,7 @@ Common variables: ```text APP_SHUTDOWN_TIMEOUT=30s QUEUE_SHUTDOWN_TIMEOUT=10s -SCHEDULER_SUBPROCESS_SHUTDOWN_TIMEOUT=90s +SCHEDULER_SUBPROCESS_SHUTDOWN_TIMEOUT=30s ``` Constructors should build dependencies, not start long-running work. Keep business behavior independent of process topology, allow workers time to finish in-flight jobs, and use locking or singleton control before running multiple schedulers. Splitting processes does not provide shared state or job correctness by itself. diff --git a/docs/operations/scheduler-processes.md b/docs/operations/scheduler-processes.md index 787f8de..22b0fb0 100644 --- a/docs/operations/scheduler-processes.md +++ b/docs/operations/scheduler-processes.md @@ -64,10 +64,10 @@ Scheduler-owned command tasks have separate child-process controls: ```text SCHEDULER_COMMAND_TIMEOUT=10m -SCHEDULER_SUBPROCESS_SHUTDOWN_TIMEOUT=90s +SCHEDULER_SUBPROCESS_SHUTDOWN_TIMEOUT=30s ``` -`SCHEDULER_COMMAND_TIMEOUT` bounds one command task. `SCHEDULER_SUBPROCESS_SHUTDOWN_TIMEOUT` gives an App command launched with scheduler-command origin its own graceful shutdown budget; it does not replace the parent scheduler Runtime's `APP_SHUTDOWN_TIMEOUT`. +`SCHEDULER_COMMAND_TIMEOUT` bounds one command task. `SCHEDULER_SUBPROCESS_SHUTDOWN_TIMEOUT` gives an App command launched with scheduler-command origin its own graceful shutdown budget, but it cannot extend the remaining parent `APP_SHUTDOWN_TIMEOUT`. Leave it empty to inherit the App budget. ## Verify a Deployment diff --git a/docs/public/design-system.css b/docs/public/design-system.css index f500244..fdaa91b 100644 --- a/docs/public/design-system.css +++ b/docs/public/design-system.css @@ -7358,3 +7358,15 @@ html:not(.dark) .gf-blog-visual { --gf-ink: #FFFFFF; --gf-ink-2: #A9A1B3; } +/* Atlas uses its source-owned product banner while the framework guide remains the preferred user path. */ +.vp-doc .gf-atlas-banner { + margin: 1.5rem 0 1.75rem; +} + +.vp-doc .gf-atlas-banner img { + display: block; + width: 100%; + border: 1px solid var(--vp-c-divider); + border-radius: 16px; + box-shadow: 0 18px 48px rgb(0 0 0 / 18%); +} diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 7da86fc..4fc0641 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -10,7 +10,7 @@ GoForj has two main configuration layers: - `.goforj.yml` for project rendering and development workflow. - environment variables for runtime behavior. -This page defines accepted keys and values. Start with [Configuration](/getting-started/configuration) for the first working change, or [forj dev](/developer-tools/forj-dev) for the build, SPA, and runtime loop. The lifecycle examples below illustrate configuration shapes rather than a second development tutorial. +This page is a lookup for accepted keys and values. It does not replace the task-oriented setup guides or define production runtime policy. Start with [Configuration](/getting-started/configuration) for the first working change, or [forj dev](/developer-tools/forj-dev) for the build, SPA, and runtime loop. The lifecycle examples below illustrate configuration shapes rather than a second development tutorial. ## `.goforj.yml` @@ -70,7 +70,17 @@ Compiled defaults fill values that remain unset after normal file-backed loading Compiled overrides take precedence over process and file-backed values. A compiled `APP_ENV` override selects its matching environment file and remains authoritative after loading. -Use these options for controlled packaging workflows. Prefer environment files and process environment for normal local development. See [CLI Reference](/reference/cli#framework-command-options) for the complete `forj build` option list. +These values become part of the artifact contract: + +| Build input | Runtime can replace it? | Appropriate use | +| --- | --- | --- | +| `--env-defaults KEY=value` | Yes, with a process or file-backed value | A non-secret fallback that should travel with this artifact. | +| `--env-overrides KEY=value` | No; rebuild the artifact to change it | A non-secret packaging constraint that must remain fixed for every process using this artifact. | +| Process environment or deployment configuration | Yes, when the deployment changes it | Environment-specific endpoints, credentials, ports, scaling, retention, and operational policy. | + +Do not compile secrets into either channel. Compiled values can be recovered from or observed with the artifact, and an override prevents the deployment platform from correcting that key at startup. Treat a change to a compiled default or override like any other artifact change: rebuild, identify, test, and promote the new binary or image. + +Most production configuration remains deployment-owned. Use the deployment platform's environment, configuration, and secret delivery mechanisms for values that differ by environment or must rotate independently of a build. Prefer environment files and process environment for normal local development. See [CLI Reference](/reference/cli#framework-command-options) for the complete `forj build` option list and [Deploy an App](/operations/deployment-basics#keep-configuration-outside-the-artifact) for the production handoff. ## Development Tasks @@ -334,6 +344,8 @@ Catalog dependencies are resolved in memory by the renderer. For example, metric ## Module Replaces +`render.module_replaces` manages local Go module replacements during Project rendering. On render, GoForj applies each entry to `go.mod` with `go mod edit -replace` and records which module paths it owns in `.goforj.module_replaces.json`. When an owned entry is later removed from `.goforj.yml`, the next render drops that replacement without touching unrelated replacements that a maintainer added directly to `go.mod`. + Use paths that are stable from the Project root. For local sibling repositories, prefer a relative path: ```yaml @@ -342,7 +354,7 @@ render: github.com/goforj/web: ../web ``` -Do not use container-specific absolute paths; they only work in one local environment. +This is a development and rendering aid, not runtime dependency configuration. Do not use container-specific or machine-specific absolute paths in shared Project configuration; they only work in one local environment. Before a release build, confirm that `go.mod` does not resolve production dependencies through unintended local replacements. ## Related Pages diff --git a/docs/reference/env-vars.md b/docs/reference/env-vars.md index ef67c13..f880eea 100644 --- a/docs/reference/env-vars.md +++ b/docs/reference/env-vars.md @@ -48,6 +48,7 @@ Variables for components that are not selected are not rendered and have no gene | `APP_DIAG_TOKEN` | Generated when Web API is rendered | Bearer token for protected diagnostic commands and endpoints. | | `APP_SHUTDOWN_TIMEOUT` | `30s` | Root graceful-shutdown budget. | | `APP_VERSION` | Empty | Deployment version reported to Lighthouse. | +| `APP_REVISION` | Empty | Immutable deployment revision used by framework-managed metrics discovery. Keep it bounded and low-cardinality, such as a commit SHA. | | `APP_INSTANCE_ID` | Empty | Explicit process or replica identity reported to Lighthouse. When empty, Lighthouse identifies the instance by hostname, then its generated agent ID. | | `APP_INSTANCE_KIND` | Empty | Optional deployment-specific instance classification reported to Lighthouse. | | `APP_MODE` | Empty | Optional runtime mode used in generated log labels. Runtime commands normally set their own context. | @@ -64,6 +65,8 @@ Variables for components that are not selected are not rendered and have no gene | `APP_LOG_DEDUPE_WINDOW_MS` | `1200` | Dedupe window in milliseconds. | | `APP_LOG_DEDUPE_BURST` | `2` | Matching messages emitted before suppression begins within a window. | | `APP_LOG_DEDUPE_SUMMARY_EVERY` | `1000` | Suppressed occurrences between summary messages. | +| `APP_LOG_REDACT_KEYS` | Empty | Comma-separated application-specific field names to redact in addition to mandatory secret-bearing names. | +| `APP_LOG_REDACT_MESSAGE_PATTERNS` | Empty | JSON array of regular expressions replaced with `[REDACTED]` before output and sink delivery. Invalid JSON or expressions fail during logger construction. | See [Logging](/operations/logging) for event shape, output modes, and sensitive-data guidance. @@ -196,7 +199,7 @@ Framework-owned instrumentation toggles all default to `true` when their compone | `METRICS_DATABASE_ENABLED` | Database operations. | | `METRICS_AUTH_ENABLED` | Auth flows. | | `METRICS_SCHEDULER_ENABLED` | Scheduler operations. | -| `METRICS_MONITORING_ENABLED` | Monitoring metrics in the generated demo App. | +| `METRICS_MONITORING_ENABLED` | Monitoring metrics in the demo App. | See [Metrics](/operations/metrics) for endpoints, labels, and scrape topology. @@ -249,7 +252,7 @@ The default connection uses `DB_`. Named connections use `DB___*` directly. | -The renderer supplies usable local MySQL or Postgres connection values when those services are selected. Active driver values also accept the compatibility aliases `sqlite3`, `mariadb`, and `postgresql`; supported-driver lists and new configuration should use the canonical names above. See [Database Strategy](/data/database-strategy) and [Database Shell](/data/database-strategy#shell-options). +The renderer supplies usable local MySQL or Postgres connection values when those services are selected. Active driver values also accept the compatibility aliases `sqlite3`, `mariadb`, and `postgresql`; supported-driver lists and new configuration should use the canonical names above. See [Database Connections](/data/database-strategy) and [Database Shell](/data/database-strategy#shell-options). ## Shared Redis and NATS @@ -481,8 +484,8 @@ See [Mail](/applications/mail) for local delivery, named mailers, and production | Variable | Default | Purpose | | --- | --- | --- | | `SCHEDULER_COMMAND_TIMEOUT` | `10m` | Maximum runtime for a command launched by a scheduled task. | -| `SCHEDULER_SUBPROCESS_SHUTDOWN_TIMEOUT` | `APP_SHUTDOWN_TIMEOUT`; rendered as `90s` | Grace period for scheduler-owned subprocesses. | -| `QUEUE_SHUTDOWN_TIMEOUT` | `10s` | Queue shutdown budget, also listed with Queue settings. | +| `SCHEDULER_SUBPROCESS_SHUTDOWN_TIMEOUT` | `APP_SHUTDOWN_TIMEOUT` | Grace period for scheduler-owned subprocesses, capped by the remaining App shutdown budget. | +| `QUEUE_SHUTDOWN_TIMEOUT` | `10s` | Queue shutdown budget, capped by the remaining App shutdown budget and also listed with Queue settings. | ## Local Observability @@ -550,7 +553,7 @@ S3-backed `STORAGE__*` resources can be inventoried as backup inputs, but See [Make Command Shared Options](/reference/make-commands#shared-options) and [forj dev](/developer-tools/forj-dev). -## Generated Demo App +## Demo App These controls exist only when the demo monitoring and Lighthouse benchmark surfaces are rendered. @@ -572,6 +575,6 @@ These controls exist only when the demo monitoring and Lighthouse benchmark surf - [Configuration](/getting-started/configuration) - [Driver Selection](/data/driver-selection) -- [Generated Components](/core/code-generation) +- [Code Generation](/core/code-generation) - [Named Resources](/core/named-resources) - [Production Hardening](/security/production-hardening) diff --git a/docs/reference/errors.md b/docs/reference/errors.md index ba1878a..c59fd1a 100644 --- a/docs/reference/errors.md +++ b/docs/reference/errors.md @@ -21,7 +21,7 @@ Likely causes: Start with: -- [Generated Components](/core/code-generation) +- [Code Generation](/core/code-generation) - [Generation Commands](/reference/generation-commands) - [Configuration Reference](/reference/configuration) @@ -40,7 +40,7 @@ Start with: - [Providers](/core/dependency-injection#providers) - [Wiring Recipes](/developer-tools/wiring-recipes) - [Reading Wire Errors](/developer-tools/reading-wire-errors) -- [Generated Files](/reference/generated-files) +- [File Ownership](/reference/generated-files) ## Runtime Readiness Errors diff --git a/docs/reference/generated-files.md b/docs/reference/generated-files.md index 08aa356..1b356ff 100644 --- a/docs/reference/generated-files.md +++ b/docs/reference/generated-files.md @@ -1,60 +1,82 @@ --- -title: Generated Files -description: Lookup reference for common generated files and ownership boundaries in GoForj Projects. +title: File Ownership +description: Identify which GoForj Project files belong to your application, the Framework, generators, or build tooling. --- -# Generated Files - -Generated files are not all owned the same way. - -Check file headers and local package READMEs before editing. - -## Common Files and Directories - -| Path | Purpose | -| --- | --- | -| `.goforj.yml` | Project render and development contract. | -| `.env` | Local runtime environment defaults. | -| `cmd/app/main.go` | Default app binary entrypoint. | -| `cmd//main.go` | Additional app binary entrypoint. | -| `cmd//frontend/` | Frontend source and embedded build output for an app with Web UI. | -| `app/commands.go` | Default app command exposure. | -| `app/lifecycle.go` | Default app lifecycle hooks. | -| `app/routes.go` | Default app route exposure. | -| `app/schedules.go` | Default app schedule exposure. | -| `app/wire/wire.go` | Default app Wire injector definitions. | -| `app/wire/wire_gen.go` | Generated Wire output. Do not edit by hand. | -| `app//...` | Additional app composition files. | -| `app//wire/...` | Additional app Wire graph. | -| `internal/runtime/apps.go` | App metadata derived from Project configuration and deterministic runtime defaults. Do not edit by hand. | -| `internal/caches/*_gen.go` | Generated cache accessors and config when Cache is enabled. | -| `internal/storages/*_gen.go` | Generated storage accessors and config when File Storage is enabled. | -| `internal/queues/*_gen.go` | Generated queue accessors and config when Background Jobs is enabled. | -| `internal/events/*_gen.go` | Generated event bus accessors and config when Events is enabled. | -| `internal/database/*_gen.go` | Generated DB accessors and config when a database component is enabled. | -| `build/api_index.json` | Default app API index output. | -| `build/api_index.diagnostics.json` | Default app API index diagnostics. | -| `build/openapi.json` | Default app OpenAPI output. | -| `build/.webindex-artifacts.lock` | Publication lock coordinating the default app artifact set. | -| `build//api_index.json` | Per-app API index output. | -| `build//api_index.diagnostics.json` | Per-app API index diagnostics. | -| `build//openapi.json` | Per-app OpenAPI output. | -| `build//.webindex-artifacts.lock` | Publication lock coordinating one per-app artifact set. | -| `.goforj/backups//manifest.json` | Local backup set inventory. | -| `.goforj/backups//checksums.txt` | Checksums for backup artifacts. | +# File Ownership + +Files in a GoForj Project have different owners. Some are normal application code, some are extension points created once, and some are refreshed from configuration or build inputs. + +Use this page when deciding whether to edit a file or change the input that creates it. File headers remain authoritative when a specific file says `DO NOT EDIT`. + +## Application and App Composition + +| Path | Owner | Created or refreshed by | Edit? | +| --- | --- | --- | --- | +| `internal//...` | Application | You and `forj make:*` | Yes | +| `app/commands.go` | App | Initial render and make commands | Yes | +| `app/lifecycle.go` | App | Initial render | Yes | +| `app/routes.go` | App | Initial render and controller commands | Yes | +| `app/schedules.go` | App | Initial render | Yes | +| `app/wire/inject_*_app.go` | App | Initial render and make commands | Yes | +| `app//...` | Additional App | Same conventions as the default App | Follow the matching default-App file | +| `migrations/` | Application | `forj make:migration` and application changes | Yes | + +## Framework-Managed Project Files + +These files implement the selected Project configuration. Change `.goforj.yml`, component selection, or the owning framework template instead of treating them as durable extension points. + +| Path | Created or refreshed by | Edit? | +| --- | --- | --- | +| `cmd/app/main.go` | `forj render` | No; use App registration files | +| `cmd//main.go` | `forj render` | No; use that App's registration files | +| `app/root_cmd.go` | `forj render` | No | +| `app/wire/app.go`, `app//wire/app.go` | `forj render` | No; add providers through `_app.go` files | +| `app/wire/wire.go` | `forj render` | Avoid; compose custom sets through `_app.go` files | +| `app/wire/inject_*.go` without `_app` | `forj render` | No | +| `internal/runtime/apps.go` | `forj render` | No | + +## Generated Go Output + +These filenames are the concrete outputs readers will encounter; they are not wildcard categories. + +| Capability | Paths | Refreshed by | Edit? | +| --- | --- | --- | --- | +| Wire | `app/wire/wire_gen.go`, `app//wire/wire_gen.go` | `forj build` or Wire generation | No | +| Cache | `internal/caches/manager_gen.go`, `internal/caches/accessors_gen.go` | Cache generation during `forj build` | No | +| Storage | `internal/storages/manager_gen.go`, `internal/storages/accessors_gen.go` | Storage generation during `forj build` | No | +| Queues | `internal/queues/manager_gen.go`, `internal/queues/accessors_gen.go` | Queue generation during `forj build` | No | +| Events | `internal/events/manager_gen.go`, `internal/events/accessors_gen.go` | Event generation during `forj build` | No | +| Mail | `internal/mail/manager_gen.go`, `internal/mail/accessors_gen.go` | Mail generation during `forj build` | No | +| Database | `internal/database/connections_gen.go` | Database generation during `forj build` | No | + +## Build and Operational Output + +| Path | Purpose | Owner | +| --- | --- | --- | +| `build/api_index.json` | Default App API index | Build tooling | +| `build/api_index.diagnostics.json` | Default App indexing diagnostics | Build tooling | +| `build/openapi.json` | Default App OpenAPI document | Build tooling | +| `build/.webindex-artifacts.lock` | Coordinates publication of the default App artifact set | Build tooling | +| `build//...` | Equivalent artifacts for an additional App | Build tooling | +| `bin/app`, `bin/` | Compiled App binaries | GoForj build pipeline | +| `cmd//frontend/dist/` | Built frontend embedded by a Web UI App | SPA build tooling | +| `.goforj/backups//manifest.json` | Backup set inventory | Backup tooling and operators | +| `.goforj/backups//checksums.txt` | Backup artifact checksums | Backup tooling and operators | + +Project inputs such as `.goforj.yml`, `.env`, and `go.mod` are configuration rather than generated output. See [Configuration Reference](/reference/configuration) and [Environment Reference](/reference/env-vars) for their separate render, build, and restart boundaries. ## Ownership Rules -- Files marked `DO NOT EDIT` should be regenerated. -- Render-once files are App-owned extension points. -- `internal/` owns behavior; `app/` owns exposure. -- Framework-wide changes belong in GoForj templates or generators, not only in a rendered Project. -- API artifacts and publication locks are tool-owned. Backup sets are operator-owned data and must not be committed. +- Edit application behavior and App-owned `_app.go` extension points normally. +- Change inputs and regenerate files marked `DO NOT EDIT`. +- Keep Framework-wide fixes in GoForj templates or generators, not only in one rendered Project. +- Do not commit build output, publication locks, or operator backup sets unless a repository explicitly owns a checked artifact. ## Related Pages - [Apps](/core/apps) -- [Generated Components](/core/code-generation) -- [Generated Extension Points](/core/code-generation#choose-a-safe-extension-point) +- [Project Structure](/getting-started/project-structure) +- [App Extension Points](/core/code-generation#choose-a-safe-extension-point) - [Code Generation](/core/code-generation) - [Backup and Restore](/operations/backups) diff --git a/docs/reference/generation-commands.md b/docs/reference/generation-commands.md index 1c88a85..4ba6ce3 100644 --- a/docs/reference/generation-commands.md +++ b/docs/reference/generation-commands.md @@ -81,6 +81,6 @@ Use `forj build` when unsure. ## Related Pages - [Code Generation](/core/code-generation) -- [Generated Components](/core/code-generation) +- [Code Generation](/core/code-generation) - [Make Command Reference](/reference/make-commands) - [Rendered App Smoke Tests](/testing/rendered-app-smoke-tests) diff --git a/docs/reference/index.md b/docs/reference/index.md index 33fae17..53c5b50 100644 --- a/docs/reference/index.md +++ b/docs/reference/index.md @@ -14,7 +14,7 @@ Use reference pages after you know the workflow and need exact command names, en - [CLI Reference](/reference/cli) - [Environment Reference](/reference/env-vars) - [Configuration Reference](/reference/configuration) -- [Generated Files](/reference/generated-files) +- [File Ownership](/reference/generated-files) - [Generation Commands](/reference/generation-commands) - [Errors](/reference/errors) diff --git a/docs/reference/make-commands.md b/docs/reference/make-commands.md index fcb92ed..929d58f 100644 --- a/docs/reference/make-commands.md +++ b/docs/reference/make-commands.md @@ -342,19 +342,14 @@ func NewSyncReportsJob(queues *queues.Manager) *SyncReportsJob { return &SyncReportsJob{queues: queues} } -// Queue creates a task and dispatches it to the selected queue. -// Add application inputs as arguments when defining the payload contract. -func (t *SyncReportsJob) Queue(ctx context.Context, name string) error { - var p SyncReportsJobPayload - // add your payload fields here - // p.User = name - - payload, err := json.Marshal(p) +// Queue dispatches the typed payload to the selected queue. +func (t *SyncReportsJob) Queue(ctx context.Context, payload SyncReportsJobPayload) error { + data, err := json.Marshal(payload) if err != nil { return err } _, err = t.queues.WithContext(ctx).Dispatch( - queue.NewJob(SyncReportsJobTypeName).Payload(payload).OnQueue("billing"), + queue.NewJob(SyncReportsJobTypeName).Payload(data).OnQueue("billing"), ) return err } @@ -856,7 +851,7 @@ Generate a model and repository helpers in an explicit package. forj make:model invoices --package billing ``` -The generator inspects the existing `invoices` table through the default database connection, so that connection must be available. Models use `--package` rather than `-d` because their placement follows database table ownership. +The positional argument is the exact name of an existing table. The generator inspects `invoices` through the default database connection, so that connection must be available. It does not create or migrate the table, and `make:model` does not select a named connection. The generated Go type and filename are singularized from the inspected table name; when an exact table is missing, the command may suggest an existing singular or plural variant. Models use `--package` rather than `-d` because their placement follows database table ownership. ```bash forj make:model invoices --package billing --remove diff --git a/docs/scenarios/cached-user-profile.md b/docs/scenarios/cached-user-profile.md index 1d9e429..ca165b9 100644 --- a/docs/scenarios/cached-user-profile.md +++ b/docs/scenarios/cached-user-profile.md @@ -383,7 +383,9 @@ forj route:list Expected output includes: -- `/api/v1/users/:id` +```text +/api/v1/users/:id +``` ## Try the Route diff --git a/docs/scenarios/file-upload-storage.md b/docs/scenarios/file-upload-storage.md index fecdd92..9264e44 100644 --- a/docs/scenarios/file-upload-storage.md +++ b/docs/scenarios/file-upload-storage.md @@ -471,7 +471,9 @@ forj route:list Expected output includes: -- `/api/v1/uploads` +```text +/api/v1/uploads +``` ## Try the Route diff --git a/docs/scenarios/json-api-route.md b/docs/scenarios/json-api-route.md index 074f9ea..6b7a783 100644 --- a/docs/scenarios/json-api-route.md +++ b/docs/scenarios/json-api-route.md @@ -11,7 +11,7 @@ This page is generated from an executable spec. An automated suite renders a fre Scenario 1 of 7 in the [verified path](/scenarios/). Plan on about 15 minutes. -This scenario adds a `GET /api/v1/users/:id` endpoint to a generated GoForj App. +This scenario adds a `GET /api/v1/users/:id` endpoint to a GoForj App. The endpoint is intentionally small. It establishes the normal shape for application features: start from the make command, keep the controller thin, put behavior behind a service, register providers explicitly, and verify the route through the generated runtime. @@ -25,7 +25,7 @@ The endpoint is intentionally small. It establishes the normal shape for applica ## Prerequisites -Start from a generated GoForj App with HTTP enabled. +Start from a GoForj App with HTTP enabled. ## Golden Path State @@ -258,7 +258,9 @@ forj route:list Expected output includes: -- `/api/v1/users/:id` +```text +/api/v1/users/:id +``` ## Try the Route diff --git a/docs/scenarios/reports-daily-schedule.md b/docs/scenarios/reports-daily-schedule.md index 04687a7..7dc4833 100644 --- a/docs/scenarios/reports-daily-schedule.md +++ b/docs/scenarios/reports-daily-schedule.md @@ -18,7 +18,8 @@ The schedule decides when daily report work should begin. The queue still owns e ## What You Will Build - `internal/reports/daily.go` selects users that need daily reports. -- `app/schedules.go` registers a named `reports:daily` schedule. +- `internal/reports/daily_schedule.go` keeps the schedule's name, interval, and handler together. +- `forj make:schedule` adds the schedule constructor to the App schedule collection without replacing `app/schedules.go`. - The schedule calls a domain-owned method instead of putting report logic in scheduler bootstrap. - The method dispatches `reports:generate` jobs, so workers continue to process report generation. @@ -69,7 +70,8 @@ internal/users/repository.go **Scheduler** ```text -app/schedules.go +internal/reports/daily_schedule.go +app/wire/inject_schedules_app.go ``` **App wiring** @@ -186,64 +188,56 @@ func (r *MemoryUserRepository) ListDailyReportTargets(_ context.Context) ([]repo } ``` -## Step 4: Import Reports into the Schedule Registry +## Step 4: Scaffold the Daily Schedule -Add the daily runner package to the app-owned schedule registry. +Use the App-owned generator so the schedule enters the existing `AppSchedules` collection without replacing the App registry or any schedules already registered there. -Update `app/schedules.go` so it includes: - -```go -"your/module/internal/reports" -"your/module/internal/schedules" -``` - -## Step 5: Add Schedule Registry Field - -Store the injected runner on the app schedule registry. - -Update `app/schedules.go` so it includes: - -```go -type ScheduleRegistry struct { - dailyReports *reports.DailyRunner +```bash +forj make:schedule reports:daily --every 24h --no-open ``` -## Step 6: Add Schedule Registry Constructor Parameter +## Step 5: Connect the Schedule to the Runner -Wire can now provide the runner to the app schedule registry. +Replace the new App-owned schedule scaffold with its real dependency and handler. Its constructor is already present in `app/wire/inject_schedules_app.go`, so Wire supplies `DailyRunner` when it builds the schedule collection. -Update `app/schedules.go` so it includes: +Create or replace `internal/reports/daily_schedule.go`: ```go -func NewScheduleRegistry( - dailyReports *reports.DailyRunner, -``` - -## Step 7: Assign Schedule Registry Runner - -Preserve generated schedule wiring and add the new field assignment. +// Package reports owns report generation and its recurring dispatch boundary. +package reports -Update `app/schedules.go` so it includes: +import ( + "context" + "time" +) -```go -return &ScheduleRegistry{ - dailyReports: dailyReports, -``` +// DailySchedule dispatches the daily report workflow on its configured interval. +type DailySchedule struct { + runner *DailyRunner +} -## Step 8: Register the Schedule +// NewDailySchedule requires the workflow that this schedule triggers. +func NewDailySchedule(runner *DailyRunner) *DailySchedule { + return &DailySchedule{runner: runner} +} -Keep the registry declarative. The registry names the schedule and points to the domain-owned method. +// Name returns the operational schedule name. +func (s *DailySchedule) Name() string { + return "reports:daily" +} -Update `app/schedules.go` so it includes: +// Interval returns how often the schedule should run. +func (s *DailySchedule) Interval() (time.Duration, error) { + return 24 * time.Hour, nil +} -```go -func (r *ScheduleRegistry) Register(s *schedules.Scheduler) error { - s.DailyAt("04:00"). - Name("reports:daily"). - Do(s.InspectTask("reports:daily", r.dailyReports.Run)) +// Handle dispatches eligible report work when the schedule is due. +func (s *DailySchedule) Handle(ctx context.Context) error { + return s.runner.Run(ctx) +} ``` -## Step 9: Wire the Runner +## Step 6: Wire the Runner The previous scenario already binds the report job to `ReportQueue`. Add the daily runner and bind the user repository to daily target lookup. @@ -255,7 +249,7 @@ reports.NewDailyRunner, wire.Bind(new(reports.DailyTargetRepository), new(*users.MemoryUserRepository)), ``` -## Step 10: Test the Runner +## Step 7: Test the Runner Create `internal/reports/daily_test.go`. @@ -327,12 +321,12 @@ go test ./... ## Verify the Schedule -For a fast local check, first edit `app/schedules.go` to use a short interval: +For a fast local check, first edit `DailySchedule.Interval` in `internal/reports/daily_schedule.go` to use a short interval: ```go -s.Every(30).Seconds(). - Name("reports:daily"). - Do(s.InspectTask("reports:daily", r.dailyReports.Run)) +func (s *DailySchedule) Interval() (time.Duration, error) { + return 30 * time.Second, nil +} ``` Rebuild after changing the schedule: @@ -347,7 +341,7 @@ With the default process-local `workerpool` driver, start the combined App so th forj app ``` -After the check, restore `DailyAt("04:00")` in `app/schedules.go` and rebuild again: +After the check, restore `24 * time.Hour` in `internal/reports/daily_schedule.go` and rebuild again: ```bash forj build diff --git a/docs/scenarios/reports-generate-job.md b/docs/scenarios/reports-generate-job.md index 1174b59..52ccd7c 100644 --- a/docs/scenarios/reports-generate-job.md +++ b/docs/scenarios/reports-generate-job.md @@ -628,7 +628,9 @@ forj route:list Expected output includes: -- `/api/v1/users` +```text +/api/v1/users +``` ## Try the Route diff --git a/docs/scenarios/runtime-observability.md b/docs/scenarios/runtime-observability.md index f637965..9f36ad6 100644 --- a/docs/scenarios/runtime-observability.md +++ b/docs/scenarios/runtime-observability.md @@ -74,7 +74,9 @@ grep -Fx LIGHTHOUSE_INSPECT_ENABLED=true .env.local Expected output includes: -- `LIGHTHOUSE_INSPECT_ENABLED=true` +```text +LIGHTHOUSE_INSPECT_ENABLED=true +``` ```bash forj route:list @@ -82,8 +84,10 @@ forj route:list Expected output includes: -- `/api/v1/users` -- `/metrics` +```text +/api/v1/users +/metrics +``` ## Trigger the Workflow diff --git a/docs/scenarios/users-created-event.md b/docs/scenarios/users-created-event.md index 7ac381e..f4577f8 100644 --- a/docs/scenarios/users-created-event.md +++ b/docs/scenarios/users-created-event.md @@ -875,7 +875,9 @@ forj route:list Expected output includes: -- `/api/v1/users` +```text +/api/v1/users +``` ## Try the Route diff --git a/docs/starter-kits.md b/docs/starter-kits.md index c18938d..afd4542 100644 --- a/docs/starter-kits.md +++ b/docs/starter-kits.md @@ -46,7 +46,7 @@ noAutoTitle: true

Account flows with real states

Sign-in, registration, password reset, profile, password, and appearance screens are - generated as app-owned source. Teams start from complete flows instead of blank forms. + created as App-owned source. Teams start from complete flows instead of blank forms.