Files
contracts/README.md
T
Rene Nochebuena 3d440fd8bc feat(contracts): release SecurityBag; anchor framework at v1.1.0
Releases the SecurityBag request-scoped security context (drafted in the
CHANGELOG since 2026-05-28) and anchors the coordinated framework release at
v1.1.0 — the einherjar modules move to v1.1.0 in lockstep.

security:
- SecurityBag value type + NewSecurityBag, Identity/WithIdentity,
  Get/With, SetBagInContext (immutable; copy-on-write attributes).

No einherjar dependencies. Badge bumped to v1.1.0.
2026-08-07 18:23:14 -06:00

5.3 KiB

einherjar/contracts

version license go

For those who come after.

The foundation of the Einherjar framework. Pure interfaces. Zero dependencies. A constitution, not a changelog.

contracts defines the interfaces and minimal types that every Einherjar starter depends on. Nothing is above it in the dependency graph. Its interfaces, once published, are permanent — adding a method is a breaking change in Go, and breaking changes require a major version bump and coordinated updates across the entire framework.


What Is Einherjar

In Norse mythology, the Einherjar are the chosen — warriors who fell in battle and were carried to Valhalla by the Valkyries. There they train, day after day, preparing for Ragnarök. They are not preserved for themselves. They are prepared for what comes after.

This framework carries that name deliberately. Every interface defined here, every ADR written, every pattern documented exists for the developer who will build on this code without ever being in the room where it was designed. No tribal knowledge. No implicit conventions. The documentation is the system.


Module

code.nochebuena.dev/einherjar/contracts

Dependencies: none. This module imports only the Go standard library. Stability guarantee: existing interface signatures are permanent. New interfaces may be added. Existing ones are not modified.


Sub-packages

Package Purpose
lifecycle Component — three-phase managed lifecycle (OnInit → OnStart → OnStop)
observability Checkable + Level — health reporting for infrastructure components
logging Logger — structured, leveled logging
errs CodedError + ContextualError — structured error enrichment consumed by the logger
security Identity, Permission, PermissionMask, PermissionProvider — authentication and authorization primitives

Usage

Import only the sub-packages you need. There is no root contracts package to import.

import (
    "code.nochebuena.dev/einherjar/contracts/lifecycle"
    "code.nochebuena.dev/einherjar/contracts/logging"
    "code.nochebuena.dev/einherjar/contracts/observability"
    "code.nochebuena.dev/einherjar/contracts/errs"
    "code.nochebuena.dev/einherjar/contracts/security"
)

Implementing a managed component

// MyClient satisfies lifecycle.Component and observability.Checkable.
var _ lifecycle.Component   = (*MyClient)(nil)
var _ observability.Checkable = (*MyClient)(nil)

func (c *MyClient) OnInit() error                        { /* open connection */ }
func (c *MyClient) OnStart() error                       { return nil }
func (c *MyClient) OnStop() error                        { /* close connection */ }
func (c *MyClient) HealthCheck(ctx context.Context) error { /* ping */ }
func (c *MyClient) Name() string                          { return "my-client" }
func (c *MyClient) Priority() observability.Level         { return observability.LevelCritical }

Implementing structured errors

// MyErr satisfies errs.CodedError and errs.ContextualError.
var _ errs.CodedError       = (*MyErr)(nil)
var _ errs.ContextualError  = (*MyErr)(nil)

func (e *MyErr) ErrorCode()              string         { return e.code }
func (e *MyErr) ErrorContext()           map[string]any { return e.ctx }

The logger in core consumes these interfaces to append error_code and context fields to every log record automatically — without either package importing the other.

Working with identity and permissions

// Store identity after authentication.
ctx = security.SetInContext(ctx, security.NewIdentity(uid, name, email))

// Retrieve identity downstream.
id, ok := security.FromContext(ctx)

// Resolve and check permissions.
mask, err := provider.ResolveMask(ctx, id.UID, "orders")
if !mask.Has(PermWrite) { /* deny */ }

Dependency Rules

contracts sits at the root of the Einherjar dependency graph:

contracts
    └── core          (absorbs launcher, logz, xerrors, valid)
         ├── web      (absorbs httpserver, httpmw, httputil, health)
         │    └── auth
         │         ├── auth-jwt
         │         └── auth-firebase
         └── db-*, cache-*, storage-*, telemetry, worker, httpclient

Changes flow outward from contracts, never inward. A starter never drives a contracts change. Before any modification, calculate the blast radius: which interfaces are affected → which starters implement them → which starters consume those starters. Release sequence is always contracts first, then implementors, then consumers.


Compliance

go build ./...   # zero external dependencies
go vet ./...
go test ./...    # structural (one type per file) + behavioural tests
gofmt -l .       # no output

The Einherjar did not train for themselves. They trained for Ragnarök — for the battle they knew was coming, for those who would need them to be ready. Write code the same way.