potluck, diigo-cache: docs one line per paragraph; potluck deploy caveat

Obsidian and GitHub render each newline as a line break, so the
hard-wrapped READMEs and CONTRIBUTINGs rendered ragged. Whitespace
only, except a note in potluck's CONTRIBUTING that other workerd
hosts are untested: they'd have to supply the D1 and R2 bindings.

Generated-by: Claude Opus 5.5
This commit is contained in:
dckc committed 2026-10-09 01:30:56 -05:00
1 parent a25bd699e1
commit c66aaea366
4 files changed
+53 -182

No files matched your search

+24 -94
View File
@@ -1,100 +1,37 @@
# Contributing to diigo-cache # Contributing to diigo-cache
diigo-cache is a Cloudflare Worker cache over [the webapp factory](../webapp/README.md): diigo-cache is a Cloudflare Worker cache over [the webapp factory](../webapp/README.md): session-cookie reads of diigo bookmarks, cached in Durable-Object SQLite storage. Evolved from the diigo-dump worker (provenance: [mm-diigo `projects/diigo-bak/`](https://github.com/dckc/mm-diigo)); the static API-key path is dead (see [the capsite checklist](../capsite-checklist.md), goal 2 preamble).
session-cookie reads of diigo bookmarks, cached in Durable-Object SQLite
storage. Evolved from the diigo-dump worker (provenance:
[mm-diigo `projects/diigo-bak/`](https://github.com/dckc/mm-diigo)); the
static API-key path is dead (see [the capsite checklist](../capsite-checklist.md),
goal 2 preamble).
## What it holds ## What it holds
- **The create flow, on one port**: `/_connect` renders webapp's own - **The create flow, on one port**: `/_connect` renders webapp's own mint form (diigo-prefilled, cookie blank) — wrangler multi-config dev doesn't serve aux workers' HTTP, so `/_mint` relays the form opaquely into the WEBAPP binding (the cookie crosses; nothing on this side opens or logs it). Mint → `/_wire` (webapp id pre-filled, mode public|private) → the created page, where the user saves the ids.
mint form (diigo-prefilled, cookie blank) — wrangler multi-config dev - **Two independent page caches** (`src/cache.ts`), one behavior: a page hit is served from storage with no diigo call; a miss asks the webapp instance in the cache's wiring and etches the page.
doesn't serve aux workers' HTTP, so `/_mint` relays the form opaquely - `DiigoCache` (binding `CACHE_DO`): every item. Addressed by its cache id.
into the WEBAPP binding (the cookie crosses; nothing on this side opens - `PublicCache` (binding `PUBLIC_DO`): shared items only — private rows are dropped *before* etching (`publicOnly`), so its storage holds nothing private. Same cookie webapp as the private cache. Public page n is diigo page n, filtered (may show fewer than `PAGE_SIZE`, 100: diigo's cap). `/public/<username>` resolves through the `idFromName("index")` registry, and reads through `viewer()`, a read-only facet: it shows stored pages and fill progress, and can't fetch from diigo or write.
or logs it). Mint → `/_wire` (webapp id pre-filled, mode - **Storage**: SQL lives in `src/cache-read.sql` (selects only) and `src/cache-write.sql`; `src/store.ts` turns them into a reader and a writer facet, so the cache logic holds no SQL text.
public|private) → the created page, where the user saves the ids. - **Pages**: home's dump form (cache id + page number, POST) shows raw dump rows; `/public/<username>?page=n` shows an HTML list with a page form (JSON for `Accept: application/json`). Both say `cache: HIT` or `MISS`; every page read also logs `{event: "cache", kind, page, outcome}`.
- **Two independent page caches** (`src/cache.ts`), one behavior: a - **Filters on the public page** (`src/query.ts`): `days`, `after`, `before`, `in` (`2026`, `2026-03`, `2026-Q1`, `2026-W41`), `by=updated|created`, and `q` (words, `"phrases"`, `#tag`, `-` negation, gmail/Slack/GitHub date operators). UTC, half-open windows. The same fields work as a dev-side spec (`toQuery({ days: 7 })`). A filter queries the stored `items` table through `viewer()`, never diigo: results are as fresh as the cache, and JSON says `complete` when every diigo page is stored.
page hit is served from storage with no diigo call; a miss asks the - **Publish later**: `POST /_publish` (cache id in the body) wires a public companion to a private-only cache.
webapp instance in the cache's wiring and etches the page. - **The webkey page and Cap'n Web**: `/cache/#<cache id>` is a skeleton page, the same for every key; its script reads the key from the fragment and trades it in-band for a capability over [Cap'n Web](https://github.com/cloudflare/capnweb) at `/api` (`CacheApi.cache(key)` → a `CacheCap` facet: fill, status, the public companion; `src/api.ts`). The worker serves capnweb's browser build itself at `/_capnweb.js`.
- `DiigoCache` (binding `CACHE_DO`): every item. Addressed by its - **Fill job**: walks every diigo page into a cache in the background — DO alarms, a few pages per step, progress stored in the DO (`startFill`/`fillStatus`/`stopFill`), driven from the webkey page; the public page shows progress. "Invalidate the last N days" there drops the cached pages holding anything updated that recently (`invalidateRecent`), so the next fill refetches them.
cache id. - The `DiigoBridge` entrypoint for other workers (its cache-id source is the goal-2 wiring TODO).
- `PublicCache` (binding `PUBLIC_DO`): shared items only — private
rows are dropped *before* etching (`publicOnly`), so its storage
holds nothing private. Same cookie webapp as the private cache.
Public page n is diigo page n, filtered (may show fewer than
`PAGE_SIZE`, 100: diigo's cap).
`/public/<username>` resolves through the `idFromName("index")`
registry, and reads through `viewer()`, a read-only facet: it shows
stored pages and fill progress, and can't fetch from diigo or write.
- **Storage**: SQL lives in `src/cache-read.sql` (selects only) and
`src/cache-write.sql`; `src/store.ts` turns them into a reader and a
writer facet, so the cache logic holds no SQL text.
- **Pages**: home's dump form (cache id + page number, POST) shows raw
dump rows; `/public/<username>?page=n` shows an HTML list with a page
form (JSON for `Accept: application/json`). Both say `cache: HIT` or
`MISS`; every page read also logs `{event: "cache", kind, page,
outcome}`.
- **Filters on the public page** (`src/query.ts`): `days`, `after`,
`before`, `in` (`2026`, `2026-03`, `2026-Q1`, `2026-W41`),
`by=updated|created`, and `q` (words, `"phrases"`, `#tag`, `-`
negation, gmail/Slack/GitHub date operators). UTC, half-open windows.
The same fields work as a dev-side spec (`toQuery({ days: 7 })`). A
filter queries the stored `items` table through `viewer()`, never
diigo: results are as fresh as the cache, and JSON says `complete`
when every diigo page is stored.
- **Publish later**: `POST /_publish` (cache id in the body) wires a
public companion to a private-only cache.
- **The webkey page and Cap'n Web**: `/cache/#<cache id>` is a skeleton
page, the same for every key; its script reads the key from the
fragment and trades it in-band for a capability over
[Cap'n Web](https://github.com/cloudflare/capnweb) at `/api`
(`CacheApi.cache(key)` → a `CacheCap` facet: fill, status, the public
companion; `src/api.ts`). The worker serves capnweb's browser build
itself at `/_capnweb.js`.
- **Fill job**: walks every diigo page into a cache in the background —
DO alarms, a few pages per step, progress stored in the DO
(`startFill`/`fillStatus`/`stopFill`), driven from the webkey page;
the public page shows progress. "Invalidate the last N days" there
drops the cached pages holding anything updated that recently
(`invalidateRecent`), so the next fill refetches them.
- The `DiigoBridge` entrypoint for other workers (its cache-id source is
the goal-2 wiring TODO).
## State surface ## State surface
Ids are **webkeys** ([the pattern](../docs/webkeys.md)): 256-bit bearer Ids are **webkeys** ([the pattern](../docs/webkeys.md)): 256-bit bearer secrets that never ride a query string or path. The webkey page carries the cache id in the URL fragment (never sent to the server) and passes it in-band over Cap'n Web; forms carry ids in POST bodies; logs and pages show only a fingerprint (a prefix of the id's one-way hash).
secrets that never ride a query string or path. The webkey page carries
the cache id in the URL fragment (never sent to the server) and passes
it in-band over Cap'n Web; forms carry ids in POST bodies; logs and pages
show only a fingerprint (a prefix of the id's one-way hash).
- **The cache id**: the cache's read capability (bearer; not logged). - **The cache id**: the cache's read capability (bearer; not logged). Held by the user — saved to the password manager under a petname they choose (the username field; never sent to the server) — and passed per POST.
Held by the user — saved to the password manager under a petname they - **The webapp instance id**: the diigo-read capability; lives in each cache DO's wiring, arrives from the mint step.
choose (the username field; never sent to the server) — and passed - **The cookie**: lives **only in webapp's DO storage**; crosses the wire once, at mint time, as an opaque relay.
per POST. - **The public cache**: no capability — `/public/<username>` is public by design (a guessable URL; hiding the link doesn't hide the page).
- **The webapp instance id**: the diigo-read capability; lives in each
cache DO's wiring, arrives from the mint step.
- **The cookie**: lives **only in webapp's DO storage**; crosses the
wire once, at mint time, as an opaque relay.
- **The public cache**: no capability — `/public/<username>` is public
by design (a guessable URL; hiding the link doesn't hide the page).
Stored state is meant to be durable: see the breaking-change rule in Stored state is meant to be durable: see the breaking-change rule in [capsite AGENTS.md](../AGENTS.md).
[capsite AGENTS.md](../AGENTS.md).
## Deployment concern: rate-limit `/api` ## Deployment concern: rate-limit `/api`
Public pages only read storage, so strangers can't make the cache spend Public pages only read storage, so strangers can't make the cache spend your diigo session. `/api` is keyed (a cache id is needed), but Cap'n Web pipelining makes calls cheap to send. Before exposing a deployment:
your diigo session. `/api` is keyed (a cache id is needed), but Cap'n
Web pipelining makes calls cheap to send. Before exposing a deployment:
- rate-limit `/api` (and `/public/*`, which reads storage) — on - rate-limit `/api` (and `/public/*`, which reads storage) — on Cloudflare, the [Workers rate-limit binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/) (keyed by client and/or username); on other workerd hosts, a rate limit in the proxy in front;
Cloudflare, the
[Workers rate-limit binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/)
(keyed by client and/or username); on other workerd hosts, a rate
limit in the proxy in front;
- run the fill job (goal 1e): public pages show only what's stored. - run the fill job (goal 1e): public pages show only what's stored.
## RPC use (from another worker, same account) ## RPC use (from another worker, same account)
@@ -115,14 +52,11 @@ const ndjson = await env.DIIGO.dumpNdjson(4); // 4 pages of PAGE_SIZE
pnpm lint # fails if those types are stale; tsc over src/ and tests/ pnpm lint # fails if those types are stale; tsc over src/ and tests/
pnpm test # vitest pnpm test # vitest
Tests run the real worker and both DOs in the vitest pool, with Tests run the real worker and both DOs in the vitest pool, with `WEBAPP` bound to [a fake webapp](tests/fake-webapp.js) serving diigo-shaped items (`wrangler.tests.jsonc`).
`WEBAPP` bound to [a fake webapp](tests/fake-webapp.js) serving
diigo-shaped items (`wrangler.tests.jsonc`).
## Run ## Run
[The README](README.md) gets a cache going in dev. The same create flow [The README](README.md) gets a cache going in dev. The same create flow from a terminal:
from a terminal:
wid=$(curl -s -X POST http://localhost:8787/_mint --data-urlencode \ wid=$(curl -s -X POST http://localhost:8787/_mint --data-urlencode \
'url=https://www.diigo.com/interact_api/' --data-urlencode \ 'url=https://www.diigo.com/interact_api/' --data-urlencode \
@@ -131,12 +65,8 @@ from a terminal:
curl -s -X POST http://localhost:8787/_wire -H 'content-type: application/json' \ curl -s -X POST http://localhost:8787/_wire -H 'content-type: application/json' \
-d "{\"webappId\":\"<from $wid>\",\"mode\":\"private\"}" -d "{\"webappId\":\"<from $wid>\",\"mode\":\"private\"}"
Check a filled cache against a diigo RSS export: Check a filled cache against a diigo RSS export: `node scripts/compare-export.ts <cache.sqlite> <export.xml> <out-dir>`.
`node scripts/compare-export.ts <cache.sqlite> <export.xml> <out-dir>`.
## Deploy ## Deploy
Not deployed yet; the order matters (goal 5 in Not deployed yet; the order matters (goal 5 in [the capsite checklist](../capsite-checklist.md)): webapp first (`cd ../webapp && npx wrangler deploy`), then diigo-cache, after rate-limiting `/api` (above).
[the capsite checklist](../capsite-checklist.md)): webapp first
(`cd ../webapp && npx wrangler deploy`), then diigo-cache, after
rate-limiting `/api` (above).
+7 -19
View File
@@ -1,9 +1,6 @@
# diigo-cache # diigo-cache
Your diigo bookmarks, cached in a [workerd](https://github.com/cloudflare/workerd) Your diigo bookmarks, cached in a [workerd](https://github.com/cloudflare/workerd) worker, with a public page you can filter by week: `/public/<you>?in=2026-W41`. Your diigo session cookie is stored in one Durable Object and nowhere else.
worker, with a public page you can filter by week:
`/public/<you>?in=2026-W41`. Your diigo session cookie
is stored in one Durable Object and nowhere else.
## Getting started ## Getting started
@@ -14,25 +11,16 @@ pnpm dev # → http://localhost:8787/
Then, in the browser: Then, in the browser:
1. **mint the diigo session at webapp**: paste the `Cookie` header from 1. **mint the diigo session at webapp**: paste the `Cookie` header from diigo.com (devtools, any logged-in request).
diigo.com (devtools, any logged-in request). 2. **wire** it, mode **public**. Save the cache id it shows (the password-manager button does this).
2. **wire** it, mode **public**. Save the cache id it shows (the
password-manager button does this).
3. **open this cache** → **fill**, and watch it walk your bookmarks. 3. **open this cache** → **fill**, and watch it walk your bookmarks.
4. Visit `/public/<your diigo username>?days=7`. 4. Visit `/public/<your diigo username>?days=7`.
## Bookmarks you hold, a page anyone can read ## Bookmarks you hold, a page anyone can read
- **Filter by date and search:** `?days=7`, `?in=2026-W41` (ISO week, - **Filter by date and search:** `?days=7`, `?in=2026-W41` (ISO week, UTC), `?after=2026-01-01&before=2026-02-01`, `?by=created`, and `?q=` with words, `"phrases"`, `#tags`, `-negation`, and gmail-style `newer_than:7d`. Add `Accept: application/json` for JSON.
UTC), `?after=2026-01-01&before=2026-02-01`, `?by=created`, and - **Private stays private:** the public cache drops private bookmarks before storing anything, so it never holds them.
`?q=` with words, `"phrases"`, `#tags`, `-negation`, and gmail-style - **Ids are capabilities:** the cache id is a bearer key you keep in your password manager. It rides URL fragments and POST bodies, never a path or query string, so it stays out of logs and history.
`newer_than:7d`. Add `Accept: application/json` for JSON. - **Cheap to read:** pages come from Durable Object SQLite storage, and a fill job in the background fetches each diigo page once.
- **Private stays private:** the public cache drops private bookmarks
before storing anything, so it never holds them.
- **Ids are capabilities:** the cache id is a bearer key you keep in
your password manager. It rides URL fragments and POST bodies, never a
path or query string, so it stays out of logs and history.
- **Cheap to read:** pages come from Durable Object SQLite storage, and
a fill job in the background fetches each diigo page once.
Design, tests, and deploying: [CONTRIBUTING](CONTRIBUTING.md). Design, tests, and deploying: [CONTRIBUTING](CONTRIBUTING.md).
+17 -55
View File
@@ -1,44 +1,17 @@
# Contributing to potluck # Contributing to potluck
potluck started as EmDash's `cloudflare:blog` template potluck started as EmDash's `cloudflare:blog` template (`pnpm create emdash@latest`); it's part of [the capsite goal tree](../capsite-checklist.md), where it is the site that publishes the weekly roundup.
(`pnpm create emdash@latest`); it's part of
[the capsite goal tree](../capsite-checklist.md), where it is the site
that publishes the weekly roundup.
## How it's put together ## How it's put together
- **Issues** are the `issues` collection - **Issues** are the `issues` collection ([seed/issues.json](seed/issues.json)): `title`, `week` (an ISO week, `2026-W40`), `dek` (one-line summary), `body` (Portable Text). The slug is the week, lowercased: `/issues/2026-w40`. [issues-sketch.md](issues-sketch.md) is the dry run behind the three sample issues: real public bookmarks, editorial lines marked *draft*.
([seed/issues.json](seed/issues.json)): `title`, `week` (an ISO week, - **Pages:** home and `/issues` list issues ([src/pages/issues/](src/pages/issues/)); `/tag/<tag>` lists issues by diigo tag; search and `/rss.xml` cover issues. The `posts` collection and its pages are the template's, still in place.
`2026-W40`), `dek` (one-line summary), `body` (Portable Text). The - **Microformats:** an issue list is an `h-feed` of `h-entry`s; an issue is an `h-entry` (`p-name`, `dt-published`, `p-summary`, `e-content`). In issue bodies, [CiteLink.astro](src/components/CiteLink.astro) marks off-site links as `h-cite` and `/tag/<tag>` links as rel-tag `p-category`.
slug is the week, lowercased: `/issues/2026-w40`. - **Look:** plain HTML, after kmjn.org's [Plain HTML with a few bureaucratic gestures](https://www.kmjn.org/notes/modern_plain_html.html): system fonts (no webfonts), one ~800px column, links in the text color with a dotted underline. Restyle in [src/styles/theme.css](src/styles/theme.css), which overrides the defaults in `tokens.css` (unlayered, so it always wins); don't edit `tokens.css` or `Base.astro` for visual changes. Colors use `light-dark(<light>, <dark>)`.
[issues-sketch.md](issues-sketch.md) is the dry run behind the three
sample issues: real public bookmarks, editorial lines marked *draft*.
- **Pages:** home and `/issues` list issues
([src/pages/issues/](src/pages/issues/)); `/tag/<tag>` lists issues
by diigo tag; search and `/rss.xml` cover issues. The `posts`
collection and its pages are the template's, still in place.
- **Microformats:** an issue list is an `h-feed` of `h-entry`s; an
issue is an `h-entry` (`p-name`, `dt-published`, `p-summary`,
`e-content`). In issue bodies,
[CiteLink.astro](src/components/CiteLink.astro) marks off-site links
as `h-cite` and `/tag/<tag>` links as rel-tag `p-category`.
- **Look:** plain HTML, after kmjn.org's
[Plain HTML with a few bureaucratic gestures](https://www.kmjn.org/notes/modern_plain_html.html):
system fonts (no webfonts), one ~800px column, links in the text
color with a dotted underline. Restyle in
[src/styles/theme.css](src/styles/theme.css), which overrides the
defaults in `tokens.css` (unlayered, so it always wins); don't edit
`tokens.css` or `Base.astro` for visual changes. Colors use
`light-dark(<light>, <dark>)`.
## Seeds ## Seeds
[seed/seed.json](seed/seed.json) (site settings, menus, widgets, the [seed/seed.json](seed/seed.json) (site settings, menus, widgets, the template's collections) is applied on the first request to an empty database. [seed/issues.json](seed/issues.json) is separate: `pnpm seed:issues` applies it to the local D1 database, which exists after the dev server's first request. Run it with the dev server stopped.
template's collections) is applied on the first request to an empty
database. [seed/issues.json](seed/issues.json) is separate:
`pnpm seed:issues` applies it to the local D1 database, which exists
after the dev server's first request. Run it with the dev server
stopped.
## Checks ## Checks
@@ -49,22 +22,14 @@ pnpm build
## EmDash rules ## EmDash rules
- All content pages are server-rendered (`output: "server"`); no - All content pages are server-rendered (`output: "server"`); no `getStaticPaths()` for CMS content.
`getStaticPaths()` for CMS content. - Image fields are objects (`{ src, alt }`), not strings; render with `<Image image={...} />` from `"emdash/ui"`.
- Image fields are objects (`{ src, alt }`), not strings; render with - `entry.id` is the slug (for URLs); `entry.data.id` is the database ULID (for API calls like `getEntryTerms`).
`<Image image={...} />` from `"emdash/ui"`. - When Astro's cache is enabled, pass query hints to `Astro.cache.set(cacheHint)`.
- `entry.id` is the slug (for URLs); `entry.data.id` is the database - Taxonomy names in queries match the seed's `"name"` exactly (`"tag"`, not `"tags"`).
ULID (for API calls like `getEntryTerms`). - `npx emdash types` regenerates `emdash-env.d.ts` from a running site (the dev server also regenerates it on start).
- When Astro's cache is enabled, pass query hints to
`Astro.cache.set(cacheHint)`.
- Taxonomy names in queries match the seed's `"name"` exactly (`"tag"`,
not `"tags"`).
- `npx emdash types` regenerates `emdash-env.d.ts` from a running site
(the dev server also regenerates it on start).
The EmDash docs are an MCP server at `https://docs.emdashcms.com/mcp` The EmDash docs are an MCP server at `https://docs.emdashcms.com/mcp` (wired up in `.mcp.json`, `.cursor/mcp.json`, `.vscode/mcp.json`). Check them rather than recalling APIs from memory.
(wired up in `.mcp.json`, `.cursor/mcp.json`, `.vscode/mcp.json`).
Check them rather than recalling APIs from memory.
## Deploying ## Deploying
@@ -73,13 +38,10 @@ pnpm wrangler login
pnpm deploy pnpm deploy
``` ```
The first deploy provisions the D1 database and R2 bucket named in The first deploy provisions the D1 database and R2 bucket named in [wrangler.jsonc](wrangler.jsonc). See [Deploy to Cloudflare](https://docs.emdashcms.com/deployment/cloudflare/).
[wrangler.jsonc](wrangler.jsonc). See
[Deploy to Cloudflare](https://docs.emdashcms.com/deployment/cloudflare/). Other workerd hosts are untested: the site uses D1 and R2 through `@emdash-cms/cloudflare` and the Cloudflare adapter (locally, miniflare stands in for both), so a host has to supply those two bindings.
## For agents ## For agents
`AGENTS.md` is a symlink to this file. capsite's `AGENTS.md` is a symlink to this file. capsite's [working rules](../AGENTS.md) apply here too (the dev server is the user's to start and stop). EmDash skills are in `.agents/skills/`: `building-emdash-site` (start here), `creating-plugins`, `emdash-cli`.
[working rules](../AGENTS.md) apply here too (the dev server is the
user's to start and stop). EmDash skills are in `.agents/skills/`:
`building-emdash-site` (start here), `creating-plugins`, `emdash-cli`.
+5 -14
View File
@@ -1,7 +1,6 @@
# potluck # potluck
A weekly zine of what you bookmarked elsewhere, published on your own A weekly zine of what you bookmarked elsewhere, published on your own site. Each issue is one week of links with a few words around them.
site. Each issue is one week of links with a few words around them.
## Getting started ## Getting started
@@ -14,18 +13,10 @@ pnpm dev # → http://localhost:4321/issues
## Bookmarks in, a zine out ## Bookmarks in, a zine out
- **Your web, your site:** the bookmarks come from your own diigo - **Your web, your site:** the bookmarks come from your own diigo account, so the web you read becomes something you publish ([PESOS](https://indieweb.org/PESOS)). For now issues are put together by hand. Pulling a week from diigo-cache is next.
account, so the web you read becomes something you publish
([PESOS](https://indieweb.org/PESOS)). For now issues are put
together by hand. Pulling a week from diigo-cache is next.
- **A week per issue:** `/issues/2026-w40`, by ISO week, UTC. - **A week per issue:** `/issues/2026-w40`, by ISO week, UTC.
- **Microformats:** feed readers and IndieWeb tools see each issue as an - **Microformats:** feed readers and IndieWeb tools see each issue as an [h-entry](https://microformats.org/wiki/h-entry) and each bookmark as an [h-cite](https://microformats.org/wiki/h-cite).
[h-entry](https://microformats.org/wiki/h-entry) and each bookmark as
an [h-cite](https://microformats.org/wiki/h-cite).
- **Plain HTML** that's easy to read: system fonts, one column. - **Plain HTML** that's easy to read: system fonts, one column.
- **A real editor:** it's an [EmDash](https://docs.emdashcms.com/) site, - **A real editor:** it's an [EmDash](https://docs.emdashcms.com/) site, so you write issues in its admin UI. It runs in [workerd](https://github.com/cloudflare/workerd), on your machine or a host.
so you write issues in its admin UI. It runs in
[workerd](https://github.com/cloudflare/workerd), on your machine or a host.
How it's put together, checks, and deploying: How it's put together, checks, and deploying: [CONTRIBUTING](CONTRIBUTING.md).
[CONTRIBUTING](CONTRIBUTING.md).