neokit

package module
v0.13.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 28, 2026 License: MIT Imports: 8 Imported by: 0

README

neokit

Composable Go building blocks for small services. Neokit does not replace the standard library or a web framework; it is the operational layer around them — safe HTTP clients, structured errors and logs, health checks, ordered shutdown, observability, caching, and optional integrations.

neokit builds on Fiber v3. app.HTTP is a *fiber.App and handlers are ordinary Fiber handlers — a deliberate choice, not an implementation detail waiting to be abstracted away.

Take one package at a time, or start with app when a service wants the whole boot sequence from one constructor. There is no service container and no lookup by string: every dependency app builds is an exported field on it — including a.API, a route group already mounted at a.APIBase, so route registration never concatenates a version prefix by hand — and a handler stays an ordinary Fiber handler over ordinary types. Reflection is confined to the two places Go offers no alternative — decoding the environment in config, and validator tags in fiberx. Nothing reaches a binary unless its package is imported.

The one global: app.New installs its logger as slog.Default(), unless you pass your own through Options.Log.

Pre-1.0: the API may change between v0.x releases.

A new project

There is no generator. A service starts as one file, and each feature is one call — calling it is enabling it:

cfg, err := config.Load[config.Base]()
a, err := neokit.New(app.Options{Name: "okstables", Base: cfg})
defer a.Close()

db, err := a.Database(cfg.DatabasePath, nil)   // ✓ database + ordered shutdown

a.API.Get("/hello", func(c fiber.Ctx) error {  // ✓ mounted at /api/v1
    return c.JSON(fiber.Map{"hello": "okstables"})
})
return a.Run()

neokit.New is the batteries-included layer — a.Database, a.Login, a.Backups, a.Ntfy — and importing it compiles every feature it fronts. A service that wants less imports neokit/app and the feature packages it actually uses; they compose identically.

That is the whole boot, and it is already a production shape. Load, New, Close, Run — four calls — get you structured logging, a request log with trace ids, the {"error": …} envelope, panic recovery, CORS, compression, request/idle timeouts, /healthz, /metrics, OTLP export when you point it somewhere, and an ordered SIGTERM drain. You did not configure any of it and you can override all of it. Readiness is the one thing app does not decide for you — see below.

Even the version fills itself in — go build embeds the commit, so every log line says okstables dev (a1b2c3d, dirty) without a Makefile.

Add your own settings by embedding config.Base in a struct of your own, and your own version once you have release builds to stamp:

type Config struct {
    config.Base
    Issuer string `env:"OIDC_ISSUER"`
}

a, err := app.New(app.Options{
    Name:    "okstables",
    Version: buildinfo.Get(version, commit, date).String(),
    Base:    cfg.Base,
})

A scaffolding command would only write this out for you and then need a template set kept in step with the API forever.

Metrics and probes

One set of instruments, two ways out, no wiring. Every instrument in the process — the HTTP server histogram, Go runtime stats, whatever you declare yourself — is OpenTelemetry-native and leaves by both exits at once:

  • GET /metrics, Prometheus text format, on the application listener. Always mounted, nothing to configure. http.server.request.duration arrives as http_server_request_duration_seconds with your route templates intact.
  • OTLP push to OTEL_EXPORTER_OTLP_ENDPOINT, when you set one. Traces and metrics share the gate; the histogram carries trace exemplars, so a slow bucket in Grafana links straight to its span in Tempo.

Nothing is declared or recorded twice — the two are readers on one MeterProvider, so a metric you add anywhere shows up on both without being touched again.

The endpoint is unauthenticated by default, which is what Grafana and Traefik do and is fine on a private network. Set METRICS_TOKEN and it requires Authorization: Bearer … — the same header Prometheus sends from authorization: in a scrape config. Nothing announces which of the two you are running, so treat the empty default as a decision rather than a leftover.

/healthz is mounted for you and answers 200 unconditionally — a liveness probe that consulted a dependency would have Kubernetes restart a healthy process because its database blinked. Probes and scrapes are registered above the middleware chain, so none of them are logged or counted: a ten-second liveness interval is 8 640 log lines a day, and metering a scrape means recording a data point about the act of collecting data points.

Readiness is yours to mount, because only you know what "ready" means for this service and where the answer may safely be read. health is the registry:

ready := &health.Registry{Log: a.Log}
ready.Register("database", func(ctx context.Context) error { return db.PingContext(ctx) })

a.HTTP.Get("/readyz", adaptor.HTTPHandler(ready.ReadyHandler()))     // {"ready":false}
admin.Get("/readyz", adaptor.HTTPHandler(ready.DetailHandler()))     // the full sweep

ReadyHandler answers with the verdict and nothing else, because the audience on a public port is an orchestrator that reads only the status code, and naming your dependencies there would give away a map of your infrastructure to buy nothing. The detail is not lost:

  • it goes to the log, once per transition rather than once per probe: WARN not ready failing="database: connection refused", then INFO ready;
  • DetailHandler is the same sweep in full, for a route behind your own authentication:
{"ready":false,"checks":[{"name":"database","ok":false,
                          "error":"connection refused","tookMs":3}]}

Shutdown is the same shape — a name and a function, in one place:

a.Shutdown.Push("plugins", func(context.Context) error { return manager.Close() })

The stack unwinds LIFO after the HTTP drain, so a dependency closes after the requests still using it have finished.

Login gate in ten lines

authn, ok := oidcauth.New(oidcauth.Config{
    Issuer:   os.Getenv("OIDC_ISSUER"),   ClientID:     os.Getenv("OIDC_CLIENT_ID"),
    BaseURL:  os.Getenv("OIDC_BASE_URL"), ClientSecret: os.Getenv("OIDC_CLIENT_SECRET"),
})
sessions, err := session.NewSQLite(db)   // creates its own table; or bring your own store
gate := a.Login(fiberauth.Options{
    Provider:     func() *oidcauth.Provider { if !ok { return nil }; return authn },
    Sessions:     sessions,
    CookiePrefix: "myapp",
})
admin := a.API.Group("/admin", gate.RequireOwner())

Login mounts the identity middleware, then the handshake routes — in that order, which is the one a caller cannot arrange by hand: you need the gate to obtain the middleware, and by then the routes would already be mounted.

ok == false (no credentials configured) means the gate is off: the middleware returns immediately, the guards pass through, and the handshake routes 404 — so an app can ship open and close later without a second feature flag.

The expired-session sweep comes with it: a store that can prune in one statement is swept daily for as long as the app runs. Building the gate directly with fiberauth.New(a.App, …) skips only that step — start it yourself with safe.Go(a.Context(), "session sweep", func() { gate.Run(a.Context()) }).

Runnable examples

Need Example
A small HTTP service examples/minimal-api
HTTP, health, metrics, shutdown, and SQLite examples/production-service
A bounded, retrying external API client examples/external-client

Run one from the repository root with go run ./examples/minimal-api. They are ordinary Go types and constructors: copy the pieces your service needs rather than adopting the whole thing.

Packages

app is one way in, not the only one. Every package below stands alone, so a service that wants a retrying HTTP client and nothing else imports httpc and gets exactly that.

Package What
logx slog context handler, canonical Err attribute, request-id propagation
httpc NewHTTPClient, BaseClient, retry transport, APIError, Classify, SSRF guard
fiberx {"error": …} envelope, bind+validate, metrics/logging middleware, rate limiters
cache stale-while-revalidate GetOrFetch
sqlitex PRAGMA user_version append-only migration runner, VACUUM INTO snapshot backup
tracing metrics OpenTelemetry traces and metrics — /metrics to scrape, OTLP to push, one set of instruments
health liveness/readiness registry — bounded sweep, terse public verdict, detail behind your own auth
safe ids clock goroutine recovery, id/token generation, injectable clock
netx AddrInUseHint — a readable message for a listener already bound
disk Usage — free/total filesystem space via syscall.Statfs
session Session, Store, Policy — and NewSQLite, the store neokit ships. Standard library only
oidcauth provider-agnostic OpenID Connect relying party: PKCE, nonce, typed error sentinels
oidcauth/fiberauth the browser half — handshake routes, __Host- cookies, session middleware, guards
jobs Job{Every, Timeout, RunAtStart, Do} — periodic work that is bounded and panic-guarded
lifecycle Signals context + a LIFO named shutdown Stack
pubsub Bus[T] — keyed fan-out for SSE/WebSocket, drop-on-slow
capset generic capability resolution by type assertion, memoised
notify Sender — webhook, ntfy, Apprise, and a Multi fan-out
webpush VAPID keypair generation and sub normalisation (no dependencies)
buildinfo version/commit/date with Go's embedded VCS metadata as the fallback

Every package is independent: importing one never drags in another's dependencies. A binary that does not import oidcauth links neither go-oidc nor oauth2.

MIT.

Documentation

Overview

Package neokit is the batteries-included layer: one constructor, then one method per feature — a.Database, a.Login, a.Backups, the notify senders. Calling a method is enabling the feature; each returns what it builds, so a missing dependency is a compile error, never a runtime lookup.

a, err := neokit.New(app.Options{Name: "myapp", Base: cfg.Base})
if err != nil { return err }
defer a.Close()

db, err := a.Database(cfg.DatabasePath, migrate)
gate := a.Login(fiberauth.Options{Sessions: store})

return a.Run()

Importing this package compiles every feature it fronts — SQLite, OIDC, the notify backends. A service that wants less imports app and the feature packages it actually uses; they compose identically, this is only the convenient spelling.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type App

type App struct{ *app.App }

App is app.App plus one method per neokit feature. Everything the embedded app exports — HTTP, Shutdown, Run — is reachable unchanged.

func New

func New(o app.Options) (*App, error)

New builds the application. See app.New for the boot sequence.

func (*App) Apprise

func (a *App) Apprise(url string, o notify.Options) *notify.Apprise

Apprise builds an Apprise notification sender.

func (*App) Backups

func (a *App) Backups(s backup.Snapshotter, o backup.Options) *backup.Service

Backups wires scheduled database backups and starts the daily schedule. See backup.New.

func (*App) Database

func (a *App) Database(path string, migrate func(*sql.DB) error) (*sql.DB, error)

Database opens the service's SQLite database and registers its teardown. See sqlitex.Open.

func (*App) Login

func (a *App) Login(o fiberauth.Options) *fiberauth.Gate

Login builds the OIDC login gate, mounts its routes, and starts the expired-session sweep. See fiberauth.New.

func (*App) Ntfy

func (a *App) Ntfy(topicURL, token string, o notify.Options) *notify.Ntfy

Ntfy builds an ntfy notification sender.

func (*App) Webhook

func (a *App) Webhook(url, secret string, o notify.Options) *notify.Webhook

Webhook builds a signed-webhook notification sender.

Directories

Path Synopsis
Package app is neokit's application builder: the boot sequence every service would otherwise retype, performed once in a documented order.
Package app is neokit's application builder: the boot sequence every service would otherwise retype, performed once in a documented order.
Package backup turns a snapshot primitive into the two things an application needs: a fresh copy streamed on demand, and a dated copy written to disk on a schedule.
Package backup turns a snapshot primitive into the two things an application needs: a fresh copy streamed on demand, and a dated copy written to disk on a schedule.
Package buildinfo reports the identity of the running binary: the version, commit and build date baked in at link time, plus the Go toolchain and target it was built for.
Package buildinfo reports the identity of the running binary: the version, commit and build date baked in at link time, plus the Go toolchain and target it was built for.
Package cache provides a tiny in-memory TTL cache with per-key single-flight and stale-while-revalidate refreshes.
Package cache provides a tiny in-memory TTL cache with per-key single-flight and stale-while-revalidate refreshes.
Package capset resolves optional capabilities out of a set of named components by type assertion, and memoises the answer.
Package capset resolves optional capabilities out of a set of named components by type assertion, and memoises the answer.
Package clock supplies the two implementations of a Now() time.Time dependency: the real one, and a fake a test drives by hand.
Package clock supplies the two implementations of a Now() time.Time dependency: the real one, and a fake a test drives by hand.
Package config holds the environment settings every service has, and the one call that parses them.
Package config holds the environment settings every service has, and the one call that parses them.
Package disk reports free and total space on the filesystem holding a path.
Package disk reports free and total space on the filesystem holding a path.
Package errs holds the error sentinels neokit's HTTP layer recognises without being told.
Package errs holds the error sentinels neokit's HTTP layer recognises without being told.
examples
external-client command
External-client calls a fixed upstream with a bounded timeout, idempotent retry policy, optional tracing, and structured errors.
External-client calls a fixed upstream with a bounded timeout, idempotent retry policy, optional tracing, and structured errors.
minimal-api command
Minimal-api is the smallest service built with app: configuration, standard middleware, structured errors, and graceful shutdown with one constructor.
Minimal-api is the smallest service built with app: configuration, standard middleware, structured errors, and graceful shutdown with one constructor.
production-service command
Production-service is the batteries-included layer: neokit.New, then one call per feature.
Production-service is the batteries-included layer: neokit.New, then one call per feature.
Package fiberx holds cross-cutting Fiber v3 HTTP helpers: the error envelope, request binding/validation, metrics+logging middleware, rate limiting and optimistic-concurrency helpers.
Package fiberx holds cross-cutting Fiber v3 HTTP helpers: the error envelope, request binding/validation, metrics+logging middleware, rate limiting and optimistic-concurrency helpers.
Package health answers the two questions an orchestrator asks, and keeps them apart.
Package health answers the two questions an orchestrator asks, and keeps them apart.
Package httpc is the HTTP client core: a BaseClient for JSON APIs with built-in retries, bearer-token injection/refresh, and bounded reads; a Fault classification (see fault.go) that turns an error into "what should the caller do about it" instead of a status code to grep; and an SSRF-guarded transport (see ssrf.go) for fetching URLs a caller only partly controls.
Package httpc is the HTTP client core: a BaseClient for JSON APIs with built-in retries, bearer-token injection/refresh, and bounded reads; a Fault classification (see fault.go) that turns an error into "what should the caller do about it" instead of a status code to grep; and an SSRF-guarded transport (see ssrf.go) for fetching URLs a caller only partly controls.
Package ids generates identifiers and secrets: UUIDGenerator for v4 UUIDs and CryptoTokenGenerator (see tokengen.go) for crypto-random tokens and passwords.
Package ids generates identifiers and secrets: UUIDGenerator for v4 UUIDs and CryptoTokenGenerator (see tokengen.go) for crypto-random tokens and passwords.
Package jobs runs periodic background work correctly.
Package jobs runs periodic background work correctly.
Package lifecycle owns process startup and shutdown: a context cancelled by the usual termination signals, and an ordered stack of teardown steps.
Package lifecycle owns process startup and shutdown: a context cancelled by the usual termination signals, and an ordered stack of teardown steps.
Package logx holds the small slog helpers that make logs correlatable and consistent: a request-ID carried on the context, a handler decorator that stamps it onto every record, and a canonical error attribute.
Package logx holds the small slog helpers that make logs correlatable and consistent: a request-ID carried on the context, a handler decorator that stamps it onto every record, and a canonical error attribute.
Package metrics wires the OpenTelemetry metrics pillar — the sibling of package tracing, and the third leg (with tracing plus the trace-correlated logs logx already emits) of a full traces + metrics + logs setup.
Package metrics wires the OpenTelemetry metrics pillar — the sibling of package tracing, and the third leg (with tracing plus the trace-correlated logs logx already emits) of a full traces + metrics + logs setup.
Package netx holds small network helpers.
Package netx holds small network helpers.
Package notify sends short operational notifications to the places a self-hosted deployment already has: a generic JSON webhook, an ntfy topic, or an Apprise API server.
Package notify sends short operational notifications to the places a self-hosted deployment already has: a generic JSON webhook, an ntfy topic, or an Apprise API server.
Package oidcauth is a provider-agnostic OpenID Connect relying party: the half of a login that talks to an identity provider.
Package oidcauth is a provider-agnostic OpenID Connect relying party: the half of a login that talks to an identity provider.
fiberauth
Package fiberauth is the browser-facing half of oidcauth: the Fiber v3 routes, cookies and middleware that turn a verified identity into a session and back again.
Package fiberauth is the browser-facing half of oidcauth: the Fiber v3 routes, cookies and middleware that turn a verified identity into a session and back again.
Package pubsub is an in-process, topic-keyed fan-out for pushing events at connected clients — Server-Sent Events and WebSocket streams, where each browser subscribes to one topic (a user id, a job token, a room) and wants only that topic's events.
Package pubsub is an in-process, topic-keyed fan-out for pushing events at connected clients — Server-Sent Events and WebSocket streams, where each browser subscribes to one topic (a user id, a job token, a room) and wants only that topic's events.
Package safe guards background work that runs outside a request's own recover middleware — schedulers, event hooks, detached goroutines — so a panic there is logged with a stack trace and contained, rather than crashing the whole process or silently ending that one goroutine forever.
Package safe guards background work that runs outside a request's own recover middleware — schedulers, event hooks, detached goroutines — so a panic there is logged with a stack trace and contained, rather than crashing the whole process or silently ending that one goroutine forever.
Package session is the vocabulary for a signed-in browser, and one store that persists it.
Package session is the vocabulary for a signed-in browser, and one store that persists it.
Package sqlitex provides SQLite operational helpers — a forward-only migration runner and a consistent snapshot — with no schema of its own.
Package sqlitex provides SQLite operational helpers — a forward-only migration runner and a consistent snapshot — with no schema of its own.
Package tracing wires OpenTelemetry distributed tracing for the HTTP server.
Package tracing wires OpenTelemetry distributed tracing for the HTTP server.
Package webpush holds the parts of Web Push (RFC 8030) that are pure key handling: generating a VAPID keypair (RFC 8292) and normalising the `sub` contact that rides in every VAPID JWT.
Package webpush holds the parts of Web Push (RFC 8030) that are pure key handling: generating a VAPID keypair (RFC 8292) and normalising the `sub` contact that rides in every VAPID JWT.
delivery
Package delivery sends Web Push messages to the browsers subscribed to a topic, and prunes the ones that have gone away.
Package delivery sends Web Push messages to the browsers subscribed to a topic, and prunes the ones that have gone away.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL