A live chat room for one lecture, where students send the lecturer feedback
and links. It is also the worked example in a lecture on spec-driven
development. See DOMAIN.md and PROJECT.md.
The app is one Python server that also serves the browser client. It keeps
every message and the list of who is online in memory, with no database, and
runs as one container on Google Cloud Run. make help lists every command.
You need uv, Node 22, GNU Make, and, to deploy, the Google Cloud CLI
(gcloud) signed in to an account that may administer the mstc-chat
project.
uv sync # Python tools
npm --prefix frontend ci --ignore-scripts # TypeScript tools, no install scripts
pre-commit install # commit, commit-message and pre-push hooks
cp .env.example .env # local settings; git never tracks .env- Fill in
.env. Choose any local class password. Generate the session secret without printing it:printf 'SESSION_SECRET=%s\n' "$(openssl rand -base64 48)" >> .env(delete the emptySESSION_SECRET=line above it). The server refuses to start if either secret is missing or the session secret is under 32 characters. - Run
make dev. It builds the client intofrontend/dist/and starts the server on http://localhost:8080. - Open http://localhost:8080 in Chrome or Firefox. Safari refuses the
session cookie on
http://localhost, because the cookie is markedSecure, so sign-in does not stick there.
To try the container itself: docker build -t mstc-chat . and
docker run --rm -p 8080:8080 --env-file .env mstc-chat.
The app has two secrets. Cloud Run reads both from Secret Manager; they never live in the repository, the image, CI or the service's settings.
| Setting | Secret Manager secret | What it is |
|---|---|---|
CLASS_PASSWORD |
class-password |
The one password every student types. |
SESSION_SECRET |
session-secret |
The key that signs session cookies. |
Rules:
- Never write the class password in this repository, in a commit, an issue or a pull request, not even in an example. The repository is public, and no secret scanner can recognise a password made of plain words.
- Choose a class password of at least 12 characters, such as three random words. Wrong guesses each wait 1 second, but nothing locks anyone out.
- Enter secrets only through the commands below. They read the value at a
hidden prompt, or generate it unseen, and pass it to
gcloudon standard input, so it never appears on a command line, in shell history or on a projected screen.
make class-password # type the class password at the hidden prompt
make session-secret # generate a new session secret; nobody sees itEach command adds a new version of the secret. The running service keeps the
version it was deployed with until the next make deploy. A new session
secret signs everyone out, which is also the only way to end every session
early.
gcloud auth login
make setup
make class-passwordmake setup is safe to run again: it creates only what is missing and never
replaces a secret value. It:
- turns on the Cloud Run, Cloud Build, Artifact Registry and Secret Manager APIs;
- creates the runtime account
mstc-chat-run@mstc-chat.iam.gserviceaccount.comand gives it Secret Accessor on the two secrets only, so the server can read nothing else; - creates the build account
mstc-chat-build@mstc-chat.iam.gserviceaccount.comand gives it only Cloud Run Builder (roles/run.builder), never Editor, so dependency code running during a build cannot change the project; - creates the two secrets and the
cloud-run-source-deployimage repository inus-central1, and generates a session secret if none exists.
Set a US$5 budget alert once, in the Cloud Console under Billing, Budgets and
alerts, for the mstc-chat project. A lecture costs a few cents; an alert
means something is misconfigured or abused.
make deploymake deploy uploads this folder (minus what .gcloudignore excludes), builds
the image in Cloud Build under the build account, and deploys the service
mstc-chat in us-central1. It sets every setting on the command line, so a
deploy never inherits a stale one:
| Cloud Run setting | Value | Why |
|---|---|---|
| Maximum instances (service level) | 1 | All messages live in one process's memory; a second instance would split the room. |
| Concurrency | 250 | About 100 open streams plus their posts must fit in one instance. |
| Request timeout | 60 s | The server ends each stream after 20 s anyway. |
| CPU, memory | 1 vCPU, 512 MiB | Enough for one class; the smallest that fits. |
| Start-up CPU boost | off | Keeps the cost near zero. |
| Access | public, invoker IAM check off (--no-invoker-iam-check) |
Students reach the login page without a Google account. The heavychain.org organization policy (domain-restricted sharing) refuses the allUsers binding that --allow-unauthenticated would add; turning the check off needs no binding. |
| Runtime account | mstc-chat-run |
Reads the two secrets and nothing else. |
| Build account | mstc-chat-build |
Holds only Cloud Run Builder. |
| Secrets | CLASS_PASSWORD=class-password:<n>, SESSION_SECRET=session-secret:<n> |
References pinned to the newest enabled version numbers, looked up at deploy time; the settings show names and numbers, never values. |
| Minimum instances | unchanged | Only make warm-up and make cool-down change it. |
When it finishes, it prints the address, the commit, any uncommitted changes (a source deploy uploads those too) and the UTC time. Check that the commit is the one you meant to ship.
A redeploy wipes the room. The new version starts with no messages. Every open page reconnects to it within about 30 seconds, shows "The chat restarted", and reloads. Rolling back empties the room the same way. Deploy before the lecture, not during it, unless the redeploy is the demo.
make rollback # lists the revisions
make rollback REVISION=mstc-chat-00007-abc # sends all traffic to that revisionRolling back builds nothing and empties the room. make deploy later sends
traffic to the new revision again.
Before the lecture:
make warm-up. It sets the service's minimum to 1 instance, so no student waits for a cold start. It creates no revision, so nothing restarts.- Confirm in its output that
run.googleapis.com/minScalereads'1',run.googleapis.com/maxScalereads'1', andlatestReadyRevisionNameis the revision you deployed. - Open the address in your browser, sign in, and post one message.
- Optional rehearsal:
make rehearsesigns in 50 simulated students named "Rehearsal 01" and up, posts a probe every 2 seconds for 60 seconds, and prints the slowest delivery. It passes when every student got every probe within 2 seconds and nothing was refused. SetPARTICIPANTS,DURATIONorURLto change it, as inmake rehearse PARTICIPANTS=10. Run it well before class: its messages stay in the room until the next restart.
After the lecture:
make cool-down. It sets the minimum back to 0, so the idle service costs nothing. Open tabs keep the instance running, and billed, until they close.- Confirm that
run.googleapis.com/minScalereads'0'or is gone.
The app never logs names, message text, passwords or cookies. Cloud Run itself logs every request for 30 days: the URL (with its query, which holds only a random tab id), the method, status, latency, the client's IP address and browser name. It logs no body and no cookie. To stop keeping this service's request logs:
gcloud logging sinks update _Default --project=mstc-chat \
--add-exclusion='name=mstc-chat-requests,filter=resource.type="cloud_run_revision" AND resource.labels.service_name="mstc-chat" AND log_id("run.googleapis.com/requests")'.github/workflows/ci.yml runs on every push to main and every pull request,
with read-only access and no secrets. It runs Ruff, mypy, pytest with at least
80% coverage, tsc, ESLint, Prettier, the frontend tests, pip-audit and npm
audit; scans the whole git history for secrets with Gitleaks, after proving on
a throwaway repository that the scan fails on a deleted token; and builds the
image without pushing it. CI never deploys.
These are repository settings, which only an admin can change:
- Settings, Code security: turn on Secret scanning and Push protection. Both are free for public repositories.
- Settings, Branches (or Rules, Rulesets): protect
main. Require a pull request, require the status checkschecks,secret-scanandimageto pass, and block force pushes. - Settings, Actions, General: allow only actions pinned to a full commit SHA, and keep "Require approval for first-time contributors".
Delete the service, its images, the secrets and the accounts:
gcloud run services delete mstc-chat --region=us-central1 --project=mstc-chat
gcloud artifacts repositories delete cloud-run-source-deploy --location=us-central1 --project=mstc-chat
gcloud secrets delete class-password --project=mstc-chat
gcloud secrets delete session-secret --project=mstc-chat
gcloud iam service-accounts delete mstc-chat-run@mstc-chat.iam.gserviceaccount.com --project=mstc-chat
gcloud iam service-accounts delete mstc-chat-build@mstc-chat.iam.gserviceaccount.com --project=mstc-chatOr shut down the whole project: gcloud projects delete mstc-chat.
- Logo. The McCombs logo comes unchanged from McCombs' official logo
downloads on UT's Box service (formal logo:
https://utexas.box.com/s/b88qfe69z1mhr4d441yj28k1gwo7c402). The person who
runs this course holds permission to use the McCombs mark for this class
tool through their service on the MSTC Advisory Council. When no official
file is in
frontend/public/brand/, the login page shows the text wordmark "MSTC Chat" instead. UT owns all rights in its marks. - Fonts. Libre Franklin and Charis SIL come from the Fontsource npm
packages and are served by the app, so students' browsers contact no third
party. Both are under the SIL Open Font License 1.1; the licence texts are
served with the app from
frontend/public/fonts/.