# Week 7: Full stack

> A2 shipped Monday, and the course goes full-stack. Once there’s a server, state has two more places to live; find them, check changes against the running app, and deploy it to Fly

# Welcome

---

# Week 7

## Full stack

---

(qr: <https://courses.slop.university>)

---

## what changed this week?

[Jev](https://typesafe.ai/blog/introducing-system-one-models-and-jev) (TypeSafe
AI, 15 September): a general model that returns a **typed value** rather than
text

[Claude Opus 5.5](https://www.anthropic.com/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](https://openai.com/index/introducing-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](https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Server-side/First_steps/Introduction)
- the final project spec lands soon (and will be full-stack)

the code on today's slides comes from two repos: the
[static template](https://github.com/comp4020-agentic-coding-studio/template-static)
you've shipped from since week 1, and the
[C7 starter](https://github.com/comp4020-agentic-coding-studio/template-dynamic)

---

Astro is the default, not **compulsory**

---

## the page you've been shipping

```html
<!-- 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

```text
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](https://sqlite.org/)\* to do it

("stack" is loose: three places code and data sit, joined by
[HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Overview) and a
database driver)

---

(qr: <https://lecture-demos.fly.dev>)

---

## 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`](https://developer.mozilla.org/en-US/docs/Web/API/Window/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](https://en.wikipedia.org/wiki/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](https://lecture-demos.fly.dev): one text box, a Sign
button, a list of messages

```html
<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?

| state          | reload | restart | redeploy |
| -------------- | ------ | ------- | -------- |
| the URL        |        |         |          |
| the DOM        |        |         |          |
| a JS store     |        |         |          |
| `localStorage` |        |         |          |
| server memory  |        |         |          |
| the database   |        |         |          |

the guestbook uses two: the DOM and the `messages` table

---

## what survives?

| state          | reload   | restart | redeploy |
| -------------- | -------- | ------- | -------- |
| the URL        | kept     |         |          |
| the DOM        | **gone** |         |          |
| a JS store     | **gone** |         |          |
| `localStorage` | kept     |         |          |
| server memory  | kept     |         |          |
| the database   | kept     |         |          |

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

---

## what survives?

| state          | reload   | restart  | redeploy |
| -------------- | -------- | -------- | -------- |
| the URL        | kept     | kept     |          |
| the DOM        | **gone** | kept     |          |
| a JS store     | **gone** | kept     |          |
| `localStorage` | kept     | kept     |          |
| server memory  | kept     | **gone** |          |
| the database   | kept     | kept     |          |

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

---

## what survives?

| state          | reload   | restart  | redeploy |
| -------------- | -------- | -------- | -------- |
| the URL        | kept     | kept     | kept     |
| the DOM        | **gone** | kept     | kept     |
| a JS store     | **gone** | kept     | kept     |
| `localStorage` | kept     | kept     | kept     |
| server memory  | kept     | **gone** | **gone** |
| the database   | kept     | kept     | kept     |

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

```sh
# 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](https://docs.docker.com/reference/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

```html
<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**](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/form#attributes_for_form_submission):
the browser does the
[POST](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods/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

```text
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](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers),
a blank line, then `name=value` pairs,
[percent-encoded](https://developer.mozilla.org/en-US/docs/Glossary/Percent-encoding)

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

---

## one request, end to end

---

## a rename on one side

```diff
- <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`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/303)
(see other: go GET this URL), then
[`200`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/200)
(OK), then an unchanged list: no exception, no row, and no sign the message was
dropped

---

## the rename, on both sides

```diff
  <!-- 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

```text
✓ 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

```text
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](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods)

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](https://comp.anu.edu.au/courses/comp4020-agentic-coding-studio/topics/forms-and-state/#server-side-routing))

---

## the same request, seen from the browser

```text
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](https://en.wikipedia.org/wiki/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](https://comp.anu.edu.au/courses/comp4020-agentic-coding-studio/topics/forms-and-state/#state-on-each-side))

---

## server-side secrets

```astro
---
// 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](https://comp.anu.edu.au/courses/comp4020-agentic-coding-studio/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
