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.Connectionin domain receivers; the concrete*storage.Postgresstays ininternal/runtime.storage.Transactorandstorage.InsertQueuein services.email.TransactionalSenderandemail.MarketingSenderanywhere mail is sent.cache.Storein handlers and middleware.*inertia.Rendererin handlers, with its construction ininternal/runtime.*routing.Hostswherever 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.