agentleFS
NARUBROWNNARUBROWN/spine/llms.txt

spine

llms.txt104 starsChanged 3 months agoMITView on GitHub

Agent-oriented guide to understanding Spine and building correct applications with the current checkout. Spine is a Go backend framework whose method signatures are the request/response contract and whose startup wiring is explicit. This guide was verified against checkout d05b629, the untagged v0.5.2 development snapshot on main. It requires Go 1.25.5 or later. The latest tag in this checkout is v0.5.1; do not mistake the runtime banner for a released v0.5.2 tag. Use sources in this order: 1. The current checkout's…

  • Installs packages

We split this llms.txt into the rules it gives an agent

31 rules. About 0 are common, 12 are distinctive, and 19 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈12

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 19

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Use sources in this order:

    line 9
  2. Very specific:

    Do not import github.com/NARUBROWN/spine/internal/...

    line 20
  3. Very specific:

    Prefer direct and unconditional registrations in BuildApplication() (spine.App, boot.Options, error) so the CLI can analyze and update the …

    line 121
  4. Distinctive:

    Use this map instead of searching the entire repository blindly:

    line 128
  5. Very specific:

    Check errors from WebSocket().Register, Consumers().Register, Validate, Run, and RunContext.

    line 188
  6. Very specific:

    Use httperr.* or *httperr.HTTPError for expected HTTP failures.

    line 191
  7. Very specific:

    Use Spine semantic input types.

    line 192
  8. Distinctive:

    Keep Kafka/RabbitMQ consumers idempotent.

    line 193
  9. Distinctive:

    Use an outbox when a database write and external event publication must form one durable business operation.

    line 194
  10. Distinctive:

    Never enable plaintext broker compatibility in production.

    line 195
  11. Distinctive:

    Run it and call it:

    line 253
  12. Very specific:

    Use RunContext(ctx, opts) instead of Run(opts) when a test or service host owns cancellation.

    line 260
  13. Very specific:

    When validation returns *boot.ConfigError, inspect Issues and use each issue's Code as the stable machine-readable value.

    line 262
  14. Distinctive:

    If multiple registered concrete providers implement the same requested interface, resolution fails as ambiguous.

    line 283
  15. Very specific:

    Use route.WithInterceptors for route-only policies:

    line 323
  16. Very specific:

    Use *httperr.HTTPError for another status:

    line 395
  17. Very specific:

    Every core.Interceptor must implement all four methods:

    line 409
  18. Very specific:

    Use App.RegisterTransport only for an independent custom transport implementing core.CustomTransport (Init, Start, and Stop).

    line 458
  19. Very specific:

    Prefer func (c *ChatController) OnMessage(ctx context.Context, id ws.ConnectionID, command ChatCommand) error.

    line 481
  20. Very specific:

    Default active-plus-pending connection capacity is boot.DefaultWebSocketMaxConnections (1024).

    line 503
  21. Very specific:

    Use a positive limit or explicit boot.UnlimitedWebSocketConnections.

    line 504
  22. Distinctive:

    Write the business row and outbox row in one database transaction.

    line 584
  23. Distinctive:

    Default handler failure policy rejects without requeue.

    line 599
  24. Distinctive:

    If Kafka and RabbitMQ writers are both enabled, a queued event is dispatched to both.

    line 602
  25. Very specific:

    Use EnableGracefulShutdown: true for signal-driven HTTP shutdown.

    line 614
  26. Distinctive:

    Do not log broker URLs with embedded credentials or secret tokens.

    line 617
  27. Distinctive:

    Do not trust forwarded headers unless the immediate peer matches an exact configured proxy CIDR.

    line 618
  28. Very specific:

    Read docs/security-configuration.md and the latest applicable migration guide before changing production defaults.

    line 620
  29. Very specific:

    Check every registration/startup error.

    line 649
  30. Very specific:

    Verify WebSocket origins/auth/capacity, secure broker transport, poison-message behavior, and durable consumer deduplication.

    line 650
  31. Very specific:

    Run relevant broker/database/WebSocket E2E separately.

    line 662

≈ “Common” is an estimate: at least 1 in 5 of the 3,225 llms.txt files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
llms.txt
# Spine

> Agent-oriented guide to understanding Spine and building correct applications with the current checkout. Spine is a Go backend framework whose method signatures are the request/response contract and whose startup wiring is explicit.

## Read this first

This guide was verified against checkout `d05b629`, the untagged `v0.5.2` development snapshot on `main`. It requires Go `1.25.5` or later. The latest tag in this checkout is `v0.5.1`; do not mistake the runtime banner for a released `v0.5.2` tag.

Use sources in this order:

1. The current checkout's public API and tests are authoritative.
2. Local executable examples and migration/security documents explain intended behavior.
3. The three external demo repositories show complete application architecture.
4. The external Spine CLI can scaffold projects and inspect explicit execution contracts, but its output is
   separate evidence with the limits described below.
5. README prose and generated Swagger files are supporting material; when they disagree with code, follow code.

The external demos reviewed on 2026-07-30 all pin Spine `v0.5.1`. Their architecture is useful, but reconcile copied code with this checkout before using it. The public usage style remains compatible with `v0.5.2`; the newer checkout rejects malformed requests and invalid registrations earlier.

Do not import `github.com/NARUBROWN/spine/internal/...` from an application. Application code should use the root package, `core`, `interceptor/...`, and `pkg/...` only.

For an external module, pin the intended version or commit explicitly. To match this guide's current source snapshot, use:

```sh
go get github.com/NARUBROWN/spine@d05b629
```

The linked demos intentionally pin the released `v0.5.1`; keep their pin when reproducing them exactly, and do not use a `v0.5.2`-only API without upgrading their module first.

## Choose the right reference first

| Goal | Reference | Start with |
|---|---|---|
| Learn the current minimal runtime and secure defaults | This repository | `cmd/demo`, `examples/security-config`, `test/app_integration_test.go` |
| Scaffold a project, inspect execution contracts, or generate OpenAPI/AsyncAPI | [spine-cli](https://github.com/NARUBROWN/spine-cli/tree/a9fff677b385a59df65b0befcde6c410239a8c83) | `README.md`, `llms.txt`, `internal/cli/agent.go`, `internal/cli/contract.go` |
| CRUD, Bun ORM, request transactions, and Swagger UI | [spine-user-demo](https://github.com/NARUBROWN/spine-user-demo/tree/b07d2bb35d6e0b499c6f6974e1d34f3a4fafffba) | `main.go`, `routes/user_routes.go`, `interceptor/tx_interceptor.go`, `repository/user_repository.go` |
| Kafka request/event flow, idempotency, poison messages, and outbox boundaries | [spine-simple-msa-demo](https://github.com/NARUBROWN/spine-simple-msa-demo/tree/aa3f0fae8057ec0999fadaafc9295b7c7987afbb) | `order-app/main.go`, `stock-app/main.go`, `shared/events`, `scenarios/outbox` |
| Authenticated WebSocket chat, rooms, and runtime limits | [spine-simple-chat-demo](https://github.com/NARUBROWN/spine-simple-chat-demo/tree/b1fe598329eb79828eb4c84903c7fa3847d35914) | `main.go`, `interceptor/auth_interceptor.go`, `controller/chat_controller.go`, `e2e/websocket_e2e_test.go` |

`Bun ORM` means the Go ORM at `github.com/uptrace/bun`; it does not mean the Bun JavaScript runtime.

## Spine CLI

The separate [NARUBROWN/spine-cli](https://github.com/NARUBROWN/spine-cli) repository was reviewed at
commit `a9fff677b385a59df65b0befcde6c410239a8c83` on 2026-08-03. Release `v0.1.4` requires Go 1.25.5.
Installing the module root creates `spine-cli`; installing `cmd/spine` creates the shorter `spine` command:

```sh
go install github.com/NARUBROWN/spine-cli@v0.1.4
spine-cli version

# Or install the short command used in the examples below.
go install github.com/NARUBROWN/spine-cli/cmd/spine@v0.1.4
spine version
```

Both version commands print `spine-cli 0.1.4`. Projects created by this release pin Spine `v0.5.1`; they do
not automatically use this checkout's untagged `v0.5.2` development source. When applying CLI output to
another Spine version, inspect the generated `go.mod`, run the contract checks, and reconcile API changes
against that exact Spine source.

### What the CLI covers

- `new` creates a project with `BuildApplication`, `app.Validate`, `GET /health`, and `spine.toml`.
- `generate`/`g` creates resources, vertical slices, controllers, services, repositories, consumers, and
  WebSockets and normally registers them in the composition root.
- `inspect` and `explain` report a static-source contract for HTTP, DI, interceptors, consumers, WebSockets,
  resources, and supported boot options.
- `contract check`, snapshot/diff, and OpenAPI/AsyncAPI 3.1 export validate or materialize that static contract.
- `verify` runs the static contract gate, gofmt status check, `go vet ./...`, and `go test ./...`.
- `schema`, `capabilities`, and `context` provide versioned JSON intended for agents and automation.

The generators intentionally do not design domain aggregates, ORM mappings, production persistence,
transactions/outbox, broker retry/DLQ semantics, or WebSocket security/capacity policy. Kafka, RabbitMQ, and
Bun-specific generators are not implemented in `v0.1.4`. A generated consumer or WebSocket handler returns an
explicit `not implemented` error until application logic replaces it; contract validation should fail until
then. The vertical slice's memory storage is development-only.

### Agent-safe workflow

Discover the installed CLI instead of relying on memorized flags, then inspect the target project without
changing it:

```sh
spine schema --json
spine capabilities --json
spine context --root <project-directory> --json
spine inspect --root <project-directory> --json
```

JSON-capable commands wrap success and failure in `spine.cli-result/v1`; read `ok`, `exit_code`, `result`, and
any structured `error` even when the process exits non-zero. Treat `inspect` as observation, not a gate: check
`analysis_complete` and diagnostics, and use `contract check` or `verify` for validation.

For any generated change, use optimistic concurrency rather than immediately writing or forcing files:

```sh
spine g resource order --root <project-directory> --dry-run --json
# Review result.changes, result.diff, and result.base_hash.
spine g resource order --root <project-directory> --if-match <base_hash> --json
spine contract check --root <project-directory> --warnings-as-errors --json
spine verify --root <project-directory> --warnings-as-errors --json
```

The default is to refuse overwriting an existing target. Use `--force` only after reviewing a
`--force --dry-run --json` plan. If `--if-match` reports a conflict, create and review a new plan instead of
blindly retrying. `generate` also uses a project-level `.spine-generate.lock`; do not delete a lock without
checking its type, owner, and age.

### Evidence boundaries

- `inspect`, `explain`, and `contract check` use static source analysis. Conditional, dynamic, helper-hidden,
  or otherwise unresolved registration is reported with diagnostics and `analysis_complete: false`.
- `verify` proves only the static contract plus local formatting, vet, and tests. It does not prove database,
  broker, listener, deployment, or race-detector behavior.
- `contract probe` executes project code through `BuildApplication`, runs `app.Validate`, and compares only
  public consumer and WebSocket registries. It cannot reconcile HTTP routes, DI constructors, or interceptors,
  and it can create temporary files, run Go tooling, resolve modules, or trigger composition-root side effects.
- `doctor` is offline. External infrastructure readiness and deployed behavior require separate E2E evidence.

Prefer direct and unconditional registrations in `BuildApplication() (spine.App, boot.Options, error)` so the
CLI can analyze and update the composition root. For the complete command protocol, exit semantics, schemas,
and current limitations, use the CLI repository's own
[`llms.txt`](https://github.com/NARUBROWN/spine-cli/blob/a9fff677b385a59df65b0befcde6c410239a8c83/llms.txt).

## Repository map

Use this map instead of searching the entire repository blindly:

- `app.go`: public `spine.App` assembly API.
- `pkg/boot`: HTTP, WebSocket, Kafka, RabbitMQ, lifecycle, and validation options.
- `pkg/path`, `pkg/query`, `pkg/header`, `pkg/multipart`: supported HTTP handler inputs.
- `pkg/httpx`, `pkg/httperr`: supported HTTP outputs and expected HTTP errors.
- `pkg/ws`: WebSocket connection identity and sending.
- `pkg/event/publish`: domain-event contract and request-scoped publishing.
- `pkg/route`: route-level interceptor options.
- `core/interceptor.go`, `core/context.go`: interceptor and context contracts.
- `interceptor/cors`: built-in CORS interceptor.
- `cmd/demo`: a local application covering HTTP, files, events, WebSocket, CORS, and secure configuration.
- `examples/security-config`: compact secure configuration example.
- `test`: end-to-end HTTP and WebSocket usage.
- `docs/migration`: version-specific behavioral changes.
- `docs/security-configuration.md`: production security expectations.

## Mental model

Application assembly is explicit:

```text
constructors + routes + interceptors + transport registrations
                         |
                         v
                  App.Validate(options)
                         |
                         v
        Run / RunContext repeats static preflight
                         |
                         v
       DI warm-up + registered read-consumer readiness
                         |
                         v
         consumers/custom transports, then HTTP listener
```

A successful HTTP request follows this framework-controlled order:

```text
global PreHandle
  -> route selection
  -> route PreHandle
  -> argument resolution
  -> controller method
  -> response preparation and validation
  -> queued domain-event dispatch
  -> PostHandle in reverse order
  -> BeforeResponse in reverse order
  -> transport write
  -> AfterCompletion in reverse order
```

On an error, event dispatch and `PostHandle` are skipped; `BeforeResponse`, error rendering, transport write, and `AfterCompletion` finish the failure path. Global `PreHandle` runs before route selection, so its `HandlerMeta` may be empty. Route `PreHandle` runs before body, multipart, and path argument resolution. Put authentication and early rejection there; do not make an interceptor depend on already-resolved controller arguments.

## Non-negotiable rules

- Register every DI-created controller, consumer, repository, service, and interceptor constructor with `app.Constructor`.
- A constructor is a function that returns exactly one value. It cannot return `(T, error)`, and neither the constructor nor its returned pointer may be nil.
- Register handlers as pointer method expressions such as `(*UserController).Get`, never as bound methods such as `controller.Get`.
- Check errors from `WebSocket().Register`, `Consumers().Register`, `Validate`, `Run`, and `RunContext`.
- Enable HTTP explicitly with `HTTP: &boot.HTTPOptions{}`. `HTTP: nil` means no HTTP server.
- Return `httpx.Response[T]`, `httpx.Binary`, or `httpx.Redirect` for successful HTTP responses. Do not return a raw DTO or raw string.
- Use `httperr.*` or `*httperr.HTTPError` for expected HTTP failures. Generic errors become a non-leaking HTTP 500.
- Use Spine semantic input types. Do not replace `path.Int`, `path.String`, or `path.Boolean` with primitive handler arguments.
- Keep Kafka/RabbitMQ consumers idempotent. Delivery is at least once, not exactly once.
- Use an outbox when a database write and external event publication must form one durable business operation.
- Never enable plaintext broker compatibility in production.

## Minimal HTTP application

```go
package main

import (
	"log"
	"time"

	"github.com/NARUBROWN/spine"
	"github.com/NARUBROWN/spine/pkg/boot"
	"github.com/NARUBROWN/spine/pkg/httperr"
	"github.com/NARUBROWN/spine/pkg/httpx"
	"github.com/NARUBROWN/spine/pkg/path"
)

type Health struct {
	ID     int64  `json:"id"`
	Status string `json:"status"`
}

type HealthController struct{}

func NewHealthController() *HealthController {
	return &HealthController{}
}

func (c *HealthController) Get(id path.Int) (httpx.Response[Health], error) {
	if id.Value <= 0 {
		return httpx.Response[Health]{}, httperr.BadRequest("id must be positive")
	}
	return httpx.Response[Health]{
		Body: Health{ID: id.Value, Status: "ok"},
	}, nil
}

func main() {
	app := spine.New()
	app.Constructor(NewHealthController)
	app.Route("GET", "/health/:id", (*HealthController).Get)

	opts := boot.Options{
		Address:                ":8080",
		EnableGracefulShutdown: true,
		ShutdownTimeout:        10 * time.Second,
		HTTP:                   &boot.HTTPOptions{},
	}
	if err := app.Validate(opts); err != nil {
		log.Fatal(err)
	}
	if err := app.Run(opts); err != nil {
		log.Fatal(err)
	}
}
```

Run it and call it:

```sh
go run .
curl -i http://localhost:8080/health/42
```

Use `RunContext(ctx, opts)` instead of `Run(opts)` when a test or service host owns cancellation. `Run` and `RunContext` perform the same static validation as `Validate`; calling `Validate` separately is still useful in CI and deployment preflight.

When validation returns `*boot.ConfigError`, inspect `Issues` and use each issue's `Code` as the stable machine-readable value. Do not parse the human-readable message.

```go
var configErr *boot.ConfigError
if errors.As(err, &configErr) {
	for _, issue := range configErr.Issues {
		log.Printf("%s [%s]: %s", issue.Path, issue.Code, issue.Message)
	}
}
```

`Validate` checks supported static options and registrations without network access. DI construction and broker readiness occur later in `Run`/`RunContext`, before the HTTP listener is opened.

## Dependency injection

Dependencies are constructor parameters. For example, `NewUserController(service *UserService)` depends on `NewUserService(repo UserRepository)`, which depends on a single registered concrete repository implementing that interface. Register the whole chain with `app.Constructor(NewBunDB, NewUserRepository, NewUserService, NewUserController)`. Providers are lazy singletons and controllers/consumers are warmed up before traffic starts.

DI details agents commonly miss:

- A concrete provider can satisfy one implemented interface automatically.
- Resolving that provider through its concrete type and interface returns the same singleton.
- If multiple registered concrete providers implement the same requested interface, resolution fails as ambiguous.
- Circular dependencies fail with an explicit dependency path.
- A constructor panic is converted to a startup error.
- Direct interceptor instances do not need constructor registration. DI-resolved interceptors do.
- A typed nil interceptor such as `(*TxInterceptor)(nil)` is a type token asking the container for that type. Register its constructor first.

Example:

```go
app.Constructor(NewTxInterceptor)
app.InterceptorFor(
	boot.InterceptorHTTP,
	(*TxInterceptor)(nil),
)
```

## HTTP routing and method signatures

Register a route with:

```go
app.Route("GET", "/users/:user_id", (*UserController).Get)
```

HTTP methods are trimmed and uppercased. Route paths use `:name` parameters. Path semantic arguments consume path parameters from left to right, not by Go parameter name:

```go
app.Route("GET", "/orgs/:org_id/users/:user_id", (*UserController).Get)

func (c *UserController) Get(orgID path.Int, userID path.Int) httpx.Response[User] {
	// orgID binds :org_id; userID binds :user_id.
}
```

Static and parameter routes that overlap for the same method are rejected as ambiguous during boot. A global prefix must be static, start with `/`, and contain neither `:` nor `*`.

```go
HTTP: &boot.HTTPOptions{GlobalPrefix: "/api/v1"}
```

Use `route.WithInterceptors` for route-only policies:

```go
app.Route(
	"POST",
	"/admin/users",
	(*AdminController).Create,
	route.WithInterceptors(&AdminAuthInterceptor{}),
)
```

### Supported HTTP inputs

| Handler parameter | Value supplied by Spine | Important rule |
|---|---|---|
| `context.Context` | Request context plus request-scoped event publisher | Use this with `publish.Event` and database calls. |
| `path.Int` | Parsed `int64` from the next `:segment` | Read `.Value`; malformed input returns HTTP 400. |
| `path.String` | Raw string from the next `:segment` | Read `.Value`. |
| `path.Boolean` | Parsed boolean from the next `:segment` | Accepts true/false forms supported by Spine; malformed input returns HTTP 400. |
| `query.Values` | All query values | `Int(key, default)` falls back on missing or invalid input; validate business ranges yourself. |
| `query.Pagination` | `page` and `size` | Defaults to page 1 and size 20; validate non-positive or excessive values yourself. |
| `header.Values` | HTTP headers | Use `Get` and `Has`. |
| `*SomeDTO` with no `form` tags | Bound request DTO | HTTP DTOs must be pointers to structs; the Echo binder and request `Content-Type` select the body format. Malformed input returns HTTP 400. |
| `*SomeForm` with at least one `form` tag | Bound form DTO | The presence of a form tag selects form binding. |
| `multipart.UploadedFiles` | Uploaded file descriptors | Call each file's `Open` and close the returned stream. |
| `pkg/spine.Ctx` or `core.ControllerContext` | Read-only view of values set by interceptors | Only `Get` is exposed; custom interfaces with extra methods are unsupported. |

Arbitrary primitive parameters and application service parameters are not controller arguments. Put services on the controller through constructor injection.

### Supported HTTP outputs

| Return value | Result |
|---|---|
| `httpx.Response[T]` or `*httpx.Response[T]` | JSON for non-string `T`; plain text for string `T`; default status 200. |
| `httpx.Binary` or `*httpx.Binary` | Byte response with explicit content type; default status 200. |
| `httpx.Redirect` or `*httpx.Redirect` | `Location` response; default status 302. |
| `error` | Expected `httperr.HTTPError` or sanitized HTTP 500. |
| `(supportedResponse, error)` | Response on nil error; error response otherwise. |

`httpx.ResponseOptions` controls status, headers, and cookies:

```go
return httpx.Response[User]{
	Body: user,
	Options: httpx.ResponseOptions{
		Status:  http.StatusCreated,
		Headers: map[string]string{"Location": fmt.Sprintf("/users/%d", user.ID)},
		Cookies: []httpx.Cookie{{
			Name:     "session",
			Value:    httpx.EncodeCookieValue(token),
			Path:     "/",
			HttpOnly: true,
			Secure:   true,
			SameSite: httpx.SameSiteLax,
		}},
	},
}, nil
```

Spine validates status, JSON serialization, and cookies before `BeforeResponse`. Invalid cookie names, values, `SameSite`, or `Priority` fail without partially committing a success response. Use `httpx.EncodeCookieValue`/`DecodeCookieValue` for arbitrary Unicode or delimiter-containing values.

### HTTP errors

Built-in helpers are:

```go
httperr.BadRequest("...")
httperr.Unauthorized("...")
httperr.NotFound("...")
httperr.InternalServerError("...")
```

Use `*httperr.HTTPError` for another status:

```go
return httpx.Response[User]{}, &httperr.HTTPError{
	Status:  http.StatusConflict,
	Message: "email already exists",
	Cause:   err,
}
```

Expected errors serialize as `{"message":"..."}`. Generic errors return status 500 and `{"message":"Internal server error"}`; their original text is not exposed to clients.

## Interceptors

Every `core.Interceptor` must implement all four methods:

```go
type Interceptor interface {
	PreHandle(ctx core.ExecutionContext, meta core.HandlerMeta) error
	PostHandle(ctx core.ExecutionContext, meta core.HandlerMeta)
	BeforeResponse(ctx core.ExecutionContext, meta core.HandlerMeta, executionErr error) error
	AfterCompletion(ctx core.ExecutionContext, meta core.HandlerMeta, err error)
}
```

- `app.Interceptor(x)` applies `x` to HTTP and WebSocket messages.
- `app.InterceptorFor(boot.InterceptorHTTP, x)` applies only to HTTP.
- `app.InterceptorFor(boot.InterceptorWebSocket, x)` applies only to WebSocket messages and handshake auth when implemented.
- `boot.InterceptorAll` means HTTP plus WebSocket, not Kafka/RabbitMQ consumers.
- `route.WithInterceptors` applies only to that HTTP route.
- Reusing the same interceptor pointer across scopes merges its scopes and executes that instance once. Different instances of the same type execute independently.

For CORS, prefer startup validation and scope it to HTTP:

```go
corsInterceptor, err := cors.NewValidated(cors.Config{
	AllowOrigins:     []string{"https://app.example.com"},
	AllowMethods:     []string{"GET", "POST", "OPTIONS"},
	AllowHeaders:     []string{"Authorization", "Content-Type"},
	AllowCredentials: true,
})
if err != nil {
	log.Fatal(err)
}
app.InterceptorFor(boot.InterceptorHTTP, corsInterceptor)
```

Credentialed CORS cannot use wildcard origin `*`.

### Request transaction pattern

The Bun demo uses this safe boundary:

1. `PreHandle` begins a transaction and stores `bun.Tx` in `ExecutionContext`.
2. The controller reads it through `pkg/spine.Ctx.Get("tx")` and passes it as `bun.IDB` to the service/repository.
3. `BeforeResponse` rolls back when `executionErr != nil`; otherwise it commits.

Response serialization, cookie validation, event dispatch, and post-processing happen before `BeforeResponse`, so their framework-controlled failures can trigger rollback. This also means a broker event can be published before the transaction commit, and a later commit failure cannot recall it. A database commit is atomic with neither broker publication nor the physical socket write. Use an outbox for database-plus-broker atomicity.

## Swagger and transport extensions

Spine does not generate OpenAPI automatically. The Bun demo uses Swaggo annotations and generated `docs/docs.go`, `docs/swagger.json`, and `docs/swagger.yaml`, imports that generated package for registration, then uses `app.Transport` to type-assert `*echo.Echo` and mount `httpSwagger.WrapHandler` at `/swagger/*`. Keep annotations and generated `docs/*` in sync. See the demo's `main.go` for the copy-safe imports and hook; generated Swagger is supporting output, not the authority for route behavior.

Use `App.RegisterTransport` only for an independent custom transport implementing `core.CustomTransport` (`Init`, `Start`, and `Stop`).

## WebSocket

Register WebSocket handlers with method expressions and check the registration error:

```go
app.Constructor(NewChatController)
if err := app.WebSocket().Register(
	"/ws/chat",
	(*ChatController).OnMessage,
); err != nil {
	log.Fatal(err)
}
```

Supported WebSocket handler inputs are:

- `context.Context`
- `ws.ConnectionID`
- raw `[]byte`
- a non-pointer struct decoded from the JSON message

Prefer `func (c *ChatController) OnMessage(ctx context.Context, id ws.ConnectionID, command ChatCommand) error`. Use `ws.Send(ctx, ws.TextMessage, encoded)` for the current connection. A non-nil handler error closes with WebSocket code 1011; to keep the socket open for invalid application input, send a typed error event and return nil.

`ws.Send` sends only to the current connection. For broadcast or rooms, obtain `ws.Sender` from `ctx.Value(ws.SenderKey)`, store senders in shared controller state, copy recipients while locked, then send after releasing the lock.

Spine currently has no application-level connect/disconnect callback. A sender kept by application state may only be noticed as stale on a later failed send. Do not treat an in-memory sender map as durable presence; use an external presence store with expiry for multi-instance or strict presence semantics.

### WebSocket handshake authentication

Authentication before upgrade belongs in one interceptor instance that implements the four `core.Interceptor` methods plus `core.WebSocketHandshakeInterceptor.PreHandshake`. Register that instance with `app.InterceptorFor(boot.InterceptorWebSocket, auth)`. In `PreHandshake`, read headers/query/cookies from `core.WebSocketHandshakeContext` and return a 4xx `httperr` on rejection. Copy the complete constant-time token implementation from the chat demo's [`interceptor/auth_interceptor.go`](https://github.com/NARUBROWN/spine-simple-chat-demo/blob/b1fe598329eb79828eb4c84903c7fa3847d35914/interceptor/auth_interceptor.go), not a partial interface implementation.

A connection slot is reserved before `PreHandshake`; rejected handshakes release it. `PreHandshake` runs before HTTP upgrade. Message-level `PreHandle` still runs for every accepted message.

Inside a message interceptor, type-assert `core.ExecutionContext` to `core.WebSocketMessageContext` to inspect the immutable handshake headers, query values, cookies, remote address, host, and request URI. These request-view interfaces are not controller handler parameters.

### WebSocket runtime options

WebSocket requires non-nil `HTTP`. Configure `HTTP.WebSocket` fields such as `AllowedOrigins`, `TrustedProxyCIDRs`, `MaxConnections`, `MaxMessageBytes`, `HandshakeTimeout`, `ReadTimeout`, `WriteTimeout`, and `PingInterval`.

- Empty `AllowedOrigins` performs scheme-and-host same-origin checks when an `Origin` header is present; non-browser clients without `Origin` are allowed.
- Exact production origins are preferred. `*` allows every origin.
- Trusted proxies must remove or overwrite client-supplied forwarding headers.
- Zero limits/timeouts select bounded framework defaults.
- Default active-plus-pending connection capacity is `boot.DefaultWebSocketMaxConnections` (1024).
- Use a positive limit or explicit `boot.UnlimitedWebSocketConnections`.
- Capacity rejection is HTTP 503 with `Retry-After` and code `WEBSOCKET_CAPACITY_EXCEEDED`.
- Oversized messages close with WebSocket code 1009.

## Domain events and Kafka

An event must implement:

```go
type DomainEvent interface {
	Name() string
	OccurredAt() time.Time
}
```

Example event:

```go
type OrderCreated struct {
	EventID string    `json:"event_id"`
	OrderID int64     `json:"order_id"`
	At      time.Time `json:"at"`
}

func (e OrderCreated) Name() string         { return "order.created" }
func (e OrderCreated) OccurredAt() time.Time { return e.At }
```

Queue it from an HTTP, WebSocket, or consumer handler's `context.Context`:

```go
publish.Event(ctx, OrderCreated{
	EventID: eventID,
	OrderID: orderID.Value,
	At:      time.Now().UTC(),
})
```

`publish.Event` adds the event to the current execution context. Spine dispatches queued events only after the handler succeeds and, for HTTP, after response preparation succeeds. The function returns no error. If no writer is configured, the handler can still succeed while nothing is delivered externally, so every publishing process must enable `Kafka.Write` or `RabbitMQ.Write` deliberately.

Use a consumer signature such as `func (c *OrderConsumer) OnCreated(ctx context.Context, eventName string, event OrderCreated) error`. Register `NewOrderConsumer` with `app.Constructor`, then check the error from `app.Consumers().Register("order.created", (*OrderConsumer).OnCreated)`. Consumer inputs are `context.Context`, event name `string`, raw `[]byte`, and a non-pointer struct decoded from JSON. A topic can have only one registered handler; make it idempotent by stable event ID.

### Kafka configuration

Producer and consumer in one process:

```go
Kafka: &boot.KafkaOptions{
	Brokers: []string{"kafka.example.com:9093"},
	Read: &boot.KafkaReadOptions{
		GroupID: "order-service",
	},
	Write: &boot.KafkaWriteOptions{
		TopicPrefix: "prod.",
	},
}
```

Spine creates TLS 1.2+ transport by default. `TLS` is shared, `Dialer` configures consumer/read transport, and `Transport` configures publisher/write transport. A process that both reads and writes must secure both directions when using custom SASL/TLS. Use this only for an isolated plaintext development broker:

```go
AllowInsecureTransport: true
```

`KafkaWriteOptions.TopicPrefix` is simple string concatenation and adds no separator. If a producer uses `demo.`, register the consumer topic as `demo.order.created` yourself.

Kafka behavior:

- Framework-managed writes require acknowledgements from all in-sync replicas.
- Consumption is at least once; use a stable event ID and durable deduplication.
- A handler error causes NACK/reconstruction behavior and can redeliver the event.
- `ConsumerRetry` controls transport/reader reconstruction, not an application handler retry budget.
- A permanently failing poison record can block the partition. Spine does not automatically skip it or choose a retry topic/DLQ.
- A consumer-only process may leave `HTTP` nil and still handles `SIGINT`/`SIGTERM`.
- Shutdown waits for active handlers, ACK/NACK work, and readers to drain.

### Transactional outbox boundary

`publish.Event` is request-scoped publication, not a database transaction. For durable database-plus-event behavior:

1. Write the business row and outbox row in one database transaction.
2. Give the event a stable unique ID.
3. A separate relay publishes pending rows with broker acknowledgements.
4. Mark the row published after acknowledgement.
5. Accept that a crash between publish and marking can duplicate delivery.
6. Deduplicate in every downstream consumer.

The MSA demo's `scenarios/outbox` illustrates the boundary with a JSON-file store and direct `kafka-go`; it is educational, not a production multi-process database outbox.

## RabbitMQ variant

The same `publish.Event` and `Consumers().Register` APIs use RabbitMQ when `boot.Options.RabbitMQ` configures `RabbitMqReadOptions` and/or `RabbitMqWriteOptions` with a named exchange.

- `amqps://` is required by default; `amqp://` needs local-only `AllowInsecureTransport: true`.
- Default prefetch is 1.
- Default handler failure policy rejects without requeue.
- Provision the dead-letter exchange before referencing it.
- Publisher confirms and retries are still at least once; consumers must remain idempotent.
- If Kafka and RabbitMQ writers are both enabled, a queued event is dispatched to both.

## Runtime and security defaults

Zero values select bounded defaults for HTTP timeouts, header size, request body size, WebSocket size/timeouts/capacity, and shutdown timeout. Broker retry delays are bounded by default, but Kafka/RabbitMQ consumer `MaxAttempts: 0` means unlimited transport retries; RabbitMQ publisher zero values use a bounded three-attempt default.

Important exceptions and rules:

- `HTTPOptions.MaxBodyBytes < 0` explicitly disables the request-body limit.
- Invalid JSON/form/multipart and invalid `path.Int`/`path.Boolean` produce HTTP 400.
- Request bodies above the configured maximum produce HTTP 413.
- `DisableRecover: false` (the zero value) keeps recovery enabled.
- Use `EnableGracefulShutdown: true` for signal-driven HTTP shutdown.
- Zero `ShutdownTimeout` uses the 10-second framework default; set a deliberate positive value when the application's HTTP/custom-transport drain budget differs.
- `ShutdownTimeout` does not forcibly cap consumer handler/ACK/NACK drain; consumer `Stop` waits for that runtime to finish.
- Do not log broker URLs with embedded credentials or secret tokens.
- Do not trust forwarded headers unless the immediate peer matches an exact configured proxy CIDR.

Read `docs/security-configuration.md` and the latest applicable migration guide before changing production defaults.

## External example details

### Bun ORM + Swagger: spine-user-demo

Reviewed snapshot: [`b07d2bb`](https://github.com/NARUBROWN/spine-user-demo/tree/b07d2bb35d6e0b499c6f6974e1d34f3a4fafffba), Go `1.25.5`, Spine `v0.5.1`.

Follow `HTTP -> controller -> service -> repository -> Bun -> MySQL`. Together, `main.go`, `routes/user_routes.go`, `interceptor/tx_interceptor.go`, `repository/user_repository.go`, and the transaction E2E show DI, `bun.IDB` reuse across `*bun.DB`/`bun.Tx`, response-aware rollback, error mapping, and Swaggo UI. Set `SPINE_DEMO_MYSQL_DSN`, run `go run . -migrate` before the first `go run .`, and open `/swagger/index.html`. Actual CRUD uses `/users?id=1`, not `/users/:id`, with no list endpoint; prefer code over mismatching README/generated fields.

### Kafka MSA: spine-simple-msa-demo

Reviewed snapshot: [`aa3f0fa`](https://github.com/NARUBROWN/spine-simple-msa-demo/tree/aa3f0fae8057ec0999fadaafc9295b7c7987afbb), Go `1.25.5`, Spine `v0.5.1`.

The repository name ends in `-demo`, but its `go.mod` module/import path is `github.com/NARUBROWN/spine-simple-msa`. Do not rewrite imports to add `-demo`.

The flow is `POST order -> order.created -> stock idempotency decision -> stock.reserved|stock.rejected -> order result consumer -> GET status`. Read both app `main.go` files, `shared/events`, and `scenarios/outbox`; run the stock worker then order HTTP service. Docker E2E uses Redpanda port `19092`, while manual defaults use Kafka `9092`. Topic prefix must include its separator and match both processes. JSON/in-memory stores are educational; production provisioning, keys, retention, schemas, retry topics, and DLQ remain application work.

### WebSocket chat: spine-simple-chat-demo

Reviewed snapshot: [`b1fe598`](https://github.com/NARUBROWN/spine-simple-chat-demo/tree/b1fe598329eb79828eb4c84903c7fa3847d35914), Go `1.25.5`, Spine `v0.5.1`.

This single-process in-memory server implements authenticated `join -> message -> leave`, one room per connection, room broadcast, typed errors, and runtime limit tests. Read its main, auth interceptor, controller, and E2E. Query `token` or exact `Authorization: Bearer ...` authenticates the handshake; the header wins, and blank config falls back to public `demo-token`, so production must override it and use `wss://`. There is no frontend, persistence, multi-instance state, or user/room authorization. Disconnect cleanup waits for a failed send; 1000-rune logical messages can exceed the 4096-byte frame limit; `leave` keeps the socket open. Build-tagged E2E requires a running server and `go test -tags=e2e -v ./e2e`.

## Agent implementation checklist

- Identify required transports, then read the closest example's actual `main.go`, handler, test, and `go.mod`.
- Define DTO/signature contracts, DI direction, idempotency key, transaction/outbox boundary, retry/DLQ policy, and shutdown behavior before wiring routes.
- Centralize assembly; use semantic inputs, supported outputs, narrow interceptor scopes, and environment-backed secrets.
- Check every registration/startup error; enable `HTTP` only where a listener is intended.
- Verify WebSocket origins/auth/capacity, secure broker transport, poison-message behavior, and durable consumer deduplication.
- Remember that `Validate` does not replace DI warm-up, route-ambiguity checks, or live consumer readiness in `Run`.

For Go changes, run:

```sh
gofmt -w <changed-go-files>
go test ./...
go test -race ./...
go vet ./...
```

Run relevant broker/database/WebSocket E2E separately. Translate POSIX environment syntax for PowerShell:

```powershell
$env:SPINE_DEMO_KAFKA_BROKER = "localhost:19092"
go run ./stock-app
```

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

Reviews can't be read right now.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.