The PawScapes directory isn't written in a CMS. A pipeline builds it: census scripts find candidate venues, enrichment researches each one's pet policy (with citations), a mirror copies photos into R2, and a writer drafts each guide. All of it lands in one D1 table, venues: 62 columns, one row per place. The live site lists 10,594 of them.
The pipeline owns that table and will keep owning it. Moving the rows into a CMS's own schema would mean two copies to keep in step, or rewriting every script to talk to the CMS instead of SQL. Neither was on the table.
A collection that points at the table
In highseam, table: turns a collection into a view over an existing SQL table. Field names match column names; nothing is copied.
const venues = defineCollection({
slug: "venues",
labels: { singular: "Venue", plural: "Venues" },
table: { name: "venues", autoId: true, timestamps: { createdAt: "created_at", updatedAt: "updated_at" } },
admin: {
useAsTitle: "name",
defaultColumns: ["name", "type", "state", "suburb", "status", "rating"],
description: "The national directory — machine-built, hand-polished.",
groupsAs: "tabs",
// Faceted list filters (combinable; straight SQL in table mode).
filters: ["type", "state", "status", "dogs_verified", "off_leash_allowed", "private_hire", "suburb", "written_at"],Editors get a tabbed form (content, landing sections, dog policy, location, fees, media, SEO, enrichment data), combinable filters that compile to plain SQL WHERE clauses, and the same REST and GraphQL APIs as any other collection.
The pipeline stays in charge
Three settings keep the admin from fighting the pipeline:
- No creating venues.
create: () => false. New rows come from the census, never from a form. - Pipeline fields are read-only. Rating, review counts, Google IDs, research notes and citations show in an "Enrichment data" tab but can't be edited.
- Writes touch only declared columns. Columns the config doesn't mention are left alone, so the pipeline can add columns without breaking the admin.
The pipeline is also wired into the admin as document actions. Two buttons on every venue call PawScapes' own endpoints:
actions: [
{
label: "✨ Enrich with AI",
url: "/api/enrich-venue",
confirm: "Run AI Mode enrichment for this venue? This overwrites rich_html.",
},
{ label: "📸 Mirror photo", url: "/api/mirror-photo" },
],Hand-polish on top of machine output
The guide the pipeline writes is HTML in rich_html. For venues worth extra effort, an editor writes a structured body in body_nodes instead: headings, images, galleries, reels, FAQs, tables. When body_nodes is set, the site renders it and ignores rich_html. The machine draft stays in the row as a fallback.
The hero image works the same way. photo_url is the pipeline's mirrored photo. hero_image_id is an upload field an editor can set, and it overrides the pipeline photo on the venue page. In table mode an upload stores the media document's id in the column, so that field needed no migration either: the existing text column took the id.
What table mode costs
"Nothing migrated" is true at adoption, not forever. A field in table mode is a column, so a new field means a new column. When venues gained optional landing sections in September, that was a new sections TEXT column, added by hand before the field could exist.
That trade is the right one for a table a pipeline owns. The editorial collections, where people write from scratch, use document mode and get the full draft, version and scheduling workflow.