CRM — Linked Open Data for Aesthetic Computer #
Status: ✅ Stage 1 LIVE at https://data.aesthetic.computer (deployed
2026-06-29; open to all public handles, In-Copyright by default; @jeffrey
& @fifi upgraded to CC-BY-4.0) · Owner: @jeffrey · Drafted: 2026-06-28
Live & verified: landing recent-records feed (all public contributors),
/@{handle} (Person), /painting/{code} (image URLs resolve 200),
/mood/{handle}/{rkey}, /.well-known/void, content negotiation (JSON-LD vs.
HTML), and opt-out exclusion. Cloudflare data A record (→ 209.38.133.33,
proxied) created via vault token.
Stage 1 ships in these files:
system/backend/linked-art.mjs— pure Linked Art serializers + license/rights registry (CC + rightsstatements.org InC).system/netlify/functions/crm.mjs— endpoint: path routing,rightsFor(open-by-default/In-Copyright, opt-out, CC upgrade), content negotiation, recent-records landing, VoID, 501 SPARQL stub.system/tests/linked-art.test.mjs—node --test system/tests/linked-art.test.mjs(7 passing).lith/server.mjs— host rewritedata.aesthetic.computer/* → /api/crm/*.lith/Caddyfile—@datahost block → lith.
Rights controls (set by hand in @handles until a self-serve toggle lands):
- opt out:
{$set:{linkedData:{enabled:false}}}→ excluded entirely. - CC upgrade:
{$set:{linkedData:{enabled:true, license:'CC-BY-4.0', optInAt:new Date()}}}. - default (no flag) → In Copyright. In dev,
?preview=CC-BY-4.0forces a license for validation.
Expose AC users' work as dereferenceable linked data so cultural-heritage researchers (Getty, museums, the American Art Collaborative) can discover, cite, and federate over it — e.g. a Getty researcher querying "all digital paintings produced by @sat in 2026" against AC's endpoint joined with their own collection.
This is the research/archival sibling of the Bluesky mirror. Bluesky broadcasts moods to a social firehose; this broadcasts all opted-in work to the semantic web. Same architecture (opt-in allowlist, dual-channel, stable external identity), different audience and ontology.
1. Precedent in the repo — the Bluesky pattern #
system/backend/bluesky-mirror.mjs is the template we are deliberately copying:
- Opt-in allowlist — only handles in
secrets.bluesky.mirrorHandlesbroadcast. - Dual-channel — a mood lives in Mongo and gets a stable external
identity (
atproto.rkey,bluesky.uri) plus a public permalink (https://aesthetic.computer/moods~{handle}~{rkey}—bluesky-mirror.mjs:80). - Pull-back — engagement is re-fetched from a public API
(
bluesky-engagement.mjs).
CRM keeps all three properties. The "external identity" becomes an RDF URI; the "broadcast target" becomes a SPARQL store + JSON-LD content negotiation.
2. Strategic decision: target Linked Art, a profile of CIDOC CRM #
CIDOC CRM (ISO 21127) is the ontology. But Getty and peer institutions do
not ingest raw CRM RDF in practice — they consume Linked Art,
a JSON-LD profile of CIDOC CRM with fixed property names (produced_by,
carried_out_by, classified_as) and Getty AAT vocabulary terms.
Decision: emit Linked Art JSON-LD as the primary representation, with the CIDOC CRM classes underneath. A SPARQL endpoint exposes the same triples. "Through CIDOC CRM" is satisfied by Linked Art; raw-CRM purists can still query the SPARQL store.
Why this matters: Linked Art is what their pipelines actually parse, and it
gives us an off-the-shelf @context, validators, and example consumers.
3. URI strategy — a dedicated identity domain: data.aesthetic.computer #
Decision: canonical linked-data identifiers live on data.aesthetic.computer,
mirroring Getty's own data.getty.edu and Europeana's data.europeana.eu. A
dedicated domain is the cultural-heritage convention precisely because the
identifier must resolve forever while the public app (aesthetic.computer)
keeps changing. Each identity URI carries a subject_of/equivalent link back
to the human-facing page on the apex, so the two never drift.
| Entity | Canonical identity URI | Links to (human page) | Source of truth |
|---|---|---|---|
| Person | https://data.aesthetic.computer/@sat |
aesthetic.computer/@sat |
@handles, users |
| Painting | https://data.aesthetic.computer/painting/{code} |
aesthetic.computer/painting/{code} |
paintings (image at /media/@sat/painting/{slug}.png) |
| Piece (KidLisp/.mjs) | https://data.aesthetic.computer/piece/{code} |
aesthetic.computer/{$code} |
pieces / kidlisp |
| Mood | https://data.aesthetic.computer/mood/{handle}/{rkey} |
aesthetic.computer/moods~{handle}~{rkey} |
moods (atproto.rkey) |
Dereferencing rule on data.aesthetic.computer/{path}: default (or
Accept: application/ld+json, ?format=jsonld) → Linked Art JSON-LD;
Accept: text/html → a small human-readable view that also <link rel="seeAlso">s
the apex page. data.aesthetic.computer/sparql and /.well-known/void complete
the hub.
The JSON-LD examples in §4 still show
aesthetic.computer/...ids for readability — read them asdata.aesthetic.computer/...per this table.
4. Entity mapping — CIDOC CRM classes + Linked Art shape #
All four entities attribute back to the Person node, which is the hub.
4.1 Person — @sat → E21 Person #
{
"@context": "https://linked.art/ns/v1/linked-art.json",
"id": "https://aesthetic.computer/@sat",
"type": "Person",
"_label": "@sat",
"identified_by": [
{ "type": "Name", "content": "@sat",
"classified_as": [{ "id": "http://vocab.getty.edu/aat/300404670", "_label": "primary name" }] }
],
"subject_of": [
{ "type": "LinguisticObject", "_label": "AC profile",
"digitally_carried_by": [{ "type": "DigitalObject",
"access_point": [{ "id": "https://aesthetic.computer/@sat" }] }] }
]
}
- Latest mood can ride along as a
subject_ofLinguisticObject. - No email, Auth0 sub, or
verificationsdata is ever emitted. Only the public handle and color identity.
4.2 Painting → E22 Human-Made Object / D1 Digital Object #
Production event E12 Production carries the authorship + time-span.
{
"@context": "https://linked.art/ns/v1/linked-art.json",
"id": "https://aesthetic.computer/painting/Abc123",
"type": "DigitalObject",
"_label": "painting Abc123 by @sat",
"classified_as": [
{ "id": "http://vocab.getty.edu/aat/300033973", "_label": "paintings (visual works)" },
{ "id": "http://vocab.getty.edu/aat/300312038", "_label": "digital images" }
],
"produced_by": {
"type": "Production",
"carried_out_by": [{ "id": "https://aesthetic.computer/@sat", "type": "Person" }],
"timespan": { "type": "TimeSpan", "begin_of_the_begin": "2026-03-15T00:00:00Z" },
"took_place_at": [{ "id": "https://aesthetic.computer", "type": "Place", "_label": "Aesthetic Computer" }]
},
"subject_to": [{ "type": "Right",
"classified_as": [{ "id": "https://creativecommons.org/licenses/by/4.0/", "_label": "CC BY 4.0" }] }],
"digitally_shown_by": [{ "type": "DigitalObject", "format": "image/png",
"access_point": [{ "id": "https://aesthetic.computer/media/@sat/painting/Abc123.png" }] }]
}
Source fields: paintings.code, .slug, .when, .user (→ resolved to handle).
4.3 Piece (KidLisp / .mjs) → E73 Information Object + E65 Creation #
Generative-art provenance — a novel, valuable contribution to the CRM graph.
The source code itself is a LinguisticObject; the running program is the
DigitalObject. Hash gives content-addressed identity.
{
"@context": "https://linked.art/ns/v1/linked-art.json",
"id": "https://aesthetic.computer/$cow",
"type": "DigitalObject",
"_label": "KidLisp piece $cow by @sat",
"classified_as": [
{ "id": "http://vocab.getty.edu/aat/300265727", "_label": "software" },
{ "id": "http://vocab.getty.edu/aat/300047090", "_label": "generative art" }
],
"created_by": {
"type": "Creation",
"carried_out_by": [{ "id": "https://aesthetic.computer/@sat", "type": "Person" }],
"timespan": { "type": "TimeSpan", "begin_of_the_begin": "2026-03-15T00:00:00Z" }
},
"identified_by": [{ "type": "Identifier", "content": "<sha256>",
"classified_as": [{ "id": "http://vocab.getty.edu/aat/300435704", "_label": "checksum" }] }],
"subject_to": [{ "type": "Right",
"classified_as": [{ "id": "https://creativecommons.org/publicdomain/zero/1.0/", "_label": "CC0" }] }],
"carries": [{ "type": "LinguisticObject", "content": "(wipe blue) (ink red) ...",
"classified_as": [{ "id": "http://vocab.getty.edu/aat/300028676", "_label": "source code" }] }]
}
Source fields: pieces.code, .source, .hash, .when, .user, .trustLevel
(only trusted pieces broadcast — natural spam gate).
4.4 Mood → E33 Linguistic Object + E65 Creation #
Easiest map; already has atproto.rkey permalink and a Bluesky cross-ref we
can surface as equivalent/subject_of.
{
"@context": "https://linked.art/ns/v1/linked-art.json",
"id": "https://aesthetic.computer/moods~sat~3k...",
"type": "LinguisticObject",
"_label": "mood by @sat",
"classified_as": [{ "id": "http://vocab.getty.edu/aat/300026032", "_label": "statements" }],
"content": "feeling generative today",
"language": [{ "id": "http://vocab.getty.edu/aat/300388277", "_label": "English" }],
"created_by": {
"type": "Creation",
"carried_out_by": [{ "id": "https://aesthetic.computer/@sat", "type": "Person" }],
"timespan": { "type": "TimeSpan", "begin_of_the_begin": "2026-03-15T00:00:00Z" }
},
"subject_of": [{ "type": "DigitalObject",
"access_point": [{ "id": "at://did:plc:.../app.bsky.feed.post/..." }] }]
}
Source fields: moods.mood, .when, .user, .atproto.rkey, .bluesky.uri.
Skip deleted:true.
5. Rights — open records by default, In Copyright (the museum model) #
The work shown here is already public on aesthetic.computer; a linked-data record is a catalog entry describing it (authorship, date, type, image link) — exactly what museums publish for living artists' in-copyright work. So the model is open by default, not an opt-in allowlist:
- Every public handle's records are live, marked In Copyright
(
http://rightsstatements.org/vocab/InC/1.0/— the Europeana/DPLA standard): the record is open + citable, but copyright and the right to sell stay with the artist. This is what Getty pipelines need (a rights statement) without us granting reuse rights on anyone's behalf. @handles.linkedData = { enabled, license, optInAt }is now an upgrade / opt-out flag, not a gate:enabled === false→ excluded entirely (opt-out).licenseis a CC key (CC-BY-4.0,CC0-1.0, …) → that open license.- otherwise (default) → In Copyright.
- Why not default to CC-BY: CC-BY grants third parties commercial reuse, which would undercut artists' own ability to sell. In Copyright preserves it.
- Privacy floor: never emit email, Auth0
sub, IP/boot telemetry, chat, orverifications. Only public handle + the four content types above.
A prompt-driven command (linkdata cc-by / linkdata off) is the natural
self-serve toggle later; for now it's set by hand in @handles.
6. The data.aesthetic.computer subdomain — wired like at.aesthetic.computer #
The AT subdomain is the exact template (three layers):
- Landing page —
at/landing-page.htmlis a human-facing "here's what we offer" page (mission + top users + media feed). LOD gets the siblingcrm/landing-page.html→ served as staticrootfordata.aesthetic.computer/: what linked open data is, the license, a live SPARQL box, and an example Getty-style federated query. - Backend service reverse-proxied behind it — the knot does
reverse_proxy localhost:5555(at/knot/infra/Caddyfile). LOD does the same for Oxigraph:data.aesthetic.computer/sparql → reverse_proxy localhost:7878, and/painting/* /piece/* /mood/* /@*→reverse_proxy localhost:8888(thecrm.mjsserializer on lith). - Cloudflare DNS record + a
lith/Caddyfilehost block. Newdatarecord in theaesthetic.computerzone (add tolith/DNS.md), and a block mirroring the existing@give/@paperspattern:
@data host data.aesthetic.computer
handle @data {
handle /sparql* { reverse_proxy localhost:7878 } # Oxigraph
handle /.well-known/void { reverse_proxy localhost:8888 }
handle /painting/* { reverse_proxy localhost:8888 } # crm.mjs serializer
handle /piece/* { reverse_proxy localhost:8888 }
handle /mood/* { reverse_proxy localhost:8888 }
handle /@* { reverse_proxy localhost:8888 }
handle { # static landing
root * /opt/ac/system/public/data.aesthetic.computer
file_server
}
}
Stage 1 only needs layers 1 + 3 + the localhost:8888 routes (no Oxigraph);
Stage 2 adds the localhost:7878 proxy. Same staging as below.
7. Architecture — staged rollout #
Stage 1 — Subdomain + content negotiation → Linked Art (cheapest, no triple store) #
dataDNS record +@dataCaddy block (§6) + staticcrm/landing-page.html.- New function
system/netlify/functions/crm.mjs(routes/painting/{code},/piece/{code},/mood/{handle}/{rkey},/@{handle}) + a serializer libsystem/backend/linked-art.mjs(paintingToLinkedArt,pieceToLinkedArt,moodToLinkedArt,personToLinkedArt). - Reuses current Mongo indexes (
paintings.code,pieces.code,moods.atproto.rkey). Pure serializer over data we already have. No triple store, no new process — just lith. - Validate output against linked.art tooling + a JSON-LD playground.
Stage 2 — SPARQL endpoint ✅ LIVE #
- Oxigraph v0.5.9 (single Rust
binary at
/opt/oxigraph) runs on lith bound to127.0.0.1:7878underoxigraph.service; Caddy proxies onlydata.aesthetic.computer/sparql → /query(read-only —/update+/storeare never exposed; verified 404 publicly). - ETL (
crm/build-graph.mjs): walks MongoDB → runs every entity through the same serializers → expands to CIDOC CRM N-Triples viardf.mjs(bundled linked.art context, offline) → atomic Graph-Store PUT replacing the default graph.oxigraph-sync.timerrebuilds every 30 min. Provisioned bycrm/deploy-sparql.fish. (A full rebuild is ~15 min for ~7.7k entities; a_firehose-driven incremental update is a future optimization.) - Verified live: 163,994 triples; authorship exact vs. Mongo ground truth
(@jeffrey → 409 paintings = 409; distinct productions 2238 = handled-user
paintings 2238). Example query on the landing page + VoID
void:sparqlEndpoint. - Two bugs found + fixed during rollout: (1)
jsonld.toRDFrestarts blank labels at_:b0per call → concatenation merged every work's production/ time-span into one node → skolemize per doc; (2) the Mongo driver kept the event loop alive so the oneshot never exited (hungactivating, blocking the timer) →process.exit(0)on success.
Stage 3 — Discovery & images #
- Sitemap of entity URIs;
robots/llms.txtnote pointing crawlers to the LOD. - IIIF Image API over the painting CDN — Getty's image tooling is IIIF-native; this is high-leverage for paintings specifically.
- Optional: OAI-PMH feed for harvesters; federated-query examples in docs.
8. Decisions #
Resolved:
- Domain →
data.aesthetic.computer(mirrorsdata.getty.edu). ✓ - Store → Oxigraph on lith (
localhost:7878). ✓ - AAT terms → all verified against the live Getty SPARQL endpoint
(vocab.getty.edu) 2026-06-29. The first draft was almost entirely wrong
(
300265727"software" was actually wanted posters); corrected ids are now inlinked-art.mjsand asserted in the test. No Getty term exists for "source code" or "checksum" — those carry unclassified. ✓
Still open:
- v1 allowlist vs. self-serve — curated opt-in to start (per-handle
@handles.linkedData, set by hand like now), open alinkdataprompt command later for self-serve. - License menu — which CC licenses to offer; default per entity type
(paintings CC BY, pieces CC0?).
@jeffreyis currently all CC-BY-4.0.
9. Build order #
- ✅ Stage 0 + serializer lib —
linkedDataconsent gate (incrm.mjs) +linked-art.mjswith all four mappers +node --testfixtures (6 passing). - ✅ Stage 1 —
@dataCaddy block + lith host rewrite +crm.mjsserving Linked Art + inline landing + VoID. Pending: Cloudflaredatarecord + deploy + external validation against linked.art tooling. - ✅ Stage 2 — Oxigraph on lith (
127.0.0.1:7878) +crm/build-graph.mjsETL + 30-minoxigraph-sync.timer+ read-only/sparql+void:sparqlEndpoint. - ✅ Stage 3 —
iiif.mjsIIIF Presentation v3 Manifest + Image Level-0info.jsonper painting (dims from the PNG header; validates okay=0-errors on the official IIIF validator) +/sitemap.xml(7721 URIs) + Getty-federated query example. Landing redesigned (PALS beacon, "data" corner, auto light/dark, auto-scroll slideshow, Access Points as the pitch, footer link-backs).dataroutes from the AC prompt. All 2929 handles included (0 opt-outs).