An atmospheric personal website. Every page is a record on the AT Protocol; the site is a view layer over those records.
- Vite + React SPA, plain CSS, no state library.
- AT Protocol as the database. Records live under
is.dame.*lexicons on the user's PDS; build-time JSON snapshots come fromapp.bsky.feed.getAuthorFeed,com.atproto.repo.listRecords, andapp.bsky.actor.getProfile. - Vercel for hosting. A 6-hour cron pings
/api/rebuild, which fires the Vercel deploy hook so new records publish without a manual redeploy.
Schema-only documentation lives under lexicons/:
is.dame.now— short status updates ("dame.is hiking").is.dame.page— page-content records keyed by literal page name.is.dame.profile— extended long-form bio atat://{me}/is.dame.profile/self.is.dame.creating.work— portfolio works (art, software, writing, music…).is.dame.resume/is.dame.resume.job/is.dame.resume.education— a backlinked resume model: many resume versions curate shared job/education records and pick which highlight bullets to show. Seelexicons/RESUME.md; renders at/resume. Bulk-import from a LinkedIn/resume PDF withscripts/import-resume.mjs.is.dame.annotating— stretch lexicon for book-style margin notes.is.dame.mothing/is.dame.mothing.observation— a mirror of moth observations from iNaturalist, rendered at/mothing. The summary singleton lives atat://{me}/is.dame.mothing/self; one observation record per iNaturalist observation is keyed by its iNat id. No location data is ever mirrored — coordinates, place names, and timezone are stripped insrc/lib/inaturalist.js. Sync withscripts/mirror-inaturalist.mjs. Once mirrored, moth observations also appear in the home feed under themothingverb, ordered by observation date.is.dame.observing/is.dame.observing.observation— the same mirror for every non-moth iNaturalist observation (birds, plants, fungi, butterflies, other insects…), rendered in the home feed under theobservingverb. The same sync writes both: it pulls every observation once and splits each by taxonomy — moths (Lepidoptera minus butterflies) becomeis.dame.mothing.*records, everything elseis.dame.observing.*— so one iNaturalist log shows up as mothing if it's a moth and observing otherwise, with nothing dropped or double-counted. Same location-stripping guarantee. Theis.dame.observing/selfsummary doubles as the incremental mirror's freshness marker across all observations.is.dame.guestbook/is.dame.guestbook.entry— a decentralized guestbook, rendered at/guestbook. The book is a singleton on my PDS (at://{me}/is.dame.guestbook/self); each signature is anis.dame.guestbook.entryrecord on the visitor's own PDS whosesubjectbacklinks to the book. Reads are assembled live from Constellation backlinks hydrated through Slingshot; writes happen in the browser via atproto OAuth. Seelexicons/GUESTBOOK.md. Create the book once withscripts/create-guestbook.mjs.is.dame.state— live iPhone-sourced physical + ambient state (heart rate, activity, battery, ambient sound, calories). An append-only log keyed by TID: the latest record is the current "right now" rendered by the atmosphere-bar vitals panel (src/components/VitalsPanel.jsx, read viasrc/hooks/useDameState.js), and the full series is the history. Each record is immutable, so anis.dame.nowstatus can strong-ref the one captured alongside it via its optionalstateRef— coupling a note to the body-state behind it, which renders as a small chip (src/components/VitalsChip.jsx) under the status in the feed and on its record page. Posted straight from an iPhone Apple Shortcut (app-passwordcreateSession→createRecord, no server), and also charts over time in the /logging dashboard (src/components/StateStats.jsx). Seelexicons/STATE.md.is.dame.nav— optional PDS override for the nav-menu (dock sheet) route list. When itsenabledflag is set, itsitemsselect / reorder / relabel / hide the entries; otherwise the site uses the hardcoded routes insrc/lib/navRoutes.js. Edited in the admin "Nav menu" panel.
Plus fm.teal.alpha.feed.play (teal.fm) for the now-playing signal.
/mothing reads moth observations for the iNaturalist user configured in
src/config.js (INATURALIST_USER), scoped to Lepidoptera minus butterflies.
The page paints from the build-time public/data/mothing.json snapshot and
refreshes live from the iNaturalist API in the browser.
The home feed goes further: every iNaturalist observation is mirrored onto
the PDS and surfaced there — moths under the mothing verb, everything else
(birds, plants, fungi, butterflies, other insects…) under observing. The mirror
pulls all observations once and classifies each from its taxon ancestry
(isMoth = Lepidoptera, minus butterflies) so the two feed verbs partition the
set with nothing dropped or double-counted.
To keep that owned copy on the PDS, run the mirror:
BSKY_IDENTIFIER=dame.is BSKY_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx \
node scripts/mirror-inaturalist.mjs # --full to force a complete resyncThe mirror is incremental: a cheap freshness check (one small request, over all
observations) skips the run entirely when nothing changed; otherwise it re-pulls
only observations changed since the last sync (via iNaturalist's
updated_since), writes just those to the right collection plus the two
refreshed summaries, and deletes records for observations removed upstream (or
moved across the moth boundary by a re-identification). --dry-run plans without
writing; --full ignores the freshness marker. A Vercel cron runs the same
sync every 6 hours (/api/mirror-mothing, see vercel.json), just before each
/api/rebuild, so new observations reach the PDS — and the home feed — within
hours instead of a day. It uses BSKY_APP_PASSWORD (+ optional
BSKY_IDENTIFIER); set CRON_SECRET to lock the endpoint down. The freshness
check makes the extra runs cheap: they no-op when nothing changed upstream.
The /mothing page applies the same freshness check in the browser: it paints
from the snapshot and only re-pulls the full observation set from iNaturalist
when the cheap signature says something actually changed.
npm install
npm run prefetch # writes public/data/*.json from your PDS + AppView
npm run dev # Vite dev server on http://localhost:5173npm run build runs the prefetch script, then vite build. npm run build:offline
skips the prefetch (useful when a snapshot already exists).
| Concept | Lives in |
|---|---|
| AT Protocol client (Node + browser) | src/lib/atproto.js |
| Build-time data fetcher | scripts/prefetch.mjs |
| Hybrid snapshot/live data hook | src/hooks/useLiveFeed.js |
| Per-route AT URI lookup | src/hooks/useAtUri.js |
<head> discoverability tags |
src/components/AtUriHead.jsx |
| Atmosphere debug overlay | src/components/DebugPane.jsx |
| Day-of-life math | src/lib/dayOfLife.js |
After deploying, in Vercel:
- Settings → Git → Deploy Hooks → create one named
pds-rebuildand copy the URL. - Settings → Environment Variables → set
DEPLOY_HOOK_URLto that URL. - (Optional) point your publishing flow (Apple Shortcuts, etc.) at the same URL
so a
curl -X POSTafter writing a record kicks an instant rebuild.
bluesky-comments— comment threads on blog posts.- teal.fm — listening signal.