By the end you'll know
- How the pieces from Lessons 1 to 10 fit into one Next.js site, and where each lives
- What happens, step by step, when a signed-in customer opens their orders page
- Where every setting and secret lives, locally and on Vercel
- How to write one release check that runs the track's tests together
- What kinds of mistakes those checks catch, with two real examples
Before you start
- You need: Lessons 1 to 10 of this track. This lesson builds nothing new; it puts their parts together. Node.js 20 or later, Python 3, and Google Chrome.
- Versions: Next.js 16.4.5, Supabase CLI 3.0.0,
@supabase/ssr0.12.7, Playwright 1.63.0, axe-core 4.14.0 and Lighthouse 13.6.0, November 2026. - The example: the finished Juniper & Rye site, a made-up bakery, with Ada and Ben, its made-up customers. Every run and line of output below is real.
The short version
- A real site is a few simple parts with clear jobs: static pages, a page that refreshes from an API, a login, and a database that decides who sees what.
- Every setting has one home. Public values can be
NEXT_PUBLIC_; secrets are read by the server from its environment, and never appear in the build. - One script runs every test the track wrote. In 1 minute 45 seconds, it checked the pages, prices, logins, data privacy, accessibility, speed and secrets. All passed.
- The checks earn their keep on mistakes. A new table without row level security, and a form field that lost its label, each failed the check immediately.
The map
The practice folder, lesson-11, has the finished site in site/, plus the supplier API, the database migration and the tests. The parts of the site, and where each came from:
Path in site/ | What it does | Lesson |
|---|---|---|
app/layout.js, app/globals.css | Every page's frame: header, nav, a skip link, main, footer; one stylesheet | 2, 4, 10 |
app/page.js | Home: the hero image and the latest news | 4, 9 |
content/news/*.mdx, lib/news.js, app/news/ | News posts as files, drafts hidden in production | 5 |
app/components/mdx.js | Components posts can use, including a live Price | 5, 6 |
lib/bakehouse.js, app/menu/ | Prices from the supplier's API, with a timeout and checks; an error page | 6 |
proxy.js, lib/supabase/ | Login cookies, refreshed and checked | 7 |
app/login/ | Sign in and out, with server actions | 7, 10 |
app/orders/ | Your pre-orders, and an accessible form to add one | 7, 10 |
vercel.json | Tells Vercel this is a Next.js project | 8 |
Outside site/: supabase/migrations/ holds the pre-orders table with row level security and its two policies, written in one migration this time; bakehouse_api.py is Lesson 6's made-up supplier; tests/ has the checks.
A few things changed from the earlier lessons, because the parts now meet:
- The menu comes from the API, not a file. Lesson 4's
data/menu.jsis gone. - The
Pricein news posts is live: an async server component that asks the supplier. That had a side effect, below. - The pre-order form's items come from the supplier, and the server checks every submitted item against the same list, so a tampered form can't order something that doesn't exist.
Running it all
(Your machine)
cd lesson-11
npm install
SUPABASE_EXPERIMENTAL_STACK=1 npx supabase init
npx supabase start -x realtime,storage,functions,studio,mail,analytics,pooler
python3 bakehouse_api.py &
node seed.mjs
chmod +x make-env.sh check-all.sh tests/security.sh
./make-env.sh
cd site
npm install
npm run build
npm start
npm installin the lesson folder installs the CLI and test tools; insite/, the site's packages.supabase initandsupabase startset up the local database and Auth, as in Lesson 7.startapplies the migration insupabase/migrations/itself.python3 bakehouse_api.py &starts the supplier API in the background. (&runs it without holding the terminal; stop it later withkill %1.)node seed.mjssigns up Ada and Ben with one pre-order each.chmod +xmakes the scripts runnable;./make-env.shwritessite/.env.local.npm run buildandnpm startserve the site on port 3011. Leave it running.
The build's route table shows every kind of page from the track in one place:
Route (app) Revalidate Expire
┌ ○ /
├ ○ /_not-found
├ ƒ /login
├ ○ /menu 1m 1y
├ ○ /news
├ /news/[slug]
│ ├ ● /news/rye-week 1m 1y
│ └ ● /news/new-oven 1m 1y
└ ƒ /orders
ƒ Proxy (Middleware)
○ /and○ /news: static, built once.○ /menuwith1m: static, rebuilt in the background at most once a minute (Lesson 6).● /news/rye-weekand/news/new-oven: one page per post, built at build time. The draft,holiday-orders, isn't listed: drafts are left out of production builds (Lesson 5).ƒ /loginandƒ /orders: per request, because they read cookies (Lesson 7).
The surprise is the 1m on the news posts. Nobody asked for posts to refresh. But both posts use <Price>, which fetches with revalidate: 60, and a page refreshes as often as the most frequent fetch inside it. That's usually what you want, since the price in a post stays current, but it's a reminder to read the route table after every change.
One request, start to finish
Ada is signed in and opens /orders. Here's everything that happens, and which lesson it came from:
- Her browser sends
GET /orderswith her login cookie (Lessons 1 and 7). proxy.jsruns first. It reads the cookie, refreshes the token if it's close to expiring, and checks it withgetClaims(). A missing or forged token would be redirected to/loginhere (Lesson 7).- The orders page, a server component, checks again with
getClaims(). Both checks are cheap, and a page shouldn't rely on the proxy alone. - Two requests go out at once, with
Promise.all: to Supabase for pre-orders, carrying Ada's token, and to the supplier for today's bakes, carrying the supplier's key (Lesson 6). - Postgres applies row level security. The query has no
where user_id = ...; the policy returns only rows whoseuser_idmatches the token's user (Lesson 7). - The server renders HTML with her orders and the form, and sends it (Lesson 4).
- The form is a client component. Its JavaScript makes the Reserve button submit through a server action and show the result, in a
role="status"orrole="alert"that screen readers announce (Lessons 3 and 10). - When she reserves, the server action checks the item against the supplier's list and the date's format, then inserts with her token. RLS's
with checkmakes sure it's saved in her name (Lessons 6 and 7).
Notice what the browser is trusted with: nothing. The cookie is checked twice, the form's contents are checked once more on the server, and the database makes the final decision about whose rows are whose.
Where the settings live
| Setting | Who may see it | Locally | On Vercel |
|---|---|---|---|
NEXT_PUBLIC_SUPABASE_URL | Anyone | site/.env.local | Production: the real project; Preview: a test project |
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY | Anyone; limited by RLS | site/.env.local | Same split |
BAKEHOUSE_URL | The server | site/.env.local | Production and Preview |
BAKEHOUSE_API_KEY | The server only | site/.env.local | A secret; a test key for Preview |
| Supabase secret key | Not used by this site | Kept out entirely: nothing here needs to bypass RLS | |
site/.env.local is in .gitignore, so none of these are committed. The security check below confirms the supplier key isn't in the build at all: the server reads it from the environment each time it runs.
The release check
check-all.sh runs everything the track tested, against the running site. It resets the local database and re-seeds Ada and Ben first, so every run starts from the same data:
(Your machine)
cd lesson-11
./check-all.sh
== End to end
PASS / loads (HTTP 200): "Sourdough, rye and pastries, baked every morning"
PASS /menu loads (HTTP 200): "Today's bakes"
PASS /news loads (HTTP 200): "News from the bakery"
PASS /news/rye-week loads (HTTP 200): "Rye week is back"
PASS menu lists the supplier's 3 bakes
PASS news post shows dark rye's live price, $9.00
PASS draft post is hidden in production (HTTP 404)
PASS signed out, /orders sends you to /login
PASS wrong password shows an announced error
PASS Ada sees only her own pre-order: Dark rye 2026-11-14
PASS Ada reserves a country sourdough; she now has 2
PASS a made-up item is refused by the server: "Choose one of today's bakes."
PASS Ben sees only his: Cardamom bun 2026-11-14
all end-to-end checks passed
== Accessibility
PASS /: 0 WCAG problems
PASS /menu: 0 WCAG problems
PASS /news: 0 WCAG problems
PASS /news/rye-week: 0 WCAG problems
PASS /login: 0 WCAG problems
PASS /orders: 0 WCAG problems
== Speed budget
PASS /: score 97, LCP 1.3 s, CLS 0.000, TBT 194 ms
PASS /menu: score 95, LCP 1.3 s, CLS 0.000, TBT 245 ms
PASS /news/rye-week: score 96, LCP 1.3 s, CLS 0.000, TBT 217 ms
== Security
PASS no braces MDX would swallow in news posts
PASS no supplier key or Supabase secret key in browser files
info the supplier key is in 0 build file(s) in all: the server reads it from the environment when it runs
PASS Supabase advisor: no security errors
== Summary
PASS End to end
PASS Accessibility
PASS Speed budget
PASS Security
It took 1 minute 45 seconds. What each part does:
- End to end (
tests/e2e.cjs) uses the site in a real browser with Playwright, as a visitor, as Ada and as Ben. The most important lines are the last three: Ada and Ben each see only their own orders, and the server refuses a tampered form. To tamper, the test adds a fake option to the form's dropdown in the page, the way someone could in developer tools:
await ada.locator("#item").evaluate((sel) => sel.add(new Option("50 croissants", "50 croissants", true, true)));
- Accessibility (
tests/a11y.cjs) runs Lesson 10's axe scan on every page, including/orderssigned in as Ada, where a scan of the public pages would never reach. - Speed budget (
tests/speed.mjs) runs Lesson 9's Lighthouse check as a phone on slow 4G, and fails a page if LCP is over 2.5 seconds, CLS over 0.1 or TBT over 300 ms. Like any lab test, its numbers move a little between runs, which is why the limits sit at "good", not at today's numbers. - Security (
tests/security.sh) runs Lesson 5's brace scanner on the posts, searches the browser files for either secret, and runs Lesson 7's Supabase advisor.
Each test exits with an error code when it fails, and check-all.sh does too, so the same script can stop a deploy.
Catching mistakes
A check that always passes proves little. Two realistic mistakes:
A table without row level security. A developer adds customer reviews in a hurry. The migration in mistakes/reviews.sql creates the table and forgets RLS. Applied, then checked:
(Your machine)
cp mistakes/reviews.sql supabase/migrations/20261108000002_reviews.sql
npx supabase migration up
tests/security.sh
PASS no braces MDX would swallow in news posts
PASS no supplier key or Supabase secret key in browser files
info the supplier key is in 0 build file(s) in all: the server reads it from the environment when it runs
FAIL Supabase advisor found security errors
The advisor said why:
"detail": "Table `public.reviews` is public, but RLS has not been enabled."
Lesson 7 showed what that means: anyone with the page's public key could read, change and delete every review. Caught before it shipped. (Remove the file and run npx supabase db reset to undo it.)
A form field that lost its label. During a redesign, someone replaces the pickup date's <label> with a <p>, because it was easier to style. It looks the same. After a rebuild, the accessibility check:
PASS /: 0 WCAG problems
PASS /menu: 0 WCAG problems
PASS /news: 0 WCAG problems
PASS /news/rye-week: 0 WCAG problems
PASS /login: 0 WCAG problems
FAIL /orders: 1 WCAG problems label
Only on /orders, the page you have to sign in to see. A scan of the public pages would have passed.
Putting it on the internet
Lesson 8's steps apply unchanged, with two additions:
- A hosted Supabase project instead of the local one. Run the migration against it (
npx supabase link, thennpx supabase db push --remote: in CLI 3, database commands target the local stack unless you add--remote), and set its URL and publishable key in Vercel. Give previews a separate test project, so testing never touches real customers. - The supplier's real URL and key as Vercel environment variables, the key as a secret.
Then the release check can point at a preview deployment before you promote it. That last step wasn't run for this lesson; everything above ran locally.
In real systems
- Run the release check in CI on every pull request. GitHub Actions can start a local Supabase stack, build the site and run
check-all.sh; a failing check blocks the merge. That's the habit that makes all the other lessons stick. - Watch production, too. Checks before release don't see what real visitors do. Collect errors (Vercel's logs, or a tool like Sentry), field speed data (Lesson 9) and uptime. The Monitoring track covers alerts.
- Back up the database, and test a restore. Supabase's paid plans include backups; whatever you use, a backup you've never restored is a guess.
- Keep dependencies current. Every version in this track is pinned. Tools like Dependabot or Renovate open pull requests for updates, and the release check tells you if one breaks something.
- Write down how to roll back, with Lesson 8's promote step included, before you need it.
Spot the problem
Work out what's wrong before you read the explanation.
"All the tests pass, and customers can see each other's orders." A team's site has end-to-end tests that sign in as a test customer, add an order, and check it appears. Every test passes on every release. A customer reports seeing a stranger's order. What's wrong? The tests only ever used one customer. A test that signs in as one person and sees their own order passes whether or not anyone else's orders are visible too. This lesson's tests sign in as Ada and as Ben, and check each sees exactly their own: one row each, and the right one. The fix: test with at least two users, and assert what each must not see, not just what they should; then check row level security on the table, which is probably missing or too broad. Then check: does signing in as the second test customer show only their rows?
Keep this
| Before every release | How | Lesson |
|---|---|---|
| Read the route table | npm run build: ○, ●, ƒ and Revalidate as intended | 4, 6 |
| Pages work, as visitors and as each kind of user | End-to-end tests with two customers | 7, 11 |
| Every page is accessible, signed in too | axe on every page; keyboard and screen reader by hand | 10 |
| Fast on a phone | Lighthouse with a budget | 9 |
| No secrets in the browser; RLS on every table | Search the build; Supabase advisor | 6, 7 |
| Safe to release, and to undo | Preview first; know rollback and promote | 8 |
Where you are now
That's the track. You started with what a browser does with 661 bytes of HTML, and you've built a site with static and live pages, content from files, data from an API, logins, private data per customer, and checks for speed, accessibility and security, and you know how to deploy it and roll it back. More than any one technique, the habit to keep is the one every lesson used: change one thing, measure what really happened, and trust the measurement over the documentation, the tool's guess or your own expectation.
Check yourself
Why did the news posts get a 1m Revalidate in the route table?
When Ada opens /orders, what decides which rows she gets?
Where is the supplier's API key in the built site?
Why does the end-to-end test sign in as both Ada and Ben?
Which check caught the reviews table without row level security?
The pickup date lost its label. Why did only /orders fail?
Key terms
- Release check: a script that runs every test that matters before a release, and fails if any does.
- End-to-end test: a test that uses the whole running site in a real browser, as a person would.
- Test data (seeding): known accounts and records created before tests, so every run starts the same.
- Budget: a limit, like LCP under 2.5 seconds, that fails the check when it's crossed.
- CI (continuous integration): running the build and checks automatically on every change, usually on every pull request.
- Environment: where a copy of the site runs (local, preview, production), each with its own settings.
Go deeper
- Supabase: local development and CI (opens in a new tab) and managing environments (opens in a new tab).
- Playwright: writing tests (opens in a new tab), with its own test runner, reports and retries.
- GitHub: GitHub Actions for Node.js (opens in a new tab).
- Try: add an update policy so customers can change a pickup date (Lesson 7's first Try), then add an end-to-end check that Ada can move her own order and can't move Ben's.
- Try: change the supplier to
downmode (Lesson 6) and run./check-all.sh. Which checks fail, and which pages keep working?