English | 中文
Build serverless, reliable apps at lightspeed ⚡ — with confidence 🛡️.
Anclax is a framework for small to medium-sized applications backed by a single PostgreSQL database. It provides:
- Strong schemas and code generation that move correctness checks to compile time
- An integrated toolchain of best-in-class Go tools
- Dependency inversion and a white-box testing framework
- A high-performance async task scheduling framework
Join our Discord server.
Contact: mike@anclax.com
- Install the Anclax CLI:
go install github.com/cloudcarver/anclax/cmd/anclax@latest- Bootstrap a new project (installs toolchain + runs codegen):
anclax init myapp github.com/me/myapp
cd myapp- For existing repos (or after changing
anclax.yaml), sync tools and regenerate:
anclax install
anclax genExternal tools are installed into .anclax/bin for reproducible builds.
- Optional: add the Anclax skill to your coding agent:
npx skills add cloudcarver/anclax- YAML-first, codegen-backed: Define HTTP and task schemas in YAML; Anclax generates strongly-typed interfaces so missing implementations fail at compile time, not in prod.
- Async tasks you can trust: Enqueue tasks atomically with business changes in one database transaction, with at-least-once delivery, automatic retries, cron scheduling, strict serial execution, task group management, per-tag concurrency quotas, and priority/weight scheduling. Benchmarks sustained about 5,000 concurrent tasks with 30 database connections and less than one PostgreSQL CPU core on average; see the scheduling capacity benchmark.
- Typed database layer: Powered by
sqlcfor safe, fast queries. - Fast HTTP server: Built on Fiber with OpenAPI syntax extensions for performance and ease of use.
- AuthN/Z built-in: Macaroons-based authentication and authorization.
- Ergonomic DI: Wire-based dependency inversion keeps dependencies explicit and code testable.
- Glue-code fatigue: Many teams stitch HTTP, DB, tasks, DI, and auth by hand, leaving implicit contracts and runtime surprises. Anclax makes those contracts explicit and generated.
- Background jobs are hard: Idempotency, retries, and delivery guarantees are non-trivial. Anclax ships a task engine with at-least-once semantics and cron.
- Consistency across boundaries: Keep handlers, tasks, and hooks transactional using
WithTxso invariants hold. - Confidence and testability: Every generated interface is mockable; behavior is easy to test.
- Compile-time confidence: Schema → interfaces → concrete implementations you cannot forget to write.
- Productivity:
anclax init+anclax genreduces boilerplate and wiring. - Reproducible toolchains: External tools are pinned in
anclax.yamland installed into.anclax/bin. - Extensibility: Clean plugin boundaries and event-driven architecture.
- Predictability: Singletons for core services, DI for clarity, and well-defined lifecycles.
Anclax helps you build quickly while staying scalable and production‑ready.
- Single PostgreSQL backbone: One PostgreSQL database powers both transactional business logic and the durable task queue, keeping state consistent and operations simple. For many products, a well‑provisioned instance (e.g., 32 vCPU) goes a very long way.
- Stateless application nodes: HTTP servers are stateless and horizontally scalable; you can run multiple replicas without coordination concerns.
- Task queue as integration fabric: Use async tasks to decouple modules. For example, when a payment completes, enqueue an
OrderFinishedtask and do any factory‑module inserts in its handler—no factory logic inside the payment module. - Built‑in worker, flexible deployment: Anclax includes an async task worker. Run it in‑process, as separate long‑running workers, or disable it for serverless HTTP (e.g., AWS Lambda) while keeping workers on regular servers.
- Modular apps, flexible service boundaries: Anclax works well for modular monoliths and multi-service repositories. You can share modules in one codebase and split apps, APIs, and data layers only where it improves ownership and clarity. It is not aimed at ultra-large microservice fleets with fully independent polyglot infrastructure.
These choices maximize early velocity and give you a clear, reliable path to scale with confidence.
Anclax can host one app or multiple service entrypoints in the same repository.
app/for app bootstrap: Keep each app's startup logic inapp/<service>/app.go, expose framework-managed dependencies inapp/<service>/injection.go, and keep that app's Wire graph underapp/<service>/wire/.pkg/for shared modules: Put reusable modules inpkg/so multiple apps can share business logic, helpers, and integrations.- Shared model by default: Most repos can share one top-level
sql/directory and onepkg/modelpackage across apps. - Service-specific model when needed: If one app needs isolated queries, migrations, or stronger ownership boundaries, give it its own
sql/folder and model package underapp/<service>/. Use a unique migration table name so different migration sets never conflict. - Per-app APIs and tasks: Each app can own its own OpenAPI spec, task spec, handlers, and async task executor. Add matching
oapi-codegen,task-handler,wire, andsqlcentries inanclax.yaml. - Shared schemas: Use the
schemasfeature inanclax.yamlto reuse schema definitions across multiple apps and specs.
This lets you start simple, share code aggressively, and introduce service boundaries only where they are actually useful.
# 1) Scaffold into folder 'demo'
anclax init demo github.com/you/demo
# 2) Generate code (can be re-run anytime)
cd demo
anclax gen
# 3) Generate local database credentials and start DB + API + worker
make devmake dev creates a private .env with a random database password if it is missing. The database publishes no host port; use make db for its console. Preserve .env when reusing the database volume. See the generated README for existing-volume migration.
In another terminal, choose a username and register (requires jq):
ANCLAX_USERNAME=your-name
ANCLAX_PASSWORD=$(openssl rand -hex 24)
TOKEN=$(jq -n --arg name "$ANCLAX_USERNAME" --arg password "$ANCLAX_PASSWORD" \
'{name: $name, password: $password}' | \
curl -fsS http://localhost:2910/api/v1/auth/sign-up \
-H "Content-Type: application/json" --data-binary @- | jq -er '.accessToken')
curl -X POST http://localhost:2910/api/v1/counter -H "Authorization: Bearer $TOKEN"
curl http://localhost:2910/api/v1/counterThe development configuration enables simple auth without creating a preset account. Retain your chosen username and password for later sign-in.
The API now honors anclax.host, which defaults to localhost on port 8020. Container deployments must explicitly set anclax.host: 0.0.0.0 (or MYAPP_ANCLAX_HOST=0.0.0.0 for the scaffold). The generated Compose configuration already does this.
Metrics and pprof are disabled by default. When enabled, they bind to 127.0.0.1, on ports 9020 and 8777 respectively. For local metrics scraping:
anclax:
metrics:
enable: true
host: 127.0.0.1
port: 9020
debug:
enable: false
host: 127.0.0.1
port: 8777Remote scrapers or profilers need an explicit metrics.host or debug.host that they can reach, and access controls on that management network. The legacy metricsport setting still enables metrics unless the new metrics.port is set; remove it to disable metrics. The new port takes precedence, with metrics.enable controlling the listener. These listeners do not impose fixed HTTP request timeouts in the framework.
- Define an endpoint (OpenAPI YAML) 🧩
paths:
/api/v1/counter:
get:
operationId: getCounter- Define a task ⏱️
tasks:
- name: IncrementCounter
description: Increment the counter value
cronjob:
cronExpression: "*/1 * * * * *"
retryPolicy:
interval: 5s
maxAttempts: 3- Generate and implement 🛠️
anclax genDefault scaffold layout:
- OpenAPI fragments:
api/openapi/ - Task definitions:
api/tasks/tasks.yaml - Shared schemas:
api/schemas/
oapi-codegen.path can point to either a single file or a directory. When you need one bundled OpenAPI file, run:
anclax bundle-openapi-spec --input api/openapi --output openapi-bundle.yamlfunc (h *Handler) GetCounter(c *fiber.Ctx) error {
return c.JSON(apigen.Counter{Count: 0})
}The scaffold owns outbound client policy in pkg/apiclient/client.go, an ordinary application source file. apiclient.DefaultConfig() provides a 30-second request timeout and a 10 MiB response limit. Edit those defaults or pass a different apiclient.Config to apiclient.New; anclax gen preserves application client code.
The scaffold's response limit applies to both parsed and raw responses and returns apiclient.ErrResponseBodyTooLarge, checkable with errors.Is. Set MaxResponseBodyBytes to zero for large downloads and stream and close the raw http.Response.Body. Set Timeout to zero to disable the request timeout.
Generated apigen clients handle the API protocol; applications supply their HTTP transport and policy through WithHTTPClient. Parsing closes response bodies on success and failure, and returns read errors, including context cancellation.
x-check-rules:
OperationPermit:
useContext: true
parameters:
- name: operationID
schema:
type: string
ValidateOrgAccess:
useContext: true
parameters:
- name: orgID
schema:
type: integer
format: int32
paths:
/orgs/{orgID}/projects/{projectID}:
get:
operationId: GetProject
security:
- BearerAuth:
- x.ValidateOrgAccess(c, orgID, "viewer")
- x.OperationPermit(c, operationID)components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: macaroon- Pain points before: hand-building
apigen.Taskpayloads and attributes was repetitive and easy to get wrong. - Pain points before: retry/cronjob/unique-tag logic got duplicated and drifted across services.
- Pain points before: enqueueing inside a DB transaction required custom glue code.
- Pain points before: task params and handler signatures could fall out of sync.
Refactor solution: define tasks in api/tasks/tasks.yaml, run anclax gen, and use the generated taskgen.TaskRunner (RunX / RunXWithTx) with taskcore overrides when needed.
# api/tasks/tasks.yaml
tasks:
- name: SendWelcomeEmail
description: Send welcome email to new users
parameters:
type: object
required: [userId, templateId]
properties:
userId:
type: integer
format: int32
templateId:
type: string
retryPolicy:
interval: 5m
maxAttempts: 3
timeout: 30s
cronjob:
cronExpression: "0 * * * * *"Before (manual task record):
params := &taskgen.SendWelcomeEmailParameters{UserId: user.ID, TemplateId: "welcome"}
payload, err := params.Marshal()
if err != nil {
return err
}
task := &apigen.Task{
Spec: apigen.TaskSpec{Type: taskgen.SendWelcomeEmail, Payload: payload},
Attributes: apigen.TaskAttributes{
RetryPolicy: &apigen.TaskRetryPolicy{Interval: "5m", MaxAttempts: 3},
},
Status: apigen.Pending,
}
taskID, err := taskStore.PushTask(ctx, task)After (generated runner + transaction-safe enqueue):
err := model.RunTransactionWithTx(ctx, func(tx core.Tx, txm model.ModelInterface) error {
_, err := taskrunner.RunSendWelcomeEmailWithTx(ctx, tx, &taskgen.SendWelcomeEmailParameters{
UserId: user.ID,
TemplateId: "welcome",
}, taskcore.WithUniqueTag("welcome:"+strconv.Itoa(int(user.ID))))
return err
})Need ordering or scheduling controls? Add overrides like taskcore.WithSerialKey, taskcore.WithPriority, and taskcore.WithWeight when enqueueing:
params := &taskgen.SendWelcomeEmailParameters{UserId: user.ID, TemplateId: "welcome"}
_, err := taskrunner.RunSendWelcomeEmail(ctx, params,
taskcore.WithSerialKey("user:"+strconv.Itoa(int(user.ID))),
taskcore.WithPriority(10),
taskcore.WithWeight(3),
)func (s *Service) CreateUserWithTx(ctx context.Context, tx pgx.Tx, username, password string) (int32, error) {
txm := s.model.SpawnWithTx(tx)
userID, err := txm.CreateUser(ctx, username, password)
if err != nil { return 0, err }
if err := s.hooks.OnUserCreated(ctx, tx, userID); err != nil { return 0, err }
_, err = s.taskRunner.RunSendWelcomeEmailWithTx(ctx, tx, &taskgen.SendWelcomeEmailParameters{ UserId: userID })
return userID, err
}func NewGreeter(m model.ModelInterface) (*Greeter, error) { return &Greeter{Model: m}, nil }func InitApp() (*app.App, error) {
wire.Build(model.NewModel, NewGreeter /* ...other providers... */)
return nil, nil
}-- name: GetCounter :one
SELECT value FROM counter LIMIT 1;
-- name: IncrementCounter :exec
UPDATE counter SET value = value + 1;You can run custom logic before the app starts by providing an Init function:
// Runs before the application starts
func Init(anclaxApp *anclax_app.Application, taskrunner taskgen.TaskRunner, myapp anclax_app.Plugin) (*app.App, error) {
if err := anclaxApp.Plug(myapp); err != nil {
return nil, err
}
if _, err := anclaxApp.GetService().CreateNewUser(context.Background(), "test", "test"); err != nil {
return nil, err
}
if _, err := taskrunner.RunAutoIncrementCounter(context.Background(), &taskgen.AutoIncrementCounterParameters{
Amount: 1,
}, taskcore.WithUniqueTag("auto-increment-counter")); err != nil {
return nil, err
}
return &app.App{ AnclaxApp: anclaxApp }, nil
}To customize how the Anclax application is constructed, override InitAnclaxApplication:
func InitAnclaxApplication(cfg *config.Config) (*anclax_app.Application, error) {
anclaxApp, err := anclax_wire.InitializeApplication(&cfg.Anclax, anclax_config.DefaultLibConfig())
if err != nil {
return nil, err
}
return anclaxApp, nil
}Need more dependencies inside Init? Add them as parameters (e.g., model.ModelInterface) and run anclax gen.
- Transaction Management: docs/transaction.md (中文)
- Authentication & Macaroons: docs/authentication.md
- Middleware (x-functions & x-check-rules): docs/middleware.md (中文)
- Async Tasks: Tutorial docs/async-tasks-tutorial.md · Tech reference docs/async-tasks-technical.md · Scheduling/runtime-config guide docs/async-task-scheduling-runtime-config.md (中文, 中文, 中文)
- DST E2E Testing: docs/dst-e2e.md
- Async Task Worker Lease: docs/async-task-worker-lease.md
- Async Task Production-Readiness Testing: docs/async-task-testing-production-readiness.md
examples/simple— minimal end-to-end sample with HTTP, tasks, DI, and DB.
