Skip to content
latest
Framework Packages / latest

Framework Packages

Andurel is two things: the andurel CLI that scaffolds, generates and verifies a project, and twelve library modules under github.com/mbvlabs/andurel/pkg/* that the generated application imports. The application owns its composition root, configuration, domains, routes, handlers and frontend. The packages supply infrastructure at those edges.

There is no dependency-injection container and no service locator. Every package is an ordinary Go package with constructors, and the only place that connects them is internal/runtime.

Package map

Package Responsibility Read more
pkg/anduin HTTP handlers, router, middleware, server, static/health/sitemap helpers Server
pkg/auth argon2id password hashing and token primitives Authentication
pkg/cache JSON-valued cache with memory and Redis drivers —
pkg/email Transport-neutral messages, providers, validation and retry classification Email Package
pkg/env .env loading and typed environment reads Configuration
pkg/inertia Inertia v3 server protocol, props, Vite assets and SSR client Inertia
pkg/kiks Encrypted and signed cookie stores, sessions, bags and flash —
pkg/lifecycle Process start/stop coordination and a reverse-order cleanup stack —
pkg/routing Typed route values and the virtual-host registry Routing Package
pkg/storage PostgreSQL via pgx, transactions, migrations, pagination and River clients Storage
pkg/telemetry Logs, metrics and traces over OpenTelemetry Telemetry
pkg/validation Structured field errors and composable rules Validation

Each package is its own Go module with its own semantic version, so the versions in go.mod do not have to match each other or the CLI release.

Composition

internal/runtime constructs the graph bottom-up: telemetry, database, cache, queue inserter, domains, services, renderer, handlers, router, then the server. Each dependency registers its cleanup as soon as it exists, so a failure partway through releases what was already built.

 1db, err := storage.NewPostgres(ctx, cfg.Database, storage.WithTelemetry(tel))
 2if err != nil {
 3    return nil, err
 4}
 5app.DB = db
 6app.closers.Add(func(context.Context) error { return db.Close() })
 7
 8queueInsert, err := storage.NewQueueInsert(db, cfg.QueueInsert.Config)
 9if err != nil {
10    return nil, err
11}
12
13users := domains.NewUsers(db)
14tokens := domains.NewTokens(db)
15identity := services.NewIdentity(
16    storage.NewTransactor(db),
17    users,
18    tokens,
19    queueInsert,
20    cfg.Auth,
21    cfg.Mail,
22    hosts,
23)
24
25renderer, err := newRenderer(cfg, hosts)
26if err != nil {
27    return nil, err
28}
29
30router := anduin.NewRouter(
31    anduin.WithHosts(hosts),
32    anduin.WithErrorHandler(handlers.MapError(renderer)),
33    anduin.WithTelemetry(tel),
34)
35
36app.Handler = router.Handler()
37app.Server = anduin.NewServer(
38    ctx,
39    cfg.HTTP.Host,
40    cfg.HTTP.Port,
41    app.Handler,
42    anduin.WithTimeouts(cfg.HTTP.IdleTimeout, cfg.HTTP.ReadTimeout, cfg.HTTP.WriteTimeout),
43)

runtime.App exposes Handler, Server, Shutdown and Close. cmd/app only loads the environment and runs lifecycle.Run; cmd/queue builds the worker graph with runtime.NewQueue. Neither file contains business wiring.

Use andurel patterns show composition for the full reference.

Configuration boundary

A package configuration type carries validated operational policy. It never reads environment variables. Defaults come from a Default* constructor, and Validate rejects unusable values.

1cfg := storage.DefaultConfig()
2cfg.Name = "orbit_production"
3cfg.User = "orbit"
4cfg.Password = secret
5cfg.SSLMode = "verify-full"
6
7db, err := storage.NewPostgres(ctx, cfg)

The application maps its environment onto that type in config/:

 1func NewDatabase() (storage.Config, error) {
 2    e := env.New()
 3    cfg := storage.DefaultConfig()
 4    cfg.Host = e.String("DB_HOST", DefaultDatabaseHost)
 5    cfg.Name = e.String("DB_NAME", DefaultDatabaseName)
 6    cfg.User = e.String("DB_USER", DefaultDatabaseUser)
 7    cfg.Password = e.String("DB_PASSWORD", DefaultDatabasePassword)
 8
 9    if err := errors.Join(e.Err(), cfg.Validate()); err != nil {
10        return storage.Config{}, fmt.Errorf("config: database: %w", err)
11    }
12
13    return cfg, nil
14}

This keeps a package update from silently adding a new environment contract, and lets tests or another host construct the same package from code.

Interfaces worth depending on

Depend on the smallest type that does the job. Adapters and fakes belong to the application, not to shared code.

  • storage.Connection in domain receivers; the concrete *storage.Postgres stays in internal/runtime.
  • storage.Transactor and storage.InsertQueue in services.
  • email.TransactionalSender and email.MarketingSender anywhere mail is sent.
  • cache.Store in handlers and middleware.
  • *inertia.Renderer in handlers, with its construction in internal/runtime.
  • *routing.Hosts wherever a full URL crosses a boundary, such as email links, redirects and asset tags.

Versions and upgrades

1andurel packages list
2andurel packages update --dry-run
3andurel packages update storage
4andurel upgrade --dry-run

andurel packages list reads go.mod and reports each module's current and latest version; --json gives the same table as data. andurel packages update bumps only the pkg modules. andurel upgrade re-pins the modules to this CLI version, syncs tools, and lists the manual actions to apply; --repair restores drifted framework-owned files.

go.mod is the source of truth for module versions. andurel.toml records the scaffold choices and pinned tool versions, and andurel.lock pins the tool download hashes used by andurel tool sync.

Updating a package does not rewrite application code. Read the module's CHANGELOG.md, compile the project, and exercise the process that owns the change:

  • storage: migrations, transactions, query generation, queue insertion and workers;
  • inertia: initial HTML, client visits, partial reloads, redirects, validation, Vite and SSR;
  • anduin: routes, middleware order, error mapping and graceful shutdown.

Extending and replacing

Application-specific adapters stay in the application, normally under internal/. A provider that implements email.TransactionalSender, a payment client, or a custom transport is constructed in internal/runtime and passed in as the interface the consumer declared. Tests can pass a fake sender or a storage.Connection from storagetest without touching the framework.

Importing package internals across the application defeats that boundary and makes independent upgrades harder. See Directory Structure for where each kind of code lives.