A Go framework over Gin that lets you write plain handlers,
wires them into REST + GraphQL + WebSocket from one signature, and ships a live dashboard at /__nexus/.
Why · Install · Quick start · Handlers · Dashboard · Peer mesh · Config
func main() {
nexus.Run(
nexus.Config{
Server: nexus.ServerConfig{Addr: ":8080"},
Dashboard: nexus.DashboardConfig{Enabled: true, Name: "Adverts"},
},
nexus.ProvideResources(NewMainDB),
adverts.Module,
)
}
var Module = nexus.Module("adverts",
nexus.Provide(NewService),
nexus.AsQuery(NewListAdverts),
nexus.AsMutation(NewCreateAdvert,
auth.Required(),
auth.Requires("ROLE_CREATE_ADVERT"),
),
)- One handler, three transports.
AsRest/AsQuery/AsMutation/AsWSall read the same reflective signature —func(svc, deps..., p nexus.Params[Args]) (T, error)— and wire the right transport. - Live architecture view.
nexus.Modulegroups endpoints; constructor introspection draws service → service / service → resource edges automatically. Real traffic pulses on the edges. - Built-in auth, rate limits, metrics, traces. Cross-transport bundles via
nexus.Use. Per-op observability is free — every handler gets counters + traces with no user code. - Typed peer mesh.
peer.AsCallexposes a handler to other apps;peer.Call[T]calls one. HTTP/2 + JSON over mTLS, persistent multiplexed connections, schema drift detection, trace stitching across binaries. See Peer mesh below. - Configuration server. Spring-Cloud-Config-style distribution:
config.Serverhosts plaintext TOML (local folder or git);config.Clientfetches signed snapshots with a sealed cache;nexus.Get[T]("key", default)reads typed values from anywhere. WS push for sub-second hot reload.${VAR}placeholders insidenexus.tomlresolve from the process env (and optionally a.envfile vianexus.LoadDotenvIfPresent()). See Configuration below. - Guided tours for any frontend.
extension/tourmounts a Shadow-DOM overlay on every HTML response — record click-by-click walkthroughs with auto-screenshots (multi-scene capture for dropdowns + modals), edit step text inline on the preview, play back as numbered-badge highlights, export to PDF or Word for handoff docs. Works on React, Vue, Angular, vanilla — host CSS can't leak in. See Tours below. - Vite frontend, embedded.
nexus new --frontend vue|reactscaffolds a standard Vite project underweb/.nexus devruns the Vite dev server (HMR) and proxies the API;nexus buildrunsvite buildandgo buildembedsweb/distvia//go:embed. Any Vite plugin, Tailwind, or component library works. Node/npm are build- and dev-time only — the runtime is a single Go binary. See Frontend. - fx under the hood, not in your imports.
nexus.Run/Module/Provide/Invokewrap fx so you get DI + lifecycle without the import.
go install github.com/paulmanoni/nexus/cmd/nexus@latestGo 1.25+. The CLI is pure Go — no cgo, no C toolchain, no build tags — so it
cross-compiles to a single static binary. The frontend uses Vite, so building or
running nexus dev on a project that has a web/ frontend also needs Node.js +
npm on PATH (build- and dev-time only; the compiled app embeds web/dist and needs
neither at runtime).
nexus new my-app && cd my-app
nexus dev # go run + live dashboard (add --open to launch a browser)| Command | What it does |
|---|---|
nexus new <dir> |
Scaffold a runnable app (prompts for frontend / db / cache / auth). |
nexus init [dir] --frontend=vue|react |
Add a Vite frontend (web/, embed) to an existing project. |
nexus dev [dir] |
go run + the Vite dev server (HMR on :5173); serves the dashboard and proxies /__nexus, /graphql, /oauth, /ws to the app. |
nexus build [package] |
npm install (if needed) + vite build → web/dist, then go build embeds it via //go:embed. |
nexus docs [topic] |
Inline reference (handlers, frontend, auth, …). |
nexus client [--out dir] |
Write the embedded JS/TS client SDK to disk. |
nexus generate <kind> |
Code-generation helpers (e.g. dockerfile). |
// main.go
package main
import "github.com/paulmanoni/nexus"
func main() {
nexus.Run(
nexus.Config{
Server: nexus.ServerConfig{Addr: ":8080"},
Dashboard: nexus.DashboardConfig{Enabled: true, Name: "Demo"},
},
helloModule,
)
}
// hello.go
type HelloService struct{}
func NewHelloService() *HelloService { return &HelloService{} }
type SayHelloArgs struct{ Name string }
func (s *HelloService) SayHello(_ context.Context, p nexus.Params[SayHelloArgs]) (string, error) {
return "Hello, " + p.Input.Name, nil
}
var helloModule = nexus.Module("hello",
nexus.Provide(NewHelloService),
nexus.AsQuery((*HelloService).SayHello),
)nexus dev runs it and serves the dashboard at http://localhost:8080/__nexus (add --open to launch a browser), exposing the handler over GraphQL ({ sayHello(name:"world") }) and REST (POST /hello/sayHello).
One signature, three transports:
// REST: POST /widgets
nexus.AsRest("POST", "/widgets", NewCreateWidget)
// GraphQL: mutation createWidget(input: WidgetInput!): Widget!
nexus.AsMutation(NewCreateWidget)
// GraphQL query: query listWidgets(filter: WidgetFilter): [Widget!]!
nexus.AsQuery(NewListWidgets)
// WebSocket event: emit { type:"chat.send", payload:{...} }
nexus.AsWS("/events", "chat.send", NewChatSend)Each constructor returns a function with the canonical shape:
func NewCreateWidget(svc *WidgetService) func(context.Context, nexus.Params[CreateArgs]) (Widget, error)The framework introspects: *WidgetService becomes an fx dependency; Params[CreateArgs] declares the input schema; the return type becomes the GraphQL field type. Args + return get auto-mapped to GraphQL input + object types — no schema duplication.
type Advert struct {
ID uuid.UUID `nexus:"key"`
Title string
Body string
}
var Module = nexus.Module("adverts",
nexus.ProvideCRUD[Advert]("adverts"),
)Mounts:
GET /adverts → list
GET /adverts/:id → get
POST /adverts → create
PUT /adverts/:id → update
DELETE /adverts/:id → delete
Plus the corresponding GraphQL queries + mutations. Stores ship for postgres, mysql, sqlite, in-memory; pick via ProvideStore[Advert](store.Postgres{...}).
nexus.AsMutation(NewCreateAdvert,
auth.Required(),
auth.Requires("ROLE_CREATE_ADVERT"),
ratelimit.PerUser(100, time.Minute),
)Works identically on REST, GraphQL, and WS. Each middleware reads from nexus.Context rather than *gin.Context so it's transport-agnostic.
The frontend is a standard Vite project under web/ — npm-managed, so any Vite plugin, Tailwind, PostCSS, or component library works. nexus build runs vite build and go build embeds the output into the binary; the runtime needs no Node.
nexus new my-app --frontend vue # scaffold web/ (or --frontend react)
cd my-app/web && npm install # install frontend deps (one-time / on clone)
cd .. && nexus dev # go run + Vite dev server (HMR) on :5173
nexus build # vite build → web/dist, embedded via //go:embedmain.go embeds the build output and serves it:
//go:embed all:web/dist
var webFS embed.FS
nexus.Run(nexus.Config{...},
nexus.ServeFrontend(webFS, "web/dist"),
helloModule,
)Unknown paths fall through to index.html (SPA-aware); /assets/* gets immutable cache; REST/GraphQL/WS/dashboard routes win on conflict. Boot fails fast if index.html is missing, so the scaffold commits a web/dist/index.html stub — the first go build compiles before any vite build. The frontend dir is web/ by default (override with NEXUS_FRONTEND_DIR); to mount an existing pre-built SPA, just point ServeFrontend at its embedded dist.
nexus dev runs npm run dev (the Vite dev server, with HMR) on http://localhost:5173/ alongside go run on :8080. It injects a managed proxy block into web/vite.config.ts (between // @nexus:proxy-start / // @nexus:proxy-end markers) so /__nexus, /graphql, /oauth, and /ws reach the Go app from the Vite origin. Open :5173 for the SPA during dev; the embedded web/dist is served at the app port only in production. Override the dev-server command with --frontend-cmd.
Under nexus dev (NEXUS_DEV=1), the browser auto-reloads on source changes. The watcher ignores hidden files, sourcemaps, and runtime data artifacts out of the box — SQLite databases (.db / .sqlite / .sqlite3), their -wal / -shm / -journal sidecars, and .log files — so a request that writes the database can't trigger a reload loop.
Add app-specific ignore globs via Config.DevReload.Exclude (or [runtime.devreload] in nexus.toml). Each changed file is matched against its base name, its path relative to the watch root, and as a directory-subtree prefix:
[runtime.devreload]
exclude = ["uploads", "*.tmp", "cache/*.json"]Invalid patterns are logged once at boot and skipped.
nexus dev prints a NEXUS wordmark on startup. Drop a banner.txt in the directory you run nexus dev from to replace it with your own — the file's contents are rendered verbatim above the target / starting rows, so a project can ship its own ASCII wordmark or message:
$ cat banner.txt
~ acme admin ~
staging
An empty or missing banner.txt falls back to the built-in NEXUS banner.
Mounted at /__nexus/ when Dashboard.Enabled is true. Shows:
- Architecture: services, resources, endpoints, dependency edges. Live updates as registry mutates.
- Traces: request waterfalls across services; cross-transport stitched.
- Crons: scheduled jobs + last/next-run.
- Rate limits: per-key bucket state.
In production you usually want the dashboard surface (/__nexus/*) reachable from operators but not from the public internet. Nexus does this with named listeners + scopes — one process binds multiple ports, each gated to a subset of routes.
nexus.Config{
Server: nexus.ServerConfig{
Listeners: map[string]nexus.Listener{
"public": {Addr: ":8080", Scope: nexus.ScopePublic},
"admin": {Addr: "127.0.0.1:9090", Scope: nexus.ScopeAdmin},
},
},
}ScopePublic— user routes (REST / GraphQL / WebSocket) +/__nexus/health+/__nexus/ready. Dashboard hidden.ScopeInternal— same as public; intended for sidecar / peer traffic.ScopeAdmin— everything: dashboard + user routes (so the in-page testers' relative fetches still work).
Routes outside the listener's scope return 404 — byte-equivalent to "not mounted", so scanners can't fingerprint the dashboard.
Belt-and-braces on top of scopes — defaults to closed so a misconfigured listener doesn't leak the dashboard:
nexus.Config{
Introspection: false, // closed in prod (default)
IntrospectionNetworks: []string{"10.0.0.0/8"}, // VPN bypass
}When closed, every /__nexus/* request 404s unless the caller's TCP peer matches an allowlisted CIDR. The gate uses RemoteIP(), not ClientIP() — X-Forwarded-For is spoofable, wrong default for a security gate. Behind an LB, prefer the admin-listener pattern over the CIDR allowlist.
GraphQL field introspection (the __schema query) is gated by the same Introspection flag — closing the dashboard also closes in-band GraphQL introspection.
Set Listener.TLS to terminate HTTPS on that port — the raw TCP listener gets wrapped at bind time:
adminTLS, err := nexus.ServerTLSConfig(
"admin.crt", // PEM cert
"admin.key", // PEM private key
"admin-ca.crt", // optional CA bundle; pass "" to skip mTLS
)
if err != nil { log.Fatal(err) }
nexus.Config{
Server: nexus.ServerConfig{
Listeners: map[string]nexus.Listener{
"public": {Addr: ":8080", Scope: nexus.ScopePublic},
"admin": {Addr: "10.0.0.5:9443", Scope: nexus.ScopeAdmin, TLS: adminTLS},
},
},
}When the optional CA file is supplied, the listener requires clients to present a certificate signed by that CA — mTLS, enforced at the TLS layer before any HTTP request is dispatched. Pass "" for server-only TLS.
ServerTLSConfig sets MinVersion = TLS 1.2 by default. For custom cipher suites or SNI via GetCertificate, build a *tls.Config directly and assign it to the TLS field — anything crypto/tls supports works.
For public-internet HTTPS with Let's Encrypt auto-issuance, prefer
extension/tls.Plugin— it owns its own:443/:80pair and handles ACME challenges.Listener.TLSis the right tool for admin/internal ports fronted by your own cert material (internal CA, mTLS-protected dashboard, etc.).
adminTLS, _ := nexus.ServerTLSConfig("admin.crt", "admin.key", "admin-ca.crt")
nexus.Run(
nexus.Config{
Introspection: false,
IntrospectionNetworks: []string{"10.0.0.0/8"},
Server: nexus.ServerConfig{
Listeners: map[string]nexus.Listener{
"public": {Addr: ":8080", Scope: nexus.ScopePublic},
"admin": {Addr: "10.0.0.5:9443", Scope: nexus.ScopeAdmin, TLS: adminTLS},
},
},
},
tls.Plugin(tls.Config{ // public :443 + :80, LE-issued cert for the user-facing port
Domains: []string{"app.example.com"},
Email: "ops@example.com",
}),
// ... app modules
)Result: world-facing :443 serves user traffic over LE-issued HTTPS. Dashboard is reachable only on the private 10.0.0.5:9443 with an mTLS client cert signed by admin-ca.crt. /__nexus/* is 404 on every other surface even if the network somehow leaks.
Typed RPC between nexus apps over HTTP/2 + JSON. One persistent multiplexed connection per peer pair, mTLS by default, trace stitching across binaries, schema-drift detection — explicit, type-safe, no codegen.
// callee
peer.Module(peer.Config{Identity: "orders-svc", Listen: ":7000", ...})
// + nexus.AsCall("createOrder", NewCreateOrder)
// caller
peer.Module(peer.Config{Identity: "checkout-svc", Peers: map[string]peer.PeerSpec{...}})
order, err := peer.Call[*Order](ctx, peers, "orders-svc", "createOrder", args)Full reference: extension/peer/README.md covers the safety nets (schema drift, trace stitching, health prober, concurrency cap), SRV discovery for replicas, three auth modes (mTLS / HMAC / none), and the nexus pki PKI toolchain.
Two on-disk config surfaces, distinct purposes:
| Surface | Auto-loaded | Purpose |
|---|---|---|
nexus.toml |
yes (./nexus.toml) |
Deploy manifest — topology, declared inputs, plugin blocks. What the platform sees + provisions. |
extension/config |
opt-in | Runtime key/value store; nexus.Get[T]("key", default) from any handler. |
nexus.toml supports ${VAR} / ${VAR:default} placeholders (strict mode) and an opt-in .env loader via nexus.LoadDotenvIfPresent() for dev workflows.
Full reference: extension/config/README.md covers both surfaces — config.Server / config.Client / config.Local, the sealed-cache + signed-snapshot security model, env-var expansion rules, and the dotenv contract.
Guided walkthroughs for any HTTP-served frontend. Record click-by-click sequences with auto-screenshots, edit inline, replay as numbered-badge highlights, export to PDF / Word for handoff docs. Closed Shadow DOM mount — works on React, Vue, Angular, vanilla without CSS bleed in either direction.
nexus.Run(nexus.Config{...},
tour.Module(tour.WithGORM(db.DB()), tour.AutoInject(true)),
)After restart, every HTML page carries a floating ● Tour pill; authoring dashboard is at /__nexus/tour.
Full reference: extension/tour/README.md covers recording semantics, multi-scene capture for dropdowns/modals, the preview-page editor, Word/PDF export with TOC, and the keyboard shortcuts.
The framework ships a few self-contained examples under examples/:
petstore |
REST + GraphQL CRUD; the canonical "small app" |
petstore-spa |
Same with a frontend bundle served from embed.FS |
pubsub |
Redis + RabbitMQ adapters |
wsecho, wstest |
WebSocket patterns |
graphapp |
GraphQL-first app with cross-resolver auth |
fxapp |
Direct fx wiring without the nexus.Run wrapper |
. framework root + public API surface
├── cmd/nexus/ CLI binary
├── extension/ optional integrations (auth, oauth2, ratelimit,
│ metrics, cron, dashboard, frontend, peer,
│ config, tls, …)
├── graph/ GraphQL builder + resolver introspection
├── transport/ gin/REST + WS adapters
├── manifest/ app self-description JSON (served at /__nexus/manifest)
├── registry/ endpoint + service + resource graph
└── examples/ self-contained sample apps
MIT.

