Backend capability map

Start here when you know the job and need the primitive: "how do I scope
rows to a user", "where does auth come from", "how do I prove the API works".
This page is a routing table, not a tutorial — one row per job, the symbols to
compose, and a command that proves it. Read the linked topic only once a row
tells you which one you need.

The UI equivalent is ui-capability-map.md.

The 20-line app

Everything below assumes this shape. It is the whole spine.

<!-- gofastr:compile
import "github.com/DonaldMurillo/gofastr/framework"
import "database/sql"
var db *sql.DB
import "github.com/DonaldMurillo/gofastr/framework/entity"
import "github.com/DonaldMurillo/gofastr/core/schema"
-->

12 lines
app := framework.NewApp(    framework.WithDB(db),              // any *sql.DB — sqlite3 or postgres    framework.WithAPIPrefix("/api"),   // optional; routes AND openapi paths move)app.Entity("tickets", entity.EntityConfig{    Table:      "tickets",    Scope:      &framework.ScopeConfig{OwnerField: "user_id"}, // per-user scoping — see the table    Fields: []schema.Field{        {Name: "title", Type: schema.String, Required: true},    },})app.Start(":8080")

framework.App.Entity is the declaration. From that one call you get the table, REST
CRUD, _batch, _events, OpenAPI, and MCP tools. You do not wire them
separately, and there is no route file to keep in sync.

Verify anything in three commands

These work against any GoFastr app and are the fastest way to answer "did
that actually do what I think".

3 lines
gofastr verify                       # contract check: routing, permissions, security, datacurl localhost:8080/healthz          # is it upcurl localhost:8080/openapi.json | jq '.paths | keys'   # every mounted API path

Every /api/… path below assumes framework.WithAPIPrefix("/api") from the
snippet above. It is optional: without it, entities mount at the bare
/tickets, and every command here works with the /api dropped. Reach for
curl localhost:8080/openapi.json | jq '.paths | keys' when you are unsure —
under either setting, a documented path is the path you request.

/openapi.json and /api/llm.md answer 401 by default — the schema is
a disclosure, so both are behind the auth gate until you pass
framework.WithPublicOpenAPI(). That is not a bug to debug; the startup
banner says so next to each URL. /metrics is likewise not mounted until
framework.WithMetrics(), and returns 404 otherwise.

Read the startup banner before adding a debug print. It lists every mounted
route, marks which need auth, and names the option that ungates them.

Jobs

Job to be doneComposeProve itDocs
CRUD over a tableframework.App.Entity with entity.EntityConfigcurl localhost:8080/api/ticketsEntity declarations
Scope rows to the signed-in userEntityConfig.Scope.OwnerFieldnot a hand-written filtergofastr verify data fails when an entity with user data lacks itEntity declarations, Access control
Login, signup, sessions, password resetbattery/auth: auth.New, auth.SessionMiddlewarecurl -i -X POST localhost:8080/auth/login -d '{...}'Set-CookieAuth
Roles and permissionsaccess.NewRolePolicy, access.Middleware, access.Wildcardgofastr verify permissionsAccess control
Signed sessions across replicasframework.WithSecret / GOFASTR_SECRET, framework.WithSecretRotationrestart the process; an existing cookie still authenticatesScaling, Auth
Schema changesauto-migration on Start, or gofastr migrate for explicit filesgofastr migrate statusMigrations
A REST client, typedgofastr generate sdkthe generated client compiles against the live specSDK
Let an agent drive the appentity MCP tools (automatic), framework.WithMCPIntrospectioncurl localhost:8080/api/llm.md (401 without framework.WithPublicOpenAPI)Agent-ready
Background work, retries, dead-letterbattery/queuequeue depth on /metrics (needs framework.WithMetrics)Queue
Scheduled workframework.Scheduler (cron)with framework.WithMetrics, /metrics shows the run counter advanceCron
React to a writebattery/webhook for outbound, framework/hook for in-transactiona hook that returns an error rolls the write backWebhooks, Hooks and transactions
Send mailbattery/emailthe SMTP backend logs the send; swap in a fake for testsEmail
File and image uploadsbattery/storage, framework/imagefieldcurl -F file=@x.png localhost:8080/api/postsStorage, Uploads
Full-text and vector searchbattery/search, battery/semanticcurl 'localhost:8080/api/posts?q=term'Search, Semantic search
Multi-tenancyframework/tenant — a declaration, not a WHERE clausegofastr verify dataMulti-tenant
Soft deleteframework/softdelete?trashed=true returns the deleted rowsEntity declarations
Rate limitingframework/ratelimitreplay a request past the limit → 429Rate limit
An admin back-officebattery/adminvisit /adminAdmin
First-boot setup on an empty DBbattery/setupstart against an empty database → setup token in the logFirst run
Logs, metrics, panicsbattery/log, framework.WithMetrics, framework.App.WithAuditLogcurl localhost:8080/metrics after framework.WithMetrics()Observability, Log, Audit log
Health and readiness probeswired by Startcurl localhost:8080/healthzHealth checks
Run more than one replicaframework.WithFanout for SSE/presence; everything else is statelesstwo processes, one database, sessions work on bothScaling
Tests with a real DB and real HTTPframework.TestHarness, framework.AutoMigrate, framework/factorygo test ./...Testkit, Factories

What not to hand-write

Each of these is a declaration, and writing it by hand is the most common way
an app ends up with a bug the framework would have prevented:

  • a WHERE user_id = ? filter → Scope.OwnerField
  • a pagination/sort/filter query string parser → framework/filter,
    framework/pagination, framework/dsl
  • a password hash and session cookie → battery/auth
  • an OpenAPI document → generated from the entity declarations
  • a retry loop around a background job → battery/queue

Common mistakes

  • Reading topic docs before this page. They are references, not
    orientation. Land on the row first, then open the one link it points at.
  • Hand-writing a filter for per-user data. Scope.OwnerField is enforced on
    every generated surface — REST, batch, MCP, includes. A hand-written WHERE
    covers the one handler you remembered. gofastr verify data flags the gap.
  • Trusting paths in a stale mental model of the spec. Under
    framework.WithAPIPrefix the OpenAPI path keys carry the prefix, so a
    documented path is the path you request. See
    API versioning.
  • Adding an option before framework.WithConfig. WithConfig replaces
    the whole AppConfig struct rather than merging into it, so any granular
    option placed before it is zeroed. Put granular options after
    WithConfig — later options win. Two guards make the mistake hard to keep:
    gofastr init scaffolds WithConfig as the first option (so pasting
    framework.WithPublicOpenAPI() anywhere below it works), and NewApp logs
    a warning naming each field an earlier option set that WithConfig zeroed
    and no later option restored. Replace semantics are deliberate: a merge
    could not tell an explicit zero from an unset field, so WithConfig could
    never turn a boolean back off.
  • Adding a route for in-page state. Sorting and paginating are islands,
    not routes. That is a UI question — see
    ui-capability-map.md.