This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in node_modules/next/dist/docs/ (resolved from this file's directory; in monorepos the next package may not be visible from the repo root) before writing any code. Heed deprecation notices.
This block is written and re-added by next dev — verify at node_modules/next/dist/server/lib/generate-agent-files.js. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
The website for tty, a Hack Club YSWS for terminal apps. The whole site is one terminal: Ghostty's VT engine (libghostty-vt, via ghostty-web) compiled to WebAssembly and rendered to a canvas. There is no other UI. Everything a visitor does happens by typing commands at hackclub@ysws ~/tty>.
npm run dev # node server.mjs (Next.js dev + chat WebSocket)
npm run build # next build
npm start # NODE_ENV=production node server.mjs
npx tsc --noEmit # typecheck
npx eslint . # lintThere is no test suite. Before calling a change done, run typecheck, lint and build, then check it in a real browser. For terminal behaviour, drive the page with puppeteer-core and a local Chromium, take screenshots, and read them. For mobile, emulate a touch viewport and send keyboard input the way phones do (keyCode 229 keydown followed by Input.insertText / IME composition over CDP).
Only one next dev can run per project directory. If one is already running, test against it or against a production build started with PORT=<port> NODE_ENV=production node server.mjs.
- Next.js 16 (App Router,
cacheComponents), React 19, Tailwind 4, TypeScript ghostty-web0.4 for the terminal (xterm.js compatible API).scripts/patch-ghostty-web.mjsruns onpostinstall: it copies the engine topublic/ghostty-vt.wasm(gitignored) and patchesghostty-web/distto load it from there instead of a 560 KB base64 string inside the JS bundle.layout.tsxpreloads that file. If the script fails after aghostty-webupgrade, update its pattern.- A custom Node server (
server.mjs) so the chat can use WebSockets (ws) - Postgres (
pg) for RSVPs - Hack Club Auth (OIDC) for login, Slack Web API for channel invites
server.mjs http server: Next.js handler + WebSocket upgrade routing
server/chat.mjs chat WebSocket server (auth, rate limits, broadcast)
server/slack.mjs Slack <-> chat bridge (draft, off unless SLACK_SYNC=on)
src/app/page.tsx the page: background ASCII art + the terminal
src/app/layout.tsx metadata and viewport
src/app/api/auth/* Hack Club Auth login, callback, me, logout
src/app/api/rsvp/route.ts RSVP: record in Postgres, invite to #tty
src/components/GhosttyTerminal.tsx boots ghostty-web, wires input to the shell
src/lib/shell.ts the shell: line editor, history, command dispatch, modes
src/lib/features/*.ts one file per command (auth, chat, prizes, rsvp)
src/lib/soft-keyboard.ts mobile keyboard input that ghostty-web drops
src/lib/ansi.ts SGR colours, OSC 8 links, error/warning helpers
src/lib/ansi-image.ts renders PNGs as truecolor half-block characters
src/lib/session.ts session cookie helpers for route handlers
src/lib/session-token.mjs HMAC session sign/verify, shared by Next and server/
src/lib/chat-shared.mjs sanitize + limits, shared by Next and server/
src/lib/rsvp-store.ts Postgres access for RSVPs
src/lib/hca.ts, slack.ts Hack Club Auth and Slack API clients
src/lib/terminal-theme.ts Mellow theme, font family and size
scripts/patch-ghostty-web.mjs postinstall: serve the wasm engine as a file
public/ascii/ascii-art.txt the background art
public/prizes/ prize images
public/ghostty-vt.wasm generated on install, not committed
GhosttyTerminalcreates the terminal, makes its canvas background transparent (so the ASCII art shows through), and feedsterm.onDataintoShell.handleInput.Shellowns the prompt, line editing, history and tab completion of command names. Commands write ANSI text throughshell.println/shell.write. Long-running commands receive anAbortSignalthat Ctrl+C triggers.shell.readLine(prompt)asks for input.shell.enterMode(mode)hands the prompt to something like the chat room untilexitMode.- Keys typed while a command runs are queued and replayed afterwards.
- The terminal has no shell or OS behind it. Everything is our JavaScript printing escape sequences.
- Create
src/lib/features/<name>.tsexporting aCommand(name,helprows,run(args, ctx)). - Register it in
GhosttyTerminal.tsxwithshell.register(...). Addshell.alias(...)for shortcuts. - Commands are reachable both as
<name>andhc <name>.
Anything that needs the server goes through a route handler in src/app/api/ and is called with fetch from the command.
- Login is
hc auth login: it asks for an email, opens a Hack Club Auth popup (/api/auth/login), and polls/api/auth/meuntil the callback sets the session. - Scopes are
openid profile slack_id. Accounts without a linked Slack ID are refused, because identity is tied to Slack to prevent abuse. - The display name comes from cachet (
cachet.dunkirk.sh) using the Slack ID, falling back to the profile name. - Sessions are an HMAC-signed cookie (
tty_session, 30 days, httpOnly). Signing lives insession-token.mjssoserver/chat.mjscan verify cookies on WebSocket upgrade. - Display names are not unique and can be changed in Slack. Do not treat a name as identity; use
slackId. Chat events carry{ id, nick }; the client colors names byidand appends#plus the last four characters of the id when two people online share a name.
hc chatopens a WebSocket to/api/chat, handled byserver/chat.mjs, not by Next.js (route handlers cannot hold WebSockets).- Nothing is stored. Messages only reach people connected at that moment.
- Upgrades require a valid session cookie and a same-origin
Originheader. Limits: 500 connections, 5 tabs per user, 8 messages per 10 s, 300 characters, 4 KB frames. - All text is passed through
sanitize(strips control and bidi characters) on the server and again on the client. - State is in memory, so the app must run as exactly one instance.
- RSVPs live in Postgres in the
rsvpstable (slack_idprimary key,name,sub,at). The table is created on first use. RSVPing twice is a no-op that retries the Slack invite.
| Variable | Purpose |
|---|---|
HCA_CLIENT_ID, HCA_CLIENT_SECRET |
Hack Club Auth app |
SESSION_SECRET |
session signing key, at least 32 characters |
APP_URL |
public origin; required behind a proxy so the OAuth redirect URI is correct |
DATABASE_URL |
Postgres for RSVPs |
SLACK_BOT_TOKEN, SLACK_CHANNEL_ID |
bot that invites people to #tty |
SLACK_SYNC, SLACK_APP_TOKEN |
Slack bridge, off unless SLACK_SYNC=on |
Local values go in .env.local (never committed). .env.example documents them. Do not import terminal-theme.ts from route handlers; it loads next/font.
- Production runs on Orchard (Hack Club's Kubernetes platform), project
whoami, deploymenttty, at https://tty-whoami-32a9a1.k.hackclub.dev. - Every push to
mainon github.com/cloudglides/tty builds and deploys automatically. Do not push unless the user asks. - Postgres is the Orchard database
tty. Env vars are set on the Orchard deployment, secrets as secret vars. - The deployment runs one replica with the
recreatestrategy, because chat state is in memory.
- No code comments anywhere. Code should explain itself through naming and structure.
- Terminal output must read like a real CLI (git, gh, npm), not a decorated chatbot:
- ASCII only. No
•,·,✓,✗,→,…, emoji or braille spinners. Use..., commas,-->/<--, and|/-\for spinners. The only exception is the▀▄half-blocks that draw images. - Lowercase, short and factual. Errors go through
error()(error: ...), warnings throughwarning(). - No exclamation marks, no cheerleading.
- ASCII only. No
- Write "Hack Club" with exactly that capitalization.
- Keep the page free of UI chrome. New features belong inside the terminal as commands.
- Prefer editing existing files. Keep features in
src/lib/features/, one file per command. - Server-side code shared with
server/must be plain.mjswith a.d.mtsdeclaration, sinceserver.mjsruns on Node without a build step.