App layout and rules
This app follows the apistock layered module structure. The rules below are checked by internal/app/architecture_test.go, so go test ./... fails when they are broken.
Layout#
cmd/api/ entry point: config → app → run ("api openapi" exports the spec)
cmd/migrate/ applies db/migrations, then the job queue's migrations
cmd/seed/ development seed data: an administrator and example projects
db/migrations/ one ordered goose history, including apistock module tables
internal/app/ composition root: builds, wires, runs and shuts down the app
config.go boot configuration: secrets and infrastructure from environment variables
settings.go runtime settings: tunables edited through /ops/settings
jobs.go one line per background job (//aps:anchor jobs)
job_<name>.go declares one job and its default configuration
app.go construction order and lifecycle
routes.go API, health, docs and the middleware chain
permissions.go permissions and platform roles (platform_admin, ops_viewer)
admin.go grant-role, revoke-role and roles commands (cmd/api)
admin_mfa.go reset-mfa and rotate-auth-keys commands (cmd/api)
commands.go database, audit and auth wiring shared by commands
keys.go AUTH_ENCRYPTION_KEYS: the keyring for two-factor authentication secrets
passkeys.go WEBAUTHN_*: the passkey relying party and the /.well-known files for apps
social.go GOOGLE_*, APPLE_*, APP_PUBLIC_URL: Google and Apple sign-in providers
providers.go sign-in method status: printed at start, auth-providers, /ops/auth/providers
seed.go development seed data (cmd/seed)
mail.go email delivery: Mailpit in development or the provider
infra_mail.go the email provider's configuration (replaced by `aps add mail`)
infra_mail_test.go the email provider's tests and fixtures (replaced by `aps add mail`)
modules.go one line per business module (//aps:anchor modules)
module_<name>.go wires one module: its operations and error codes
internal/jobs/<name>/ background job arguments and worker
internal/modules/<name>/ one bounded context per directory
module.go wires the module's layers
domain/ business rules and errors (standard library only)
usecase/ application logic; ports.go holds the interfaces it needs
repository/ storage adapters implementing ports (hand-written SQL)
delivery/ HTTP adapter: Huma operations ↔ use cases
internal/modules/auth/ sign-up, sign-in, sessions, passwords and roles: domain, usecase, repository (SQL), delivery
internal/modules/ops/ admin APIs for runtime settings, jobs, the audit log and email
internal/modules/projects/ example business resource owned by the signed-in user: copy it for your own
api/openapi.json exported API contract (committed; review changes in pull requests)
compose.yaml PostgreSQL and Mailpit for development and tests
Request flow#
HTTP → middleware (recover, request ID, tracing, access log, security headers, CORS,
cross-origin protection, body limit, session authentication, auth rate limit) → delivery → usecase → domain
← domain errors mapped to problem+json in internal/app/module_<name>.go
Configuration#
- Environment (
config.go): secrets and infrastructure. Changing them needs a restart. - Runtime settings (
settings.go): non-secret tunables stored in PostgreSQL, changed withPUT /ops/settings/{key}, applied on every instance within moments. Modules receive them asconfig.Value[T]and callGeteach time. - Job definitions (
job_<name>.go): each job's code defaults (enabled, schedule, timeout, retries, queue), overridable withPUT /ops/jobs/definitions/{name}. Changing a job's code still needs a deploy.
A value is never in more than one layer, and secrets are never runtime settings.
Background jobs#
Jobs run in the API process on PostgreSQL (River). A job carries the request ID, trace and actor that enqueued it, but never their permissions; it runs as the jobs system actor. Add one with aps gen job <Name>, or copy internal/jobs/heartbeat and internal/app/job_heartbeat.go and add a line in jobs.go.
Authentication#
internal/modules/auth owns sign-up, email codes, sign-in, sessions, password reset and change, account deletion and platform roles, with all four layers: its use cases hold every flow and its repository holds the SQL for auth_users, auth_sessions, auth_codes and auth_user_roles. The apistock auth library supplies password hashing, tokens, codes, cookies, the permission catalog and the middleware that puts the signed-in user's actor (with the permissions of their roles) in each request's context. Use cases check actor.Can(permission); declare permissions and roles in internal/app/permissions.go.
Business resources#
internal/modules/projects is exactly what aps gen resource Project name:string:unique description:text 'status:enum(active,archived)' creates. Generate your own resources the same way, or copy it. All of it is your code: change any rule, query or response.
- Ownership: every project has an
owner_id(the signed-in user). Every repository method takes the owner ID, and someone else's project returns 404project_not_found, so IDs can't be probed. Deleting an account deletes its projects. - Lists:
GET /v1/projectsuses keyset pagination throughapistock.dev/page:limit, an opaquecursor, andsortby one allowlisted field, with one fixed query per sort inrepository/select_projects.go. - Updates:
PATCHsends theversionit read; a stale version returns 409project_version_conflictinstead of overwriting someone else's change. - Audit:
projects.project.created,.updated(changed field names only) and.deleted. - Tests: domain rules, repository methods on real PostgreSQL, use cases including cross-owner access, and an end-to-end HTTP test in
internal/app/projects_test.go.
Email#
Modules send email through mailer, a mail.Sender built in app.go: it fills the sender from the mail.* runtime settings and queues the message; the mail worker delivers it with retries and idempotency. mail.go sends to Mailpit in development (MAIL_DELIVERY) or to the provider in infra_mail.go. The provider's secrets are environment variables in the # aps:begin mail block of .env.example. aps add mail replaces infra_mail.go, infra_mail_test.go and that block to switch between Resend and SMTP; don't edit them by hand.
Audit log#
Modules record who did what through audit.Recorder; app.go passes the auditpg store, which writes the append-only audit_events table and redacts sensitive metadata keys. GET /ops/audit lists events. To commit an event with the change it describes, call RecordTx with the same transaction.
Rules#
domain/imports only the standard library. Domain types have no struct tags.usecase/never importsdelivery/,repository/or HTTP packages.delivery/calls use cases, never repositories. Onlydelivery/,module.goandinternal/appimport the HTTP framework (Huma).- Modules never import other modules. Cross-module needs go through interfaces wired in
internal/app. - Only
internal/appreads environment variables. - Handlers return domain errors unchanged; each
module_<name>.gomaps them to an HTTP status and a stable error code. - Request body types tolerate unknown fields (
additionalProperties:"true"), so older servers accept newer clients. - Migrations never run at startup; run
cmd/migratebefore starting a new version.
Adding an endpoint#
- Add the rule and its error to
domain/. - Add the use case to
usecase/. - Add input/output types and a
huma.Registercall indelivery/. - Map new domain errors in
internal/app/module_<name>.go. - Run
go test ./..., thengo run ./cmd/api openapi --dir api.