apistockdocs
v0.4 GitHub apistock.dev
Guides/Start

Project layout

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#

output
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`)
  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#

output
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 with PUT /ops/settings/{key}, applied on every instance within moments. Modules receive them as config.Value[T] and call Get each time.
  • Job definitions (job_<name>.go): each job's code defaults (enabled, schedule, timeout, retries, queue), overridable with PUT /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 404 project_not_found, so IDs can't be probed. Deleting an account deletes its projects.
  • Lists: GET /v1/projects uses keyset pagination through apistock.dev/page: limit, an opaque cursor, and sort by one allowlisted field, with one fixed query per sort in repository/select_projects.go.
  • Updates: PATCH sends the version it read; a stale version returns 409 project_version_conflict instead 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 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#

  1. domain/ imports only the standard library. Domain types have no struct tags.
  2. usecase/ never imports delivery/, repository/ or HTTP packages.
  3. delivery/ calls use cases, never repositories. Only delivery/, module.go and internal/app import the HTTP framework (Huma).
  4. Modules never import other modules. Cross-module needs go through interfaces wired in internal/app.
  5. Only internal/app reads environment variables.
  6. Handlers return domain errors unchanged; each module_<name>.go maps them to an HTTP status and a stable error code.
  7. Request body types tolerate unknown fields (additionalProperties:"true"), so older servers accept newer clients.
  8. Migrations never run at startup; run cmd/migrate before starting a new version.

Adding an endpoint#

  1. Add the rule and its error to domain/.
  2. Add the use case to usecase/.
  3. Add input/output types and a huma.Register call in delivery/.
  4. Map new domain errors in internal/app/module_<name>.go.
  5. Run go test ./..., then go run ./cmd/api openapi > api/openapi.json.
esc
↑↓ move↵ openesc close