Starting out all 3 written
What a framework adds to React, what a Next.js project looks like on disk, and the three commands: dev, build and start.
1 Why a framework, and what Next.js adds
React draws a screen from data and has no opinion about anything else. A real site also needs addresses that lead to pages, a server that sends HTML, a way to get data, code that is compiled and split for the browser, and somewhere to answer requests that are not pages. Next.js is a framework that supplies those, with React on top. This lesson looks at what it sends back, using a small project the course built and started on this machine.
What "really ran" means here. Every panel in this course is executed when
the course is built. A panel that talks to a server talks to a real next start the
build launched on 127.0.0.1, on a port it chose, inside a sandbox that allows
nothing but loopback. Lesson 3 shows the sandbox. The badge on each panel names the command.
What React leaves out, and what Next.js adds
The React course starts from one idea: an element describes a piece of screen, and a renderer turns descriptions into HTML. That is all React does. Next.js sits around it:
- Routing. A folder and a file name are an address. There is no route table to write (lesson 4).
- Rendering on the server. Components run on the server by default, and the HTML they produce is what the browser receives first (lessons 8 to 12).
- Data and caching. A server component can
awaitits data, and Next.js decides what to remember (lessons 13 to 16). - Endpoints. A
route.jsfile answers requests that are not pages, and Server Actions answer form posts (lessons 17 and 19). - A build. Your code is compiled, split into files for the browser, and prerendered where it can be (lesson 3 and lesson 29).
Underneath it is still an HTTP server, the kind the Node.js course builds by hand. The difference is that you write components and files, and Next.js writes the server.
The first response already holds the page
The project the panels below talk to has one page. Its source is shown above the output, and
it is the real file Next.js compiled. The driver script asks the running server for
/ with fetch and looks at the text that comes back, before any
JavaScript could have run.
app/layout.js
export const metadata = {
title: 'Shelf',
description: 'A small app for the Next.js course',
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<div data-layout="root">{children}</div>
</body>
</html>
);
}
app/page.js
import { version } from 'react';
import ClientVersion from './ClientVersion';
export default function Home() {
return (
<main>
<h1>Hello from the server</h1>
<p>
Rendered by React <span data-where="server">{version}</span> on the server, and by React{' '}
<ClientVersion /> in a client component.
</p>
</main>
);
}
home.mjs
const base = process.env.BASE_URL;
const res = await fetch(base + '/');
const html = await res.text();
console.log(res.status, res.headers.get('content-type'));
console.log(html.match(/<h1>.*?<\/h1>/)[0]);
console.log(html.match(/<title>.*?<\/title>/)[0]);
check('status', res.status, 200);
check('content type', res.headers.get('content-type'), 'text/html; charset=utf-8');
check('the page content is in the HTML', html.includes('<h1>Hello from the server</h1>'), true);
check('the title comes from the layout', html.includes('<title>Shelf</title>'), true);
check('React payload for the browser is in the same response', html.includes('self.__next_f.push'), true);
What the driver printed (Next 16.3.1, next build + next start), when the build ran it
node home.mjs
200 text/html; charset=utf-8 <h1>Hello from the server</h1> <title>Shelf</title> ok status ok content type ok the page content is in the HTML ok the title comes from the layout ok React payload for the browser is in the same response
The driver ran against next start -H 127.0.0.1 -p 0 of project app, right after its next build; BASE_URL is the address Next printed.
The response is a whole HTML document. The heading is in it as text, so a search engine, a
browser with JavaScript off, or fetch sees the page without running anything. The
same response also carries a self.__next_f.push script: that is the data the
browser's copy of React needs to take over the page, and lesson 9 returns to it.
Pages are not the only thing a server answers
A file called route.js answers a request directly, with no page around it. A
file in public/ is served as it is, from the root of the site.
app/api/hello/route.js
export function GET() {
return Response.json({ hello: 'world' });
}
endpoints.mjs
const base = process.env.BASE_URL;
const api = await fetch(base + '/api/hello');
console.log(api.status, api.headers.get('content-type'));
const body = await api.json();
console.log(body);
const file = await fetch(base + '/hello.txt');
console.log(file.status, file.headers.get('content-type'));
console.log((await file.text()).trim());
const missing = await fetch(base + '/nope');
console.log(missing.status);
check('a route handler answers with JSON', body, { hello: 'world' });
check('as application/json', api.headers.get('content-type'), 'application/json');
check('a file in public/ is served from the root', file.status, 200);
check('an address with no file behind it is a 404', missing.status, 404);
What the driver printed (Next 16.3.1, next build + next start), when the build ran it
node endpoints.mjs
200 application/json
{ hello: 'world' }
200 text/plain; charset=UTF-8
A file in public/ is served as it is.
404
ok a route handler answers with JSON
ok as application/json
ok a file in public/ is served from the root
ok an address with no file behind it is a 404
The driver ran against next start -H 127.0.0.1 -p 0 of project app, right after its next build; BASE_URL is the address Next printed.
Which React runs
The project's package.json asks for React 19.2.7, and that is what sits in
node_modules. The page above rendered the React version twice, once in a server
component and once in a client component. The next panel reads both out of the HTML and compares
them with node_modules.
app/ClientVersion.js
'use client';
import { version } from 'react';
export default function ClientVersion() {
return <span data-where="client">{version}</span>;
}
react-version.mjs
import { readFileSync } from 'node:fs';
const html = await (await fetch(process.env.BASE_URL + '/')).text();
const pick = (where) => new RegExp('data-where="' + where + '">([^<]+)<').exec(html)[1];
const server = pick('server');
const client = pick('client');
const installed = JSON.parse(
readFileSync(process.env.APP_DIR + '/node_modules/react/package.json', 'utf8'),
).version;
console.log('server component: ', server);
console.log('client component: ', client);
console.log('node_modules/react:', installed);
check('server and client components render with the same React', server, client);
check('node_modules/react is what package.json asks for', installed, '19.2.7');
check('the App Router does not use that copy', server !== installed, true);
check('it uses a canary build of React 19', /^19\.\d+\.\d+-canary-/.test(server), true);
What the driver printed (Next 16.3.1, next build + next start), when the build ran it
node react-version.mjs
server component: 19.3.0-canary-cbb046ab-20260731 client component: 19.3.0-canary-cbb046ab-20260731 node_modules/react: 19.2.7 ok server and client components render with the same React ok node_modules/react is what package.json asks for ok the App Router does not use that copy ok it uses a canary build of React 19
The driver ran against next start -H 127.0.0.1 -p 0 of project app, right after its next build; BASE_URL is the address Next printed.
The App Router renders with a copy of React that ships inside the next package,
not the one in node_modules, and that copy is a canary build of React 19. The
Next.js installation guide says the same in its own words (it is in
next/dist/docs, inside the vendored package). You still list react and
react-dom in package.json, for the tools that read it. For the React
you write, nothing changes: the features in the
React course, such as server rendering,
are what this copy provides.
2 A project from nothing, and its layout
There is no generator you must use and no hidden configuration. A Next.js project is a handful of files, and this lesson looks at all of them in the project the course built.
The files that make a project
Three things turn a folder into a Next.js project: a package.json that lists
next, react and react-dom and has the three scripts, a
next.config.mjs (optional, every option has a default), and an app
folder whose layout and page files are the site. The package file is described in the
Node.js course, and the
.mjs extension means an ES module, as in
that lesson. The layout and the page in
app/ are plain function components, the kind the
React course writes first.
package.json
{
"name": "app",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "16.3.1",
"react": "19.2.7",
"react-dom": "19.2.7"
}
}
next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
// Stop sending the x-powered-by: Next.js header.
poweredByHeader: false,
// A permanent redirect decided by configuration, before any page is looked for.
async redirects() {
return [{ source: '/shelf', destination: '/books', permanent: true }];
},
// Extra response headers for the addresses that match.
async headers() {
return [{ source: '/about', headers: [{ key: 'x-shelf-config', value: 'set in next.config.mjs' }] }];
},
// Lesson 24: the image optimiser only produces the qualities listed here.
images: { qualities: [60, 75] },
};
export default nextConfig;
app/layout.js
export const metadata = {
title: 'Shelf',
description: 'A small app for the Next.js course',
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<div data-layout="root">{children}</div>
</body>
</html>
);
}
app/page.js
import { version } from 'react';
import ClientVersion from './ClientVersion';
export default function Home() {
return (
<main>
<h1>Hello from the server</h1>
<p>
Rendered by React <span data-where="server">{version}</span> on the server, and by React{' '}
<ClientVersion /> in a client component.
</p>
</main>
);
}
project.mjs
import { readdirSync, readFileSync, statSync } from 'node:fs';
const root = process.env.APP_DIR;
function tree(dir, prefix = '') {
const lines = [];
for (const name of readdirSync(dir).sort()) {
if (name === 'node_modules' || name === '.next') continue;
const dirp = statSync(dir + '/' + name).isDirectory();
lines.push(prefix + name + (dirp ? '/' : ''));
if (dirp) lines.push(...tree(dir + '/' + name, prefix + ' '));
}
return lines;
}
const lines = tree(root);
console.log(lines.join('\n'));
const pkg = JSON.parse(readFileSync(root + '/package.json', 'utf8'));
const next = JSON.parse(readFileSync(root + '/node_modules/next/package.json', 'utf8'));
console.log(pkg.scripts);
console.log('installed next:', next.version);
const served = await (await fetch(process.env.BASE_URL + '/hello.txt')).text();
const direct = await fetch(process.env.BASE_URL + '/public/hello.txt');
check('the project has an app folder with a root layout and page', lines.includes('app/') && lines.includes(' layout.js') && lines.includes(' page.js'), true);
check('and a public folder', lines.includes('public/'), true);
check('the three scripts are next dev, next build and next start', pkg.scripts, { dev: 'next dev', build: 'next build', start: 'next start' });
check('the installed Next.js', next.version, '16.3.1');
check('public/hello.txt is served from the root', served, readFileSync(root + '/public/hello.txt', 'utf8'));
check('and not from /public', direct.status, 404);
What the driver printed (Next 16.3.1, next build + next start), when the build ran it
node project.mjs
.env
.env.local
.env.production
app/
(marketing)/
layout.js
pricing/
page.js
ClientVersion.js
_drafts/
page.js
_lib/
books.js
calls.js
report.js
session-core.js
session.js
shelf.js
store.js
users.js
validate.js
about/
page.js
account/
actions.js
page.js
actions/
actions.js
done/
page.js
page.js
api/
books/
[id]/
route.js
route.js
cookie/
route.js
hello/
route.js
me/
route.js
revalidate/
route.js
static/
route.js
time/
route.js
auth/
actions.js
authors/
[name]/
not-found.js
page.js
books/
NavLink.js
[slug]/
page.js
layout.js
page.js
cache/
default/
page.js
forced/
page.js
isr/
page.js
static/
page.js
timed/
page.js
unstable/
page.js
clock/
connection/
page.js
cookies/
page.js
dynamic/
page.js
search/
page.js
static/
page.js
counter/
Counter.js
describe.js
page.js
dashboard/
layout.js
page.js
settings/
page.js
data/
basic/
page.js
memo/
page.js
parallel/
page.js
waterfall/
page.js
docs/
[...parts]/
page.js
env/
baked/
page.js
client/
ShelfName.js
page.js
leak/
Leaky.js
page.js
page.js
flaky/
error.js
page.js
fonts/
Inter-400.woff2
forms/
SignupForm.js
actions.js
signup/
page.js
thanks/
page.js
guide/
[[...parts]]/
page.js
images/
page.js
layout.js
library/
page.js
login/
page.js
meta/
[slug]/
page.js
layout.js
page.js
moved/
page.js
not-found.js
old-shelf/
page.js
page.js
proxy-demo/
new/
page.js
reports/
loading.js
page.js
request/
bad-set/
page.js
page.js
robots.js
signup/
page.js
sitemap.js
streaming/
blocking/
page.js
page.js
studio/
Collapsible.js
ServerNote.js
ThemeProvider.js
layout.js
page.js
styled/
Card.module.css
layout.js
page.js
styled.css
tags/
action/
page.js
actions.js
list/
page.js
path/
page.js
data/
shelf.json
next.config.mjs
package.json
proxy.js
public/
hello.txt
shelf.png
scripts/
extract-font.mjs
make-png.mjs
{ dev: 'next dev', build: 'next build', start: 'next start' }
installed next: 16.3.1
ok the project has an app folder with a root layout and page
ok and a public folder
ok the three scripts are next dev, next build and next start
ok the installed Next.js
ok public/hello.txt is served from the root
ok and not from /public
The driver ran against next start -H 127.0.0.1 -p 0 of project app, right after its next build; BASE_URL is the address Next printed.
The listing is the whole project except node_modules and .next,
the folder next build writes (lesson 3). In app/, only the files named
page become addresses; layout.js wraps them, and
ClientVersion.js is an ordinary component that happens to live next to them. A file
in public/ is served as it is, and its folder name is not part of the address.
JavaScript, not TypeScript
The files are .js, and they contain JSX. Next.js compiles JSX in .js
files, so nothing needs renaming. Most real projects use TypeScript. This course does not,
because TypeScript is not part of what is vendored here, and Next.js needs the
typescript package as soon as a project has TypeScript files; getting it means the
npm registry, which this course never contacts.
The root layout would look like this, and it is shown without being run.
export const metadata = {
title: 'Shelf',
description: 'A small app for the Next.js course',
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
needs the typescript package, which is not vendored here; getting it means the npm registry, which this offline course never contacts.
Starting a project on your own machine
On a machine with network access the usual start is a generator, which asks a few questions,
writes these files and installs the packages. It is shown and not run, because it downloads from
the npm registry. Doing the same by hand is exactly the three files above and the three packages
named in package.json.
npx create-next-app@latest my-app
cd my-app
npm run dev
downloads create-next-app and every dependency from the npm registry, which this offline course never contacts. The project the panels use was written by hand, file by file.
A src/ folder is optional: if you put app inside src,
Next.js finds it there. Nothing else changes, and the course does not use it.
3 Dev, build and start
A Next.js project has three commands, and they are three different programs sharing one code base. This lesson runs each of them against the real thing and reads what they print. Each one is an ordinary Node.js program started from a terminal, as in the Node.js course, and the two servers are HTTP servers like the one in that course.
next dev: compile what is asked for
next dev is for writing code. It starts a server and compiles a page the first
time someone asks for it, then keeps the result. The driver below asks for /
twice, then for /about, and only prints booleans for the timing: the first request
has to compile, so it is slower than the second by a wide margin on any machine. The second
output block is what next dev itself printed, from its start to the end of the
driver. The project is a separate small one, dev, because a project is either run
in development or built, never both.
app/page.js
export default function Home() {
return (
<main>
<h1>Hello from next dev</h1>
</main>
);
}
dev.mjs
import { readdirSync } from 'node:fs';
const base = process.env.BASE_URL;
async function timed(path) {
const started = performance.now();
const res = await fetch(base + path);
await res.text();
return { status: res.status, ms: performance.now() - started };
}
const first = await timed('/');
const second = await timed('/');
const about = await timed('/about');
console.log('GET / ', first.status);
console.log('GET / again', second.status);
console.log('GET /about ', about.status);
console.log('the first request took more than three times the second:', first.ms > second.ms * 3);
console.log('.next holds:', readdirSync(process.env.APP_DIR + '/.next').sort().join(' '));
check('every request was answered', [first.status, second.status, about.status], [200, 200, 200]);
check('next dev keeps its files in .next/dev and nothing else', readdirSync(process.env.APP_DIR + '/.next').sort(), ['dev']);
check('the first request had to compile', first.ms > second.ms * 3, true);
What the driver printed (Next 16.3.1, next dev), when the build ran it
node dev.mjs
GET / 200 GET / again 200 GET /about 200 the first request took more than three times the second: true .next holds: dev ok every request was answered ok next dev keeps its files in .next/dev and nothing else ok the first request had to compile
What next dev printed meanwhile
▲ Next.js 16.3.1 (Turbopack) - Local: http://127.0.0.1:<port> - Network: http://127.0.0.1:<port> ✓ Ready in <t> ✓ Running next.config.mjs took <t> GET / 200 in <t> (next.js: <t>, application-code: <t>) GET / 200 in <t> (next.js: <t>, application-code: <t>) GET /about 200 in <t> (next.js: <t>, application-code: <t>)
The driver ran against next dev -H 127.0.0.1 -p 0 of project dev; BASE_URL is the address Next printed.
Each request is logged with its status and how long it took; the durations are replaced by
<t> on this page because they differ on every run. In this mode .next holds only a
dev folder; the production output of the next section does not exist yet.
next build: make the production output
next build compiles the whole project, prerenders every page that does not
depend on the request, and writes the result to .next. It ends with a table of the
routes it found. Lesson 29 reads that table line by line; for now the symbols matter:
○ is a page that was rendered once, at build time, and ƒ is code that
runs on each request. The panel runs the build of the app project and then a small
script that looks inside .next.
inspect.mjs
import { readFileSync, statSync } from 'node:fs';
const root = process.env.APP_DIR;
const id = readFileSync(root + '/.next/BUILD_ID', 'utf8');
const routes = Object.keys(JSON.parse(readFileSync(root + '/.next/prerender-manifest.json', 'utf8')).routes).sort();
const swc = statSync(root + '/node_modules/@next/swc-darwin-x64/next-swc.darwin-x64.node');
console.log('build id characters:', id.length);
console.log('prerendered:', routes.join(' '));
console.log('native compiler is a file of more than a megabyte:', swc.size > 1_000_000);
check('the build wrote an id of 21 characters', id.length, 21);
check('it prerendered the static pages of lessons 1 to 4', ['/', '/_global-error', '/_not-found', '/about', '/dashboard', '/dashboard/settings'].map((r) => routes.includes(r)), [true, true, true, true, true, true]);
check('and not the route handler', routes.includes('/api/hello'), false);
check('the compiler is the native binary that came with the vendored tree', swc.size > 1_000_000, true);
What next build printed (Next 16.3.1), then the checks, when the build ran it
next build
▲ Next.js 16.3.1 (Turbopack) - Environments: .env.local, .env.production, .env ✓ Running next.config.mjs took <t> Creating an optimized production build ... ✓ Compiled successfully in <t> Running TypeScript ... Finished TypeScript in <t> ... Collecting page data using <n> workers ... ✓ Generating static pages using <n> workers (59/59) in <t> Finalizing page optimization ... Route (app) Revalidate Expire ┌ ○ / ├ ○ /_not-found ├ ○ /about ├ ƒ /account ├ ƒ /actions ├ ○ /actions/done ├ ƒ /api/books ├ ƒ /api/books/[id] ├ ƒ /api/cookie ├ ƒ /api/hello ├ ƒ /api/me ├ ƒ /api/revalidate ├ ○ /api/static ├ ƒ /api/time ├ ƒ /authors/[name] ├ ○ /books ├ /books/[slug] │ ├ ● /books/middlemarch │ ├ ● /books/dracula │ └ ● /books/persuasion ├ ƒ /cache/default ├ ƒ /cache/forced ├ ○ /cache/isr 2s 1y ├ ○ /cache/static ├ ƒ /cache/timed ├ ƒ /cache/unstable ├ ƒ /clock/connection ├ ƒ /clock/cookies ├ ƒ /clock/dynamic ├ ƒ /clock/search ├ ○ /clock/static ├ ○ /counter ├ ○ /dashboard ├ ○ /dashboard/settings ├ ƒ /data/basic ├ ƒ /data/memo ├ ƒ /data/parallel ├ ƒ /data/waterfall ├ ƒ /docs/[...parts] ├ ƒ /env ├ ○ /env/baked ├ ○ /env/client ├ ○ /env/leak ├ ƒ /flaky ├ ○ /forms/signup ├ ƒ /forms/thanks ├ ƒ /guide/[[...parts]] ├ ○ /images ├ ○ /library ├ ƒ /login ├ ○ /meta ├ ƒ /meta/[slug] ├ ○ /moved ├ ○ /old-shelf ├ ○ /pricing ├ ƒ /proxy-demo/new ├ ƒ /reports ├ ƒ /request ├ ƒ /request/bad-set ├ ○ /robots.txt ├ ƒ /signup ├ ○ /sitemap.xml ├ ƒ /streaming ├ ƒ /streaming/blocking ├ ○ /studio ├ ○ /styled ├ ƒ /tags/action ├ ƒ /tags/list └ ○ /tags/path ƒ Proxy (Middleware) ○ (Static) prerendered as static content ● (SSG) prerendered as static HTML (uses generateStaticParams) ƒ (Dynamic) server-rendered on demand ok the log shows "Compiled successfully" ok the log shows "Route (app)" ok the log shows "○ (Static)" ok the log shows "ƒ (Dynamic)" ok the log does not show "anonymous telemetry" build id characters: 21 prerendered: / /_global-error /_not-found /about /actions/done /api/static /books /books/dracula /books/middlemarch /books/persuasion /cache/isr /cache/static /clock/static /counter /dashboard /dashboard/settings /env/baked /env/client /env/leak /forms/signup /images /library /meta /moved /old-shelf /pricing /robots.txt /sitemap.xml /studio /styled /tags/path native compiler is a file of more than a megabyte: true ok the build wrote an id of 21 characters ok it prerendered the static pages of lessons 1 to 4 ok and not the route handler ok the compiler is the native binary that came with the vendored tree
The check script ran after the build, with APP_DIR set to the project, and read the files in .next.
The build id is generated per build and is masked on this page, as are the durations and the
number of workers. The log ends with the legend for the symbols, and the checks below it are the
ones this course made about the log and about .next.
next start: serve what was built
next start serves .next and compiles nothing. It listens on all
interfaces unless told otherwise: the command line help in the vendored package says
(default: 0.0.0.0) for -H. The course always passes
-H 127.0.0.1 -p 0, so the server is reachable only from this machine, on a port the
operating system picks. -p 0 works because Next.js prints the address it ended up
with, and that line is how the build learns the port.
app/about/page.js
export default function About() {
return (
<main>
<h1>About</h1>
<p>This page is a file at app/about/page.js.</p>
</main>
);
}
start.mjs
import { readFileSync } from 'node:fs';
const base = process.env.BASE_URL;
const a = await (await fetch(base + '/about')).text();
const res = await fetch(base + '/about');
const b = await res.text();
const help = readFileSync(process.env.APP_DIR + '/node_modules/next/dist/bin/next', 'utf8');
console.log(res.status, 'cache-control:', res.headers.get('cache-control'));
console.log('the same page twice:', a === b);
check('a static page is the same on every request', a === b, true);
check('and is served with a long shared-cache lifetime', res.headers.get('cache-control'), 's-maxage=31536000');
check('the hostname flag defaults to every interface', help.includes('Specify a hostname on which to start the application (default: 0.0.0.0).'), true);
What the driver printed (Next 16.3.1, next build + next start), when the build ran it
node start.mjs
200 cache-control: s-maxage=31536000 the same page twice: true ok a static page is the same on every request ok and is served with a long shared-cache lifetime ok the hostname flag defaults to every interface
What next start printed meanwhile
▲ Next.js 16.3.1 - Local: http://127.0.0.1:<port> - Network: http://127.0.0.1:<port> ✓ Ready in <t> ✓ Running next.config.mjs took <t>
The driver ran against next start -H 127.0.0.1 -p 0 of project app, right after its next build; BASE_URL is the address Next printed.
How this course keeps Next.js off the network
Next.js has a few code paths that can reach the internet: anonymous telemetry, a version check the development server makes when a browser's hot-reload socket connects, downloading a compiler when the native one will not load, and fetching Google Fonts or remote images when a project asks for them. None of them may run here, so three layers stand in the way.
- The vendored tree contains the native compiler (the
swcpackage for this machine), so nothing needs downloading. The build refuses to run unless every file matches the hashes intools/vendor.json. - Every process that runs Next.js has
NEXT_TELEMETRY_DISABLED=1, andnpmis pointed at an address nothing listens on. The build panel above checks that the log has no telemetry notice. - Every Next.js process runs under
sandbox-execwith the profile below. It allows loopback and, for anything else, kills the process withSIGKILL. A Next.js process that tried to reach the internet would not print an error; it would die, and the build would stop with a message saying so. Before any panel runs, a canary proves the profile kills: the contents page shows network canaries: 4 of 4 blocked. The wall stops connections. It does not stop name lookups: on macOS the system resolver answers those from outside the sandbox, so a lookup alone would not kill a process. That is likely rather than tested, because this course makes no lookup to see.
(version 1)
(allow default)
(deny network-outbound (with send-signal SIGKILL))
(allow network-outbound (remote ip "localhost:*"))
(deny network-bind)
(allow network-bind (local ip "localhost:*"))
this is the profile every Next.js process in this course runs under. sandbox-exec is deprecated by Apple but works on this machine, and the build refuses to run without it.
Every panel here finished, and none was killed, so none made a non-loopback connection. That
covers the compile (the native compiler), the telemetry setting, the build, and a running
next start and next dev. Lessons 24 and 25 add images and local fonts to
the same proof. The driver scripts and the data server run under the stricter
tools/sandbox.sb, which refuses instead of killing.