README
¶
Vormia Go Core
Foundation layer for the Vormia ecosystem — dependency injection, config, logging, events, lifecycle, context, and errors.
Status: stable v1.1.0 (versioned by git tags).
Module: github.com/vormialabs/vormia-go-core · Go: 1.21+ · Deps: standard library only.
Deep dive: aiguide/GUIDE.md · AI / contributor rules: aiguide/CURSOR_CODEX_MCP_GUIDE.md · Changes: CHANGELOG.md
What it provides
| Package | Purpose |
|---|---|
container |
DI: singleton + transient services, ServiceProvider |
config |
Key/value store from .env / OS env; typed getters + Prefixed |
logger |
Leveled logging (stdout, writer, or file) |
events |
Sync + async publish/subscribe |
lifecycle |
App with ordered boot/shutdown hooks and signal handling |
context |
Typed context keys + mutable request ValueBag |
errors |
Wrap / Exception / pluggable Handler |
No HTTP, database, or cache here — those belong in higher Vormia layers.
Installation
go get github.com/vormialabs/vormia-go-core@v1.1.0
Requires the tag to be published on the remote. Check available versions:
git ls-remote --tags https://github.com/vormialabs/vormia-go-core.git
go list -m -versions github.com/vormialabs/vormia-go-core
Quick start
Full Vormia app (copy this)
End-state main.go after wiring core with vormia-go, Chi, and a database. PostgreSQL is the default. Also go get the framework and drivers you use:
go get github.com/vormialabs/vormia-go
go get github.com/vormialabs/vormia-go-driver-chi
go get github.com/vormialabs/vormia-go-driver-postgresql
# optional swaps: ...-mysql | ...-sqlite | ...-redis
Remove pieces you do not need (e.g. drop DB/WithDB for a router-only app). Kernel apps: do not also call lifecycle.WaitForSignal — k.Run owns SIGINT/SIGTERM.
package main
import (
"net/http"
chi "github.com/vormialabs/vormia-go-driver-chi"
postgres "github.com/vormialabs/vormia-go-driver-postgresql"
"github.com/vormialabs/vormia-go-core/config"
"github.com/vormialabs/vormia-go-core/logger"
"github.com/vormialabs/vormia-go/app"
"github.com/vormialabs/vormia-go/contract"
)
func main() {
// Load .env, then OS env (OS wins if both set)
cfg := config.New()
_ = cfg.Load(".env")
cfg.LoadEnv()
log := logger.NewStdoutLogger()
log.SetLevel(logger.ParseLevel(cfg.GetString("LOG_LEVEL", "info")))
log.Info("starting", map[string]interface{}{
"app": cfg.GetString("APP_NAME", "storage-engine"),
})
db, err := postgres.Open(postgres.Config{
Host: cfg.GetString("DB_HOST", "localhost"),
User: cfg.GetString("DB_USER", "app"),
Password: cfg.GetString("DB_PASSWORD", "secret"),
Database: cfg.GetString("DB_NAME", "storage-engine"),
})
if err != nil {
panic(err)
}
k := app.New(chi.New(), app.WithDB(db))
k.Routes(func(r contract.Router) {
r.Get("/health", func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("ok"))
})
})
if err := k.Run(":8080"); err != nil {
panic(err)
}
}
Swap database
Replace the Postgres import + Open block. The rest of main stays the same; handlers still see contract.Database.
MySQL
import mysql "github.com/vormialabs/vormia-go-driver-mysql"
db, err := mysql.Open(mysql.Config{
Host: cfg.GetString("DB_HOST", "localhost"),
User: cfg.GetString("DB_USER", "app"),
Password: cfg.GetString("DB_PASSWORD", "secret"),
Database: cfg.GetString("DB_NAME", "app"),
})
SQLite
import sqlite "github.com/vormialabs/vormia-go-driver-sqlite"
db, err := sqlite.Open(sqlite.Config{
Path: cfg.GetString("DB_PATH", "app.db"),
ForeignKeys: true,
})
Optional Redis cache
Add alongside the database (or alone with app.WithCache only):
import redis "github.com/vormialabs/vormia-go-driver-redis"
cache, err := redis.Open(redis.Config{
Addr: cfg.GetString("REDIS_ADDR", "localhost:6379"),
})
if err != nil {
panic(err)
}
k := app.New(chi.New(), app.WithDB(db), app.WithCache(cache))
Core-only shell (no HTTP/DB)
Thin main using only this package: create App, load config, register hooks, boot, wait for signal. Use this when you are not running the vormia-go kernel.
package main
import (
"os"
"github.com/vormialabs/vormia-go-core/lifecycle"
"github.com/vormialabs/vormia-go-core/logger"
)
func main() {
app := lifecycle.NewApp()
// File first, then OS env (OS wins).
_ = app.Config().Load(".env") // missing file is OK
app.Config().LoadEnv()
app.Logger().SetLevel(logger.ParseLevel(
app.Config().GetString("LOG_LEVEL", "info"),
))
app.OnBoot(func() error {
return app.Config().Require("APP_NAME")
})
app.OnShutdown(func() error {
// close pools, flush loggers, etc.
return nil
})
if err := app.Boot(); err != nil {
app.Logger().Error("boot failed", err, nil)
os.Exit(1)
}
// Blocks on SIGINT/SIGTERM, then runs Shutdown in reverse order.
_ = app.WaitForSignal()
}
Dependency injection
c := container.New()
// Singleton — factory runs once.
_ = c.Register("userRepo", func() interface{} {
return &UserRepository{}
})
// Transient — fresh instance every Resolve.
_ = c.RegisterTransient("requestID", func() interface{} {
return newID()
})
repo, err := c.Resolve("userRepo")
if err != nil {
// match *container.ErrNotFound / *container.ErrCyclicDependency with errors.As
panic(err)
}
userRepo := repo.(*UserRepository)
Wire modules with ServiceProvider (Register then Boot via RegisterProviders).
Configuration
cfg := config.New()
_ = cfg.Load(".env")
cfg.LoadEnv()
port := cfg.GetInt("PORT", 8080)
debug := cfg.GetBool("APP_DEBUG", false)
name := cfg.GetString("APP_NAME", "vormia")
err := cfg.Require("DB_HOST", "DB_USER") // one error listing all missing keys
// Prefixed is generic — any prefix (DB_, CACHE_, MAIL_, …). Prefix is stripped from keys.
group := cfg.Prefixed("DB_MYSQL2_") // map[string]string{"HOST": "...", "PORT": "3306", ...}
Typed getters never fail — they return the default when missing or unparsable.
Prefixed only includes keys that start with the given prefix (bare keys like DB_HOST never appear under Prefixed("DB_MYSQL2_")).
Logging
log := logger.NewStdoutLogger()
log.SetLevel(logger.LevelDebug)
log.Info("started", map[string]interface{}{"version": "0.0.1"})
log.Error("failed", err, map[string]interface{}{"user_id": 123})
// Production: append to file and mirror to stdout.
fileLog, _ := logger.NewFileLogger("logs/app.log")
defer fileLog.Close()
Depend on the logger.Logger interface, not a concrete type. Also available: NewWriterLogger(w) for tests.
Events
d := events.NewDispatcher()
d.OnPanic(func(event string, recovered interface{}) {
// required if you use DispatchAsync — otherwise panics are silent
})
d.Listen("user.created", func(payload interface{}) {
// sync handlers run in registration order
})
d.Dispatch("user.created", user)
w := d.DispatchAsync("user.created", user)
w.Wait() // optional
Context values
import vctx "github.com/vormialabs/vormia-go-core/context"
const UserIDKey = vctx.Key("user_id")
ctx := vctx.WithValue(vctx.Background(), UserIDKey, "u-42")
bag := vctx.NewValueBag()
ctx = vctx.WithBag(ctx, bag)
if b := vctx.BagFrom(ctx); b != nil {
b.Set("trace_id", "abc")
}
Errors
err := errors.Wrap(cause, "loading user")
ex := errors.NewException("payment failed", "PAY_DECLINED").
WithCause(gatewayErr).
With("order_id", 1234)
handler := errors.NewLoggingHandler(app.Logger())
handler.Handle(ex)
Core contracts
type ServiceContainer interface {
Register(name string, factory func() interface{}) error
Resolve(name string) (interface{}, error)
Get(name string, defaultValue interface{}) interface{}
}
type Logger interface {
Debug(message string, context map[string]interface{})
Info(message string, context map[string]interface{})
Warn(message string, context map[string]interface{})
Error(message string, err error, context map[string]interface{})
SetLevel(level Level)
}
type EventDispatcher interface {
Listen(event string, callback func(payload interface{}))
Dispatch(event string, payload interface{})
}
Also notable:
- container:
RegisterTransient,Has,ServiceProvider,RegisterProviders - config:
Load/LoadEnv, typed getters,Require,All,Has - logger:
ParseLevel,NewFileLogger,NewWriterLogger - events:
OnPanic,ListenTo,DispatchAsync,Forget,HasListeners - lifecycle:
OnBoot/OnShutdown(+ priority variants),WaitForSignal
Design notes
- Thread-safe by default — container, config, dispatcher, loggers, value bag, hooks.
- Fail fast on boot — first boot-hook error aborts; shutdown runs every hook and joins errors.
- Config load order —
Load(".env")thenLoadEnv()so OS wins. - No magic — string-keyed services and explicit factories (no reflection autowiring).
Package layout
vormia-go-core/
├── go.mod
├── README.md
├── aiguide/ # GUIDE.md + AI contributor rules
├── container/
├── config/
├── logger/
├── events/
├── lifecycle/
├── context/
└── errors/
Development
go build ./...
go vet ./...
gofmt -l . # must print nothing
go test ./...
go test -race -count=2 ./...
License
See LICENSE.