Overview
Core (github.com/gasmod/gas) holds the app lifecycle, the container, the
router and the interfaces. Everything that talks to infrastructure is a
module: its own Go module in the same repository, with its own go.mod, so
go get pulls in only the modules you use.
The modules
Section titled “The modules”| Module | Import path | What it does | Implements | API |
|---|---|---|---|---|
| Config | github.com/gasmod/gas/config |
Loads configuration from the environment, .env and JSON files, and AWS Secrets Manager; binds and validates structs; resolves the environment. |
gas.ConfigProvider |
pkg.go.dev |
| Logging | github.com/gasmod/gas/log |
Structured logging over zerolog or log/slog, and batched HTTP log shipping with an OTLP encoder. |
gas.Logger |
pkg.go.dev |
| Database | github.com/gasmod/gas/database |
SQL connections over database/sql or a native pgx pool, transaction helpers, connection retries. |
gas.DatabaseProvider |
pkg.go.dev |
| Migrations | github.com/gasmod/gas/migrate |
Migrations owned by the services that register them, applied at startup, with rollback. | gas.MigrationManager |
pkg.go.dev |
| Cache | github.com/gasmod/gas/cache |
Key-value caching with a TTL, in process or on Valkey. | gas.CacheProvider |
pkg.go.dev |
| Auth | github.com/gasmod/gas/auth |
JWTs, server-side sessions, API keys and single-use tokens, with middleware that puts a gas.Principal on the request. |
auth.Authenticator, auth.PrincipalRevoker (declared in the module) |
pkg.go.dev |
| Templates | github.com/gasmod/gas/template |
Template storage in memory, a directory, an fs.FS, a database table, or a composite of these. |
gas.TemplateProvider |
pkg.go.dev |
| UI | github.com/gasmod/gas/ui |
HTML rendering with layouts and partials, HTMX fragments, static files. | gas.UIProvider |
pkg.go.dev |
| Storage | github.com/gasmod/gas/storage |
Object storage on AWS S3, S3-compatible services, or a local directory, and presigned URLs. | storage.Provider, storage.Presigner (declared in the module) |
pkg.go.dev |
github.com/gasmod/gas/email |
Email through AWS SES, literal or rendered from templates. | gas.EmailProvider |
pkg.go.dev | |
| Queue | github.com/gasmod/gas/queue |
Job queues on AWS SQS or an SQS-compatible endpoint. | gas.JobQueueProvider |
pkg.go.dev |
Each module page lists what its constructors resolve from the container, its configuration keys, its errors, and its mock package.
How modules fit together
Section titled “How modules fit together”Modules depend on each other only through core. No module’s non-test code
imports another: every module’s packages import only core and their own
module. The one exception is auth’s internal test helpers
(auth/internal/testutil), a non-test package used only by auth’s tests,
which imports config, database and migrate. When one module needs another’s service, its constructor asks
the container for a core interface (gas.ConfigProvider, gas.Logger,
gas.DatabaseProvider, gas.MigrationManager, gas.TemplateProvider, …),
so whatever is registered under that interface satisfies it.
Most modules implement one of those core interfaces, and you register them
under it. Two declare their own: Storage exposes storage.Provider and
storage.Presigner, and Auth exposes auth.Authenticator,
auth.PrincipalRevoker and a Provider interface per sub-package, while the
gas.Principal its middleware produces is declared in core. Register those
under the module’s own interfaces.
app := gas.NewApp( // Every module registers under a core interface. The container // hands each constructor the interfaces it asks for, whichever // implementation provides them. gas.WithServiceInstance[gas.ConfigProvider](cfg), gas.WithSingletonService[gas.Logger](gaslog.NewSlogLogger()), gas.WithSingletonService[gas.DatabaseProvider](database.New()), gas.WithSingletonService[gas.MigrationManager](migrate.New()), gas.WithSingletonService[gas.CacheProvider](cachemem.New()), gas.WithServiceInstance[gas.TemplateProvider](templatememory.NewStore()), gas.WithSingletonService[gas.EmailProvider](emailses.New()),
gas.WithSingletonService[*Signups](NewSignups),)emailses.New() asks for a gas.TemplateProvider, and gets the memory store
here; it would get a database-backed store, or your own type, just the same.
The same holds for your services, which depend on the interfaces rather than
on any module:
// Signups knows only core interfaces. Swapping the cache backend, the email// sender or the template store, or passing mocks in a test, changes nothing// here.type Signups struct { cache gas.CacheProvider email gas.EmailProvider}
func NewSignups(cache gas.CacheProvider, email gas.EmailProvider) *Signups { return &Signups{cache: cache, email: email}}
func (s *Signups) Welcome(ctx context.Context, to string) error { if err := s.email.Send(ctx, &gas.Email{ To: []string{to}, Subject: "Welcome", TextBody: "Thanks for signing up.", }); err != nil { return err } return s.cache.Set(ctx, "welcomed:"+to, []byte("1"), 24*time.Hour)}Three conventions hold across the modules:
- Configuration. A module that has settings binds its own section
(
database.*,cache.*,storage.*,email.*, …) from thegas.ConfigProviderwhen it initializes. Storage is the exception: itss3anddirproviders bindstorage.*in their constructors. The config structs of the cache, database, auth, UI, email and queue services embedgas.WithGasEnv, so they know which environment they run in. Environment variables follow one naming rule. - Lifecycle. Module services implement
gas.Service, so the container runs theirInitat startup and theirCloseat shutdown, and many implementgas.HealthReporterorgas.ReadyReporterfor the probes. See App lifecycle. The services are the cache, database, migrate, auth (jwt,session,apikey,token), UI, email and queue services, the database template store, and the shipping logger. These are not services, so the container never runs anInitorCloseon them:*config.Config, the zerolog and slog loggers, the storages3anddirproviders and thes3presigner, and the memory, directory,fs.FSand composite template stores. - Migrations. A module that owns tables, such as the auth stores or the
database template store, registers its migrations with the
gas.MigrationManagerduringInit, and core applies them after every service has initialized. See Migrations.
Swapping an implementation, or replacing it with a mock in a test, changes
only the registration. Every module except Database and Logging ships a mock
package: configtest, cachetest, migratetest, authtest,
templatetest, uitest, storagetest, emailtest and queuetest. See
Testing.
Versioning
Section titled “Versioning”All modules share one version and are released together. A release tags the
root module vX.Y.Z and each module <dir>/vX.Y.Z (for example
auth/vX.Y.Z), which is what go get github.com/gasmod/gas/auth@vX.Y.Z
resolves, and the release fails unless every module’s requirement on another
Gas module names that same version. Use the same version for core and every
module you import; mixing versions is not supported.
Gas is pre-1.0, so a minor version may contain breaking changes. See the changelog.