Welcome

--:-- recording — let's go

Week 7

Full stack

what changed this week?

Jev (TypeSafe AI, 15 September): a general model that returns a typed value rather than text

Claude Opus 5.5 (Anthropic, 22 September): claims Fable 5.1 performance on most tasks, at 40% less than Opus 5

GPT-6 Sol and Luna (OpenAI, 22 September): cheaper tiers of GPT-6 Astra, at half the API price

today’s beats

where the state lives now there’s a server

journey of a form: browser → server → SQLite and back

secrets, and deploying to Fly

static ends here

  • A2 shipped Monday — crit retros this week
  • the course switches to full-stack from c7: your pages now come from a server
  • the final project spec lands soon (and will be full-stack)

the code on today’s slides comes from two repos: the static template you’ve shipped from since week 1, and the C7 starter

Astro is the default, not compulsory

the page you’ve been shipping

<!-- index.html, the static template -->
<main>
  <h1>COMP4020 prototype</h1>
  <p>Replace this with your prototype…</p>
</main>
<script type="module" src="./main.ts"></script>

pnpm build writes it into dist/, and GitHub Pages serves that folder as-is

every visitor gets the same bytes, and main.ts runs in their browser

the full stack

browser    the HTML index.astro renders
   │       HTTP
server     api/messages.ts, lib/db.ts
   │       better-sqlite3
database   .data/app.db here, /data/app.db on Fly*

static: only the browser runs your code, from files built in CI

full-stack: a Node* server renders each page per request, and reads SQLite* to do it

(“stack” is loose: three places code and data sit, joined by HTTP and a database driver)

demo: what should the guestbook do next, and where would that state live?

then we leave it running and talk about where state lives

server-side state

state locations

week 4: the URL, the DOM and a store, all in one browser tab, plus localStorage if it has to survive a reload

from today two more: the server (e.g. Node) process’s memory and the database

each piece of state needs a single source of truth: the copy you treat as correct when two differ

the app on every slide today

the starter’s guestbook, live at lecture-demos.fly.dev: one text box, a Sign button, a list of messages

<input id="body" name="body" required maxlength="500" />

HTML: only name="body" reaches the server; id="body" is for the <label>

body: the message you typed, under the same name in all four files it passes through

TypeScript on these slides is server code, except inside a <script>

what survives?

statereloadrestartredeploy
the URL
the DOM
a JS store
localStorage
server memory
the database

the guestbook uses two: the DOM and the messages table

what survives?

statereloadrestartredeploy
the URLkept
the DOMgone
a JS storegone
localStoragekept
server memorykept
the databasekept

a reload rebuilds the tab from scratch; the URL and localStorage stay in the browser

what survives?

statereloadrestartredeploy
the URLkeptkept
the DOMgonekept
a JS storegonekept
localStoragekeptkept
server memorykeptgone
the databasekeptkept

a server restart (a crash, an auto-stop) empties its memory; the open tab keeps its DOM

what survives?

statereloadrestartredeploy
the URLkeptkeptkept
the DOMgonekeptkept
a JS storegonekeptkept
localStoragekeptkeptkept
server memorykeptgonegone
the databasekeptkeptkept

only the database survives a redeploy: it’s the source of truth, and the <li>s are a copy rebuilt on every reload

the agent reads the repo, not the data

# in the starter, with pnpm dev running
$ curl -si localhost:4321/api/messages \
    -H 'origin: http://localhost:4321' -d "body=g'day"
HTTP/1.1 303 See Other
$ sqlite3 .data/app.db 'select id, body from messages'
1|g'day

.data/ is gitignored and production’s database is on a Fly volume: the agent reads neither

“did it save?”: the agent can query the running app and the table (week 5’s agentic manual testing)

what’s different in production?

a new machine every deploy: only the volume at /data survives

auto-stop: the first request after a quiet spell is held until the machine starts

no dev server: the Node adapter in the Dockerfile runs the app, with no hot reload and no error overlay

Ben uses… “where does this app keep its state?”

list every place this app keeps state (browser, server memory, database) and say which survives a reload, a restart and a redeploy. Don’t change anything yet

cheap, and checkable: I can reload, restart and redeploy to test each answer

Web

which box does what?

forms, in the running app

<form method="post" action="/api/messages">
  <label for="body">Leave a message</label>
  <input id="body" name="body" required maxlength="500" />
  <button>Sign</button>
</form>

method and action: the browser does the POST — no JavaScript here at all

name: the only part of this markup sent to the server (body=g'day), and required runs only in the browser, so it isn’t validation — anything can post here

the HTTP request

POST /api/messages HTTP/1.1
Host: localhost:4321
Origin: http://localhost:4321
Content-Type: application/x-www-form-urlencoded

body=g%27day

everything the browser sends the server when you press Sign: some headers, a blank line, then name=value pairs, percent-encoded

the handler’s form.get("body") decodes that last line back to g'day

one request, end to end

One press of the Sign button A sequence diagram with three lifelines: browser, server and database. The browser POSTs body=g'day to /api/messages. The handler in api/messages.ts calls addMessage, which inserts a row into the messages table, and the row comes back. The server answers 303 See Other with Location /. The browser follows it with GET /, index.astro calls listMessages, which selects the latest 50 rows, and the server returns 200 with the page, g'day in its list. browser server database POST /api/messages body=g%27day, from index.astro's form INSERT INTO messages api/messages.ts → addMessage() 303 See Other Location: / GET / the browser follows the redirect SELECT … LIMIT 50 index.astro → listMessages() 200 OK the page, with g'day in the list

a rename on one side

- <input id="body" name="body" required maxlength="500" />
+ <input id="body" name="message" required maxlength="500" />

index.astro is fine. the handler, untouched, still reads form.get("body"), gets "", and its if (body) guard skips the insert

303 (see other: go GET this URL), then 200 (OK), then an unchanged list: no exception, no row, and no sign the message was dropped

the rename, on both sides

  <!-- src/pages/index.astro -->
- <input id="body" name="body" required maxlength="500" />
+ <input id="body" name="message" required maxlength="500" />

  // src/pages/api/messages.ts
- String(form.get("body") ?? "").trim();
+ String(form.get("message") ?? "").trim();

the field name is the contract between the form and the handler, and nothing in TypeScript checks it

check coverage

✓ accepts a message and redirects back to the page
✓ persists the message: a fresh page load includes it
✓ broadcasts new messages over the SSE stream

spec/guestbook.test.ts posts body=… straight to /api/messages, without loading the form

rename the handler’s field and the last two fail; rename only the form’s and all three pass

finding the form-only rename takes a browser: you, or an agent driving one

routing

pages/index.astro       GET   /
pages/readme.astro      GET   /readme/
pages/api/messages.ts   POST  /api/messages    export const POST

the file path is the URL; the exported name is the method

nothing registers a route — output: "server" in astro.config.ts is what puts a Node server behind them instead of building HTML files (more on server-side routing)

the same request, seen from the browser

POST  /api/messages   303   body=g'day
GET   /               200   the list, re-rendered from SQLite
GET   /api/events     200   text/event-stream — (pending)

two requests for one button press: Post/Redirect/Get, and the 303 is what lets the form work with no JavaScript

the third one never finishes: it’s the live stream that pushes new messages to the other tabs (server-sent events)

server-side secrets

---
// runs on the server, per request: process.env is safe here
const messages = listMessages();
---
<!-- rendered on the server, but this HTML is sent to the browser -->
<ul id="messages">
  {messages.map((message) => <li>{message.body}</li>)}
</ul>
<script>
  // bundled and sent to the browser: anyone can read it
  const source = new EventSource("/api/events");
</script>

your app on Fly, in one picture

the contract: Dockerfile, fly.toml and the CI workflow are fixed, so swapping the stack rewrites only the Dockerfile and the check entrypoints

one app per repo: <repo>.fly.dev, one small machine that auto-stops, the database on a 1GB volume at /data

who deploys: you, by hand (flyctl deploy), while the repo is private; CI on every push to main once /ship flips it public

topics/hosting-access is the whole story

yes, and…

how does it get pushed further? weird ideas welcome

C7: an ANU system

build the full-stack replacement for the ANU system that reliably ruins your week — enrolment, timetabling, room booking, whichever one it is

model the slice that annoys you, wired end to end: the core flow has to survive a reload

live at its *.fly.dev URL by the cutoff

deadline radar

Monday/Wednesday, week 8: C7 live at its *.fly.dev URL two hours before your session, with reflections/crit-7.md in the repo

this week: your Fly token, pasted into mise.local.toml, and one deploy by hand — before the repo is public, CI can’t do it for you

tl;dr

a server adds two places for state to live, its memory and the database, and the database is the source of truth

the agent reads the repo, not the data: check a change against the running app and the table

deploy to Fly this week: production differs from pnpm dev in ways you only see after a deploy

ask your agent

ask until you can explain these back:

  • where state lives: every place this app keeps state, and which of it survives a reload, a restart and a redeploy
  • routing: which file serves /api/messages, and what makes it a POST?
  • the deploy contract: what in this repo would I rewrite to move off Astro?
  • secrets: which of my code runs on the server, and which ships to the browser?

See you in the studio