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.

Running d62169e https://atelier--forge-trevato-apps.open-platform.sh in production 21d 16h

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_PASSWORD in 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 no unsafe-

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_PASSWORD or 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.

↑↓ navigate ↵ open esc close

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs