trevato/atelier
Public: anyone can read this; the crew can change it.
A design studio's website with an in-app editor: a home page, a work gallery with case-study pages, an about page and a contact form; the studio edits every page's text and uploads images from a password-protected /admin (one admin account set at first run), images are resized on upload and served with long cache headers, motion respects prefers-reduced-motion, and Lighthouse is 100 across the board.
main
- src
- static
- .gitignore 37 B · ~11 tok
- AGENTS.md 2.2 KB · ~560 tok
- forge.json 111 B · ~37 tok
- package.json 144 B · ~48 tok
- README.md 5.2 KB · ~1.3k tok
README.md
atelier
A design studio's website that is edited from inside itself: a home page, a work gallery with case-study pages, an about page and a contact form, all of whose words and pictures live in the database rather than in a template.
PORT=8080 FORGE_APP_DATA=./data bun run src/server.ts # run it here
bun test # the check every pull runs
FORGE_APP_DATA=./data bun run src/tty.ts # the terminal interface
Open / for the site and /admin for the editor.
Claiming the studio account needs a secret only whoever runs the app has. There are two ways to give it one:
- Set
ADMIN_PASSWORDin the environment. The account is made from it at boot
and the claim screen never appears.
- Otherwise the app mints a one-time set-up token at every boot while the
account is unclaimed, and prints it to the run log
(atelier: set-up token 1a2b3-…) and on the first screen of the app's
terminal. /admin then asks for that token alongside the new password.
A visitor who merely finds /admin can claim nothing. The token is spent the
moment the account is claimed, and a new one is minted at each boot until then,
so a token from an old log is already dead. There is no second account and no
password reset: to start again, delete the admin and session rows.
What is real
Everything. Text, case studies, images, contact messages and the account are
rows in SQLite under FORGE_APP_DATA; nothing is faked and nothing is
hard-coded. A fresh install seeds a finished site — a studio called Atelier
with three case studies — so there is something to look at and edit rather
than a blank page. Uploaded images are decoded and resized for real; the
contact form stores messages for real but does not send email.
The editor
/admin has four rooms:
- Text — every sentence on the site, grouped by page. Blank line starts a
paragraph; emphasis, strong and label are
the whole markup language, and everything else is escaped.
- Work — case studies: address, client, year, discipline, summary, story,
cover, order, published or draft, plus the images hung inside them.
- Images — upload once, at any size up to 12 MB. Each is resized to 1600,
800 and 320 pixels wide, encoded as WebP, and served from
/media/<hash>/… with cache-control: immutable for a year, because the
URL contains a hash of the original and can never mean anything else. Every
image needs a description before it is kept.
- Messages — what the contact form collected.
Speed and manners
- Server-rendered HTML, no JavaScript anywhere on the site, no web fonts.
One stylesheet, served under a content-hashed name and cached for a year.
- No layout shift: every image carries the width and height read back off the
bytes that were actually encoded.
- Motion — the load rise, the card hover, the scroll reveal — lives entirely
inside @media (prefers-reduced-motion: no-preference).
- Colour, focus rings and tap targets are chosen for contrast; each page has a
description, a canonical URL, an og: set, and /robots.txt and
/sitemap.xml are generated from the content.
Security
- Every response carries the floor in
src/server.ts: a CSP with nounsafe-
anything (there are no inline styles or scripts), nosniff, a referrer
policy, a permissions policy and frame-ancestors 'none'.
- Every POST must be same-origin; every input is length-checked and validated
at the boundary; every output is escaped by src/html.ts.
- The account is claimed from
ADMIN_PASSWORDor a one-time boot token
compared in constant time, never by whoever reaches /admin first. An
account claimed by an earlier version of this app, when set-up needed no
secret, is deleted at boot along with its sessions.
- The admin password is argon2id via
Bun.password. The session is a random
token in an HttpOnly, SameSite=Lax cookie, Secure when the site is https.
Sign-in attempts, claim attempts and contact posts are rate-limited per
address.
The terminal
The exact command is in FORGE_APP_SSH when the app runs, and the studio page
at /admin prints it verbatim (or says the terminal is off when it is unset).
It opens src/tty.ts on a pty over the same data: j/k move, enter opens or
edits, esc goes back, q quits; in Work p publishes and [ ] reorder; in
Messages r marks read and x deletes.
Shape
No dependencies. src/db.ts holds the schema; src/content.ts the field
registry and the seeded copy; src/html.ts escaping and the small prose
language; src/images.ts upload, resize and serve; src/projects.ts,
src/messages.ts, src/session.ts (claiming the account, and sessions),
src/throttle.ts one concept each;
src/routes.ts the table of public paths and src/admin.ts the editor's;
src/pages/* render. src/server.ts is the front door and the security floor.
The tests spawn the app as a real subprocess with its own data directory
(src/testing.ts), because bun test runs every file in one process and a
shared module would mean a shared database.
The forge sets PORT, FORGE_APP_DATA, FORGE_APP_ENV (production or
preview), FORGE_APP_URL, FORGE_APP_SSH and FORGE_APP_COMMIT when it
starts the app. forge.json says how.