Application Foundations

Wahoo provides explicit application foundations for request handling, configuration, sessions, tenant authorization, and rate limits.

Work in progress: Do not use Wahoo in production yet.

Start

Generated applications load visible runtime policy from internal/config.

runtime.Use(
    server.SecurityHeaders(configuration.SecurityHeaders),
    server.MaxBodyBytes(configuration.JSONBodyLimit),
)

The generated configuration uses a 1 MiB JSON limit and a small response-header policy. Review it before deployment. Wahoo does not infer CORS, trusted proxies, TLS, HSTS, or CSRF policy.

JSON APIs

Use the bounded JSON decoder for public API handlers.

var input createProjectInput
if err := api.DecodeJSON(w, r, &input, 1<<20); err != nil {
    _ = api.WriteDecodeError(w, r, err)
    return
}
if fields := api.Validate(input); len(fields) > 0 {
    _ = api.WriteError(w, r, http.StatusBadRequest, "invalid_request", "check the request fields", fields...)
    return
}

The decoder requires application/json. It rejects unknown fields, trailing JSON values, and bodies over the selected limit. Error responses include the request ID when the request passed through server.Server.

Sessions And CSRF

Store only auth.TokenHash(rawToken), never the raw session token. Use auth.AuthenticateSession to check token hash, expiry, and revocation through an application store.

session, err := auth.AuthenticateSession(r.Context(), sessions, rawToken, time.Now())

Use auth.NewCSRFToken and auth.VerifyCSRFToken only as primitives. The application must set secure cookies, select a CSRF pattern, rotate sessions, and revoke sessions at logout.

Tenant Authorization

Resolve the authenticated subject and tenant in application middleware. Do not infer them from request headers.

ctx := authz.WithScope(r.Context(), authz.Scope{
    SubjectID: "usr_123",
    TenantID:  "ten_123",
})
if err := authz.Require(ctx, authorizer, "project.read"); err != nil {
    // Map the error to the application response.
}

Every protected repository query must accept and constrain by the resolved tenant ID.

Rate Limits

Use server.RateLimit with an application-defined key. The built-in ratelimit.Memory implementation bounds its key count and is safe only for one process.

limiter, err := ratelimit.NewMemory(ratelimit.Policy{Limit: 10}, 10000)
runtime.Use(server.RateLimit(limiter, func(r *http.Request) string {
    return authenticatedUserID(r)
}))

Use a shared limiter for a multi-instance deployment. Do not trust forwarded client IP headers until the application defines trusted proxies.

Database Readiness

Wahoo does not choose a database driver or migration format. Implement database.Migrator in the application and use bounded database.Ready checks for /readyz.

Observe Requests

server.Config.Observer receives request ID, method, bounded path, status, bytes, duration, and panic state. Adapt it to the metrics or tracing system used by the application. Keep observers fast and bounded.