Skip to main content
Version: 1.15.0

Principles

Five principles guide every decision in Nucleus. This page explains what they are and why they were chosen — read it to understand the reasoning behind the framework's shape. The rest of the documentation is where they show up in practice.

1. Stdlib-first runtime

The runtime is built on net/http, database/sql, log/slog, and context.Context. Every new third-party dependency has to be argued for in writing and reviewed for its impact before it is taken.

The reasoning is operational, not aesthetic:

  • The standard library is the most reviewed Go code in existence; its semantics rarely surprise.
  • Third-party deps are the dominant source of maintenance debt and supply-chain risk.
  • Go's release cadence is predictable; building on the stdlib lets us adopt new features without coordinating across vendors.

When a dependency is genuinely worth taking — Asynq for the task queue, Casbin for RBAC, OpenTelemetry for tracing — it goes behind a framework interface, so that it stays replaceable and never appears in a signature you depend on.

2. Explicit configuration & lifecycle

There are no hidden global singletons, no implicit init() registration, no package-level state masquerading as the framework. Every Nucleus application is created by an explicit call:

a, err := app.New(cfg, opts...)

The implications:

  • Multiple App instances coexist trivially in the same process — end-to-end tests run a real App, not a mock.
  • Lifecycle is observable. App.Run blocks; App.Shutdown runs hooks in reverse order; defer a.Shutdown(ctx) is enough.
  • Wiring is reviewable. The composition root is the project's main.go (or a fluent builder call); nothing happens at import time.

3. Compatibility by contract

The stable surface is explicit:

  • exported symbols in pkg/*,
  • registered CLI commands,
  • registered config keys,
  • the framework services an extension may reach on app.App.

Each of these is frozen by tests under contracts/. Removals require a deprecation entry; rename-and-keep-the-shim is the default path. Details: Compatibility policy.

That last one is newer than the rest, and it exists because of what it replaced. An extension receives the whole application object, and the contract used to say it could set fields on it. That was a blank cheque: whatever an extension reached for became part of the API in practice, while being covered by nothing anyone could promise across versions. No extension ever used the permission, so it cost stability and bought nothing.

An extension now reads framework services, mounts routes and registers middleware. What it may read is frozen like everything else, so adding to it is a deliberate promise and removing from it is a break somebody has to see — which is the difference between having extension points and having an ecosystem.

4. Security by default

Production-sensitive defaults ship enabled, so relaxing one is a deliberate, visible act rather than a missing line:

  • Session cookies are Secure by default (session_cookie_secure: true); plain-HTTP local development must opt out explicitly.
  • CORS denies unknown origins until cors_origins lists them.
  • RBAC is default-deny: a route with no matching policy answers 403.
  • Proxy headers such as X-Forwarded-For are ignored until trusted_proxies names the peers you trust.
  • A hardened set of security headers is applied to every response.
  • Passwords are hashed with bcrypt at cost 12.
  • Under NUCLEUS_ENV=production, an unknown config key is a boot error.

Two protections are deliberately opt-in, because switching them on unconditionally would break valid deployments: CSRF (csrf_enabled, which only applies to cookie-authenticated browser routes) and rate limiting (rate_limit_requests).

Every setting is reachable from nucleus.yml. If you need to relax a default, you do it deliberately, in writing.

5. SQL-first operations

There is no ORM. Migrations are SQL files; the CLI applies them in order; queries are written in SQL. pkg/db adds database/sql ergonomics, telemetry and health checks; pkg/model adds metadata for the admin panel and CRUD helpers.

This is a constraint we want, not one we tolerate:

  • Postgres, MySQL and SQLite all behave differently in subtle ways. MSSQL and Oracle add their own dialects on top (opt-in via the -tags mssql / -tags oracle build tags). Hiding any of this behind an ORM either produces the lowest-common-denominator query or quietly emits incompatible SQL.
  • Migrations as SQL files are reviewable and replayable independently of the binary that wrote them. Oracle multi-block PL/SQL scripts are split correctly by db.ExecScript so the slash-terminator works as developers expect (see Models & database → Multi-block scripts).
  • The CLI is deterministic: the same nucleus migrate against the same database ends in the same state. nucleus migrate drift reports divergence between the live schema and the registered models on every supported engine.

What follows from the principles