···11-# atboards (React SPA)
11+# atbbs web
2233-A static SPA reimplementation of the atboards web UI. No server, no database — all reads go directly to Slingshot/Constellation, all writes go directly to the user's PDS via atproto OAuth (DPoP). Designed to be hosted as static files on Cloudflare Pages or any static host.
33+Static React SPA. No backend — reads go to Slingshot/Constellation, writes go to the user's PDS via atproto OAuth.
4455-## Stack
66-77-- **Vite + React 19 + TypeScript**
88-- **react-router-dom v7** (history routing)
99-- **`@atproto/oauth-client-browser`** for OAuth (same library red-dwarf uses)
1010-- **`@atproto/api`** `Agent` for authenticated XRPC writes
1111-- **Tailwind CSS v4** (via `@tailwindcss/vite`)
55+## Development
1261313-All reads (boards, threads, replies, news, bans, hides, identity resolution) go through public Microcosm services:
77+```sh
88+cd web
99+npm install
1010+npm run dev
1111+```
14121515-- `slingshot.microcosm.blue` — getRecord, listRecords, resolveMiniDoc
1616-- `constellation.microcosm.blue` — getBacklinks (used to find threads in a board, replies to a thread, news for a site, quotes of a reply)
1717-- `ufos-api.microcosm.blue` — random BBS discovery on the home page
1313+OAuth works automatically on `http://127.0.0.1:5173` via atproto's loopback client flow.
18141919-All writes go to `agent.com.atproto.repo.{createRecord, putRecord, deleteRecord, uploadBlob}` against the user's PDS, using the OAuth/DPoP session held by `@atproto/oauth-client-browser`.
1515+## Production
20162121-## Layout
1717+### Static deploy (Cloudflare Pages, etc.)
22182323-```
2424-react/
2525-├── index.html
2626-├── package.json
2727-├── vite.config.ts
2828-├── tsconfig.json
2929-├── public/
3030-│ ├── client-metadata.json # OAuth client metadata for production (edit before deploy)
3131-│ ├── _redirects # Cloudflare Pages SPA fallback
3232-│ ├── favicon.svg
3333-│ └── hero.svg
3434-└── src/
3535- ├── main.tsx # Root, BrowserRouter + AuthProvider
3636- ├── App.tsx # Routes
3737- ├── index.css # Tailwind entry
3838- ├── components/
3939- │ ├── Layout.tsx # Header / footer / breadcrumb
4040- │ └── Localtime.tsx
4141- ├── lib/
4242- │ ├── lexicon.ts # xyz.atboards.* collection IDs
4343- │ ├── util.ts # date / AT-URI helpers
4444- │ ├── atproto.ts # Slingshot + Constellation read wrappers
4545- │ ├── bbs.ts # `resolveBBS()` — port of core/resolver.py
4646- │ ├── oauth.ts # BrowserOAuthClient setup
4747- │ ├── auth.tsx # AuthProvider / useAuth() hook
4848- │ └── writes.ts # PDS write helpers (createThread, createReply, …)
4949- └── pages/
5050- ├── Home.tsx
5151- ├── Login.tsx
5252- ├── Callback.tsx # /oauth/callback (no logic — provider handles it)
5353- ├── Site.tsx # /bbs/:handle
5454- ├── Board.tsx # /bbs/:handle/board/:slug
5555- ├── Thread.tsx # /bbs/:handle/thread/:did/:tid
5656- ├── Account.tsx # /account (inbox + BBS controls)
5757- ├── SysopCreate.tsx # /account/create
5858- ├── SysopEdit.tsx # /account/edit
5959- ├── SysopModerate.tsx # /account/moderate
6060- └── NotFound.tsx
1919+```sh
2020+VITE_PUBLIC_URL=https://your-domain.com npm run build
6121```
62226363-## Routes
6464-6565-Mirror the Python app exactly:
2323+Deploy `dist/`. The `_redirects` file handles SPA routing on Cloudflare Pages.
66246767-| Route | Page |
6868-|---------------------------------|---------------|
6969-| `/` | Home |
7070-| `/login` | Login |
7171-| `/oauth/callback` | Callback |
7272-| `/account` | Account |
7373-| `/account/create` | SysopCreate |
7474-| `/account/edit` | SysopEdit |
7575-| `/account/moderate` | SysopModerate |
7676-| `/bbs/:handle` | Site |
7777-| `/bbs/:handle/board/:slug` | Board |
7878-| `/bbs/:handle/thread/:did/:tid` | Thread |
7979-8080-The old `/api/threads/...` and `/api/replies/...` JSON endpoints are gone — pages do the same aggregation client-side via `lib/atproto.ts`.
8181-8282-## Development
2525+### Docker
83268427```sh
8585-cd react
8686-npm install
8787-npm run dev
2828+docker run -d -p 8080:80 -e PUBLIC_URL=https://your-domain.com ghcr.io/alyraffauf/atbbs:latest
8829```
89309090-For OAuth in dev, `BrowserOAuthClient` automatically falls back to a **loopback client** when no `clientMetadata` is provided. This works for `http://localhost:5173` without any tunneling — the client_id becomes `http://localhost/?...` and atproto auth servers accept it.
3131+The entrypoint generates `config.json` and `client-metadata.json` at runtime from `PUBLIC_URL`.
91329292-## Production deployment (Cloudflare Pages)
3333+### OAuth
93349494-1. Edit `public/client-metadata.json` and replace every `REPLACE_WITH_YOUR_DOMAIN` with your deployed origin (e.g. `https://atbbs.app`).
9595-2. Set the build env var `VITE_PUBLIC_URL=https://atbbs.app` so `lib/oauth.ts` uses the production metadata path.
9696-3. `npm run build` — outputs static files to `dist/`.
9797-4. Deploy `dist/` to Pages. The included `public/_redirects` makes Pages serve `index.html` for all routes (history routing).
9898-5. Verify `https://your.domain/client-metadata.json` is publicly fetchable — that URL is your `client_id`, atproto auth servers will fetch it during the OAuth handshake.
9999-100100-## Auth flow
101101-102102-1. User hits `/login`, types handle, presses log in.
103103-2. `useAuth().login(handle)` → `BrowserOAuthClient.signIn(handle)` → DPoP keypair generated, PAR pushed, browser redirected to the user's authserver.
104104-3. Authserver redirects back to `/oauth/callback?code=…&state=…`.
105105-4. The `AuthProvider` runs `client.init()` on every mount; on the callback page that detects the code, exchanges it, and returns a `OAuthSession`.
106106-5. We wrap that session in an `Agent` and stash `{did, handle, pdsUrl}` in context.
107107-6. Session/refresh tokens are persisted by the OAuth client in IndexedDB; reloads silently restore the session.
108108-3535+`https://your-domain.com/client-metadata.json` must be publicly fetchable — atproto auth servers fetch it during the OAuth handshake.