Skip to main content

Choosing a data layer

A Nucleus app has two good options for talking to its database, and this page exists because the honest answer is not "always use the suite's ORM". Nucleus was designed SQL-first on purpose; Quark earns its place only when its features are ones you'll actually use.

The two options​

pkg/db + pkg/model — what Nucleus ships. A thin, deliberate layer over database/sql: connection management, health checks, telemetry, and struct metadata for scaffolding. You write SQL. Migrations are SQL files applied by the CLI (nucleus migrate), reviewable in a PR diff and reversible. There is no query builder and no ORM — that is a design decision of the framework, not a gap.

Quark — a full ORM, and a product of its own. Typed queries through Go generics (quark.For[User](ctx, client).Where(...).List() returns []User, no casts), the same query code across six SQL engines, relations with eager loading, soft deletes, batch operations, an L2 cache, and native multi-tenancy up to PostgreSQL row-level security. Schema comes from struct tags; migrations can be derived or versioned.

When pkg/db is enough​

  • You like SQL. If your team reads and reviews SQL comfortably, the framework's migration discipline (SQL files in the repo, applied in order, drift detection) is a feature, not friction.
  • The queries are few and known. A service with a dozen well-understood statements doesn't amortize an ORM's concepts.
  • You want the smallest dependency surface. pkg/db is already in the framework; Quark is a second data-access vocabulary to learn and keep consistent.
  • One database engine, forever. Cross-engine portability is one of Quark's main dividends; if you'll never leave PostgreSQL, you're not collecting it.

Orbit note: Data Studio browses the models in the app's pkg/model registry out of the box — you don't need Quark for an admin CRUD.

When Quark earns its place​

  • Typed reads and writes. No Scan boilerplate, no interface{}; the compiler checks what the database returns.
  • More than one engine. The same query code runs on PostgreSQL, MySQL, MariaDB, SQLite, SQL Server and Oracle — switching is one connection string. (Tests on SQLite, production on Postgres, with the caveats that always implies.)
  • Relations you'd otherwise hand-join. belongs_to / eager loading (Preload) replace a family of repetitive JOINs and N+1 traps.
  • Multi-tenancy as a first-class concern. Database-per-tenant, schema-per-tenant, or row-level security — client-side or PostgreSQL native — without threading tenant IDs through every query by hand.
  • The operational extras — L2 cache with stampede protection, read replicas, audit hooks — when you'd otherwise build them around database/sql yourself.

With Quark you add the two small bridges when you want the suite experience: orbit/quarkdatasource puts Quark models in Data Studio, orbit/quarkbridge puts Quark's SQL in the live feed. Both are wired by the quickstart scaffold and read there, in the generated main.go and shop/module.go.

Can I mix them?​

Yes, and the quickstart app does: Nucleus manages its own app database (auth, sessions, framework tables) through pkg/db while the domain runs on Quark — in that demo, sharing one SQLite file. The two layers don't fight; they just don't share a vocabulary. What you should not do is access the same tables through both layers and expect either one's caching or hooks to see the other's writes.

Deciding in one sentence​

If you'd describe your data needs as "SQL plus discipline", stay on pkg/db and enjoy the smaller surface. If you'd describe them as "typed models with relations, possibly across engines or tenants", take Quark — its getting started is ten minutes.