Architecture
A Master app is a decoupled full-stack monorepo. The browser talks to a Next.js frontend; the frontend talks to a MasterController API; the API talks to your database through the MasterRecord ORM. The Master CLI scaffolds all of it and runs the whole stack with one command.

The stack, top to bottom#
The App Router UI users actually see and touch. Server Components fetch data, Client Components add interactivity. This is where everything comes together visually.
A fast ESM MVC server with ASP.NET Core-style plumbing. Routes map URLs to controllers; controllers bind and validate input, use a request-scoped database context, and reply with typed JSON results. Auth, DI scopes, layered configuration, logging, health checks, OpenAPI, caching, compression and real-time sockets are built in.
Code-first models and an expressive query language over SQLite, MySQL, or PostgreSQL. Migrations keep the schema in step with your models.
Next.js: how the front end ties it all together#
In a Master app, Next.js is the entire front end — not a sprinkle of client widgets, but the real, full App Router framework. It is the one place your users meet your application, and it pulls the whole stack into a single, coherent experience.
Server Components fetch from your API#
Most pages are React Server Components. They run on the server, call your MasterController API directly (no CORS, no client round-trip), and stream finished HTML to the browser — fast first paint, SEO-friendly, no loading spinners for the initial view.
import { api } from '../lib/api';
export default async function PostsPage() {
// Runs on the server — talks straight to MasterController.
const { data } = await api<{ data: Post[] }>('/posts');
return <ul>{data.map((p) => <li key={p.id}>{p.title}</li>)}</ul>;
}Client Components add interactivity#
Anything interactive — forms, live updates, optimistic UI — is a 'use client' component that calls the same API from the browser. One typed helper, app/lib/api.ts, is the single doorway between the front end and the back end.
'use client';
import { api } from '../lib/api';
async function createPost(title: string) {
return api('/posts', { method: 'POST', body: JSON.stringify({ title }) });
}One helper, one contract#
Because every call flows through api(), the boundary between frontend and backend is explicit and typed. Change the API base URL once (via NEXT_PUBLIC_API_URL) and the whole front end follows — local, staging, or production.
Inside the backend#
backend/server.js is the composition root — the same job an ASP.NET Core Program.cs does. It builds configuration, registers services, adds middleware, loads routes and controllers, and listens:
NODE_ENV (default development) + master.root + master.userSecretsId
→ master.setupServer('http') configuration: appsettings.json → appsettings.<env>.json
→ env.<env>.json → user secrets → env vars → CLI args
→ master.addScoped('db', AppContext) one MasterRecord context per request (this.db)
→ cors · useResponseCompression · useHealthChecks · useOpenApi · useGracefulShutdown
→ JWT bearer auth + policies (only when Auth:JwtSecret is configured)
→ middleware/*.js auto-loaded, alphabetical
→ master.startMVC('app') app/routes.js + app/controllers
→ master.start(server) finalize pipeline, validate options, start hosted services
→ server.listen(PORT)A request-scoped database context#
There is no database singleton. master.addScoped('db', AppContext) gives every request its own AppContext — created lazily on first use, disposed when the response closes — exactly like a scoped EF DbContext. Controllers reach it as this.db; work outside a request (jobs, scripts) uses withDb(fn) from app/models/db.js, which opens a DI scope. Because MasterRecord’s change tracking is a unit of work, this is what keeps concurrent requests from committing each other’s changes.
Controllers: bind, validate, reply#
export default class PostsController {
static schemas = { // model binding + validation -> this.model
create: { body: { title: { type: 'string', required: true }, body: { type: 'string', required: true } } },
update: { body: { title: { type: 'string' }, body: { type: 'string' } } },
};
// static authorize = { create: true, update: true, destroy: true }; // [Authorize] once auth is on
constructor(requestObject) { this.requestObject = requestObject; }
async index() {
const data = await this.db.Post.toList(); // this.db = this request's AppContext
this.ok({ data });
}
async create() {
const item = this.db.Post.new();
Object.assign(item, this.model); // already validated (else 400 ValidationProblemDetails)
await this.db.saveChanges();
this.created(`/posts/${item.id}`, { data: item });
}
}Operational endpoints, for free#
/health— the welcome page’s JSON ping;/health/live,/health/ready(runs the database check) and/health/reportfor load balancers and orchestrators./openapi.json— an OpenAPI 3.1 document generated from routes, typed params,static schemasand auth; Swagger UI at/docsoutside production.- Responses are gzip/brotli compressed;
SIGTERMdrains in-flight requests before exit.
The life of a request#
- A user opens
/postsin the browser → Next.js renders the page. - The Server Component calls
api('/posts')→ an HTTP request to MasterController. - The pipeline runs (logging, compression, auth if configured); the router matches
GET /posts→posts#index, checks typed params andauthorize, and binds/validates input fromstatic schemas. - A fresh
AppContextis created for the request; the controller runsthis.db.Post.toList(). - MasterRecord builds and runs the SQL, returns tracked model instances.
- The controller replies with
this.ok(...); the scope (and its context) is disposed; Next.js renders the HTML and streams it back.
Why decoupled?#
- Independent scaling & deployment — host the Next.js front end on the edge and the API on a Node host; scale each to its own load.
- Clean separation of concerns — UI code never touches SQL; API code never renders HTML.
- Reusable API — the same MasterController API can serve a mobile app or third party, not just your website.
- One dev experience — despite being two services,
master devruns them together with shared config and hot reload.