Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
681 changes: 681 additions & 0 deletions docs/explorations/0319_[x]_BLOG_POST_LEGO_FOR_ALL_DATA_ON_THE_WEB.md

Large diffs are not rendered by default.

37 changes: 37 additions & 0 deletions site/public/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,29 @@ Without writing a single line of backend code, you got:
- src/ - schema.ts defineSchema() definitions - App.tsx XNetProvider - TaskList.tsx useQuery +
useMutate

## Go multiplayer in two commands

Everything above is one keystroke away from being a shared app. Point the
provider at a sync hub and give everyone the same room string — whoever
shares the room sees the same data:

```tsx

```

To run your own hub locally (dev only — auth off accepts anyone):

```bash
docker run --rm -p 4444:4444 -e HUB_AUTH=false ghcr.io/crs48/xnet-hub:latest
```

Open the app in two browser windows — changes appear in both. The room string
is the entire join flow: put it in a URL (`?room=standup`) and the URL becomes
an invitation. The smallest complete example of this is
[`examples/minimal-app`](https://github.com/crs48/xNet/tree/main/examples/minimal-app)
in the repo (~60 lines, copy-paste ready), and you can watch it run live on
the [demos page](/demos/).

## Next steps


Expand Down Expand Up @@ -7660,6 +7683,20 @@ reports upstream — the sender's DID is hashed (never sent raw) and document
content is never included. Leave them unset (the default) and nothing is
forwarded; `GET /diagnostics/health` simply reports `sharing: false`.

The upstream sink is xNet Cloud's first-party `POST /diagnostics` ingest
(exploration 0315): reports land in a quarantine and are drained into
`debug-report` nodes in the operator's own xNet workspace, where the workbench
_is_ the triage console — filterable tables, a `new → acked → fixed → released`
status field, and comments. **xNet does not use a third-party error service such
as Sentry.** The client keeps a dormant, vendor-neutral Sentry seam (no
`@sentry/*` SDK is installed and no DSN is minted), so if you self-host a
Sentry-compatible backend you can point that seam at it — [Bugsink][bugsink]
(single container, SQLite) or [GlitchTip][glitchtip] (MIT, Sentry-SDK
compatible) are the proven small-footprint options — but nothing ships enabled.

[bugsink]: https://www.bugsink.com/
[glitchtip]: https://glitchtip.com/

### CLI Flags

| Flag | Description |
Expand Down
186 changes: 186 additions & 0 deletions site/src/components/blog/BrickArt.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
---
// Decorative art for blog post #15 (exploration 0319), extracted so the same
// inline SVG backs both the post hero and the blog-index preview card (no
// external assets — Self-Audit parity). The scene draws the essay's argument:
// on the left, the silo era — two sealed boxes whose connectors don't match
// (a triangle socket, a round socket), each with a few dim points locked
// inside. On the right, one baseplate: a ground plane of studs on which
// bricks of different shapes all snap, because the coupling is shared. One
// brick hovers just above the plate, mid-lift — the grip lets go; that's the
// point. Another sits dashed with a keyhole: present, but it won't snap for
// you without the key. The cosmic-X hangs overhead as the reading lamp, as in
// the rest of the series. All geometry original; no LEGO trade dress.
interface Props {
/** Override the wrapper classes; defaults to full-bleed for hero use. */
class?: string
}
const { class: className = 'pointer-events-none absolute inset-0 h-full w-full' } =
Astro.props

// The baseplate stud row (x positions along the plate's top edge).
const studCols = Array.from({ length: 15 }, (_, i) => 448 + i * 34)

// Studs on top of a brick: n studs across a brick starting at x with width w.
const brickStuds = (x: number, w: number, n: number) =>
Array.from({ length: n }, (_, i) => x + (w / (n * 2)) * (2 * i + 1))

// The dotted footpath from the silos out to the baseplate.
const path = [
[300, 300],
[348, 294],
[396, 296],
[436, 306]
]
.map(([x, y]) => `${x},${y}`)
.join(' ')
---

<svg
class={className}
viewBox="0 0 1040 340"
preserveAspectRatio="xMidYMid slice"
aria-hidden="true"
>
<defs>
<linearGradient id="brickbg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#0a1122" />
<stop offset="55%" stop-color="#0d1526" />
<stop offset="100%" stop-color="#080d1a" />
</linearGradient>
<radialGradient id="bricklampglow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#7dd3fc" stop-opacity="0.95" />
<stop offset="45%" stop-color="#38bdf8" stop-opacity="0.38" />
<stop offset="100%" stop-color="#38bdf8" stop-opacity="0" />
</radialGradient>
<linearGradient id="bricklampx" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#e0f2fe" />
<stop offset="100%" stop-color="#38bdf8" />
</linearGradient>
<linearGradient id="baseplateglow" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#38bdf8" stop-opacity="0.16" />
<stop offset="100%" stop-color="#38bdf8" stop-opacity="0" />
</linearGradient>
<linearGradient id="silosteel" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#1d2a44" />
<stop offset="100%" stop-color="#131d33" />
</linearGradient>
</defs>

<rect width="1040" height="340" fill="url(#brickbg)" />

<!-- ── the cosmic-X reading lamp, top-right, clear of the title block ── -->
<circle cx="880" cy="58" r="60" fill="url(#bricklampglow)" />
<g stroke="url(#bricklampx)" stroke-width="7" stroke-linecap="round">
<line x1="863" y1="41" x2="897" y2="75" />
<line x1="897" y1="41" x2="863" y2="75" />
</g>

<!-- ── left: two silo boxes whose connectors don't match ────────────── -->
<!-- silo A: lidded box with a triangle socket -->
<g>
<rect x="70" y="150" width="150" height="130" rx="10" fill="url(#silosteel)" stroke="#2b3b5e" stroke-width="3" />
<rect x="58" y="128" width="174" height="30" rx="8" fill="#101a2f" stroke="#2b3b5e" stroke-width="2.5" />
<!-- its proprietary connector: a triangle socket on the lid -->
<polygon points="145,150 131,136 159,136" fill="#0b1322" stroke="#466195" stroke-width="2" />
<!-- dim points locked inside -->
<g fill="#38bdf8" opacity="0.3">
<circle cx="112" cy="206" r="4" />
<circle cx="145" cy="222" r="4" />
<circle cx="178" cy="204" r="4" />
</g>
</g>
<!-- silo B: lidded box with a round socket — it will never join silo A -->
<g>
<rect x="196" y="196" width="122" height="84" rx="10" fill="url(#silosteel)" stroke="#2b3b5e" stroke-width="3" />
<rect x="186" y="176" width="142" height="26" rx="7" fill="#101a2f" stroke="#2b3b5e" stroke-width="2.5" />
<circle cx="257" cy="189" r="9" fill="#0b1322" stroke="#466195" stroke-width="2" />
<g fill="#38bdf8" opacity="0.3">
<circle cx="230" cy="240" r="4" />
<circle cx="262" cy="252" r="4" />
<circle cx="292" cy="238" r="4" />
</g>
</g>
<!-- the failed join: a faint × where the two connectors face each other -->
<g stroke="#64748b" stroke-opacity="0.55" stroke-width="2.4" stroke-linecap="round">
<line x1="166" y1="168" x2="182" y2="184" />
<line x1="182" y1="168" x2="166" y2="184" />
</g>
<!-- ground shadow -->
<ellipse cx="196" cy="304" rx="150" ry="9" fill="#060b16" />

<!-- ── the footpath from the silos to the shared coupling ───────────── -->
<polyline
points={path}
fill="none"
stroke="#38bdf8"
stroke-opacity="0.45"
stroke-width="2.5"
stroke-dasharray="1 10"
stroke-linecap="round"
/>

<!-- ── right: one baseplate, every brick snaps ────────────────────────
Everything left of x≈700 stays low and dim (the title/deck sits over
it); the taller stack and the lifted brick live in the clear right
column, mirroring VaultArt's layout. -->
<rect x="420" y="240" width="520" height="70" fill="url(#baseplateglow)" />
<!-- the plate itself -->
<rect x="432" y="298" width="496" height="18" rx="5" fill="#101a2f" stroke="#38bdf8" stroke-opacity="0.55" stroke-width="1.6" />
<!-- its stud row, along the top edge -->
<g fill="#7dd3fc" opacity="0.45">
{studCols.map((x) => <circle cx={x} cy="294" r="2.6" />)}
</g>

<!-- brick 1: wide and low, seated on the plate (dim — prose sits above) -->
<g>
<rect x="452" y="272" width="120" height="26" rx="6" fill="#0d1729" stroke="#38bdf8" stroke-opacity="0.4" stroke-width="1.6" />
<g fill="#7dd3fc" opacity="0.5">
{brickStuds(452, 120, 4).map((x) => <circle cx={x} cy="272" r="3.6" />)}
</g>
</g>
<!-- brick 2: dashed with a keyhole — visible, but it won't snap without the key -->
<g>
<rect
x="600"
y="270"
width="88"
height="28"
rx="6"
fill="none"
stroke="#64748b"
stroke-opacity="0.6"
stroke-width="1.6"
stroke-dasharray="5 5"
/>
<g fill="none" stroke="#64748b" stroke-opacity="0.6" stroke-width="1.6">
{brickStuds(600, 88, 3).map((x) => <circle cx={x} cy="270" r="3.6" />)}
</g>
<!-- keyhole -->
<circle cx="644" cy="281" r="4.2" fill="none" stroke="#94a3b8" stroke-width="1.6" />
<line x1="644" y1="285" x2="644" y2="292" stroke="#94a3b8" stroke-width="1.6" stroke-linecap="round" />
</g>
<!-- brick 3: mid-lift above the plate — the grip lets go by hand -->
<g>
<rect x="712" y="238" width="88" height="28" rx="6" fill="#0d1729" stroke="#7dd3fc" stroke-opacity="0.5" stroke-width="1.8" />
<g fill="#7dd3fc" opacity="0.6">
{brickStuds(712, 88, 3).map((x) => <circle cx={x} cy="238" r="4.2" />)}
</g>
<!-- separation marks between brick and plate -->
<g stroke="#38bdf8" stroke-opacity="0.28" stroke-width="1.8" stroke-linecap="round" stroke-dasharray="2 7">
<line x1="728" y1="274" x2="728" y2="292" />
<line x1="756" y1="274" x2="756" y2="292" />
<line x1="784" y1="274" x2="784" y2="292" />
</g>
</g>
<!-- bricks 4+5: a stack seated in the clear right column — data on data -->
<g>
<rect x="824" y="266" width="100" height="32" rx="6" fill="#0d1729" stroke="#38bdf8" stroke-opacity="0.5" stroke-width="1.8" />
<g fill="#7dd3fc" opacity="0.6">
{brickStuds(824, 100, 3).map((x) => <circle cx={x} cy="266" r="4.2" />)}
</g>
<rect x="848" y="238" width="54" height="28" rx="6" fill="#0d1729" stroke="#38bdf8" stroke-opacity="0.5" stroke-width="1.8" />
<g fill="#7dd3fc" opacity="0.6">
{brickStuds(848, 54, 2).map((x) => <circle cx={x} cy="238" r="4.2" />)}
</g>
</g>
</svg>
38 changes: 38 additions & 0 deletions site/src/components/blog/BrickHero.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
// Hero band for blog post #15 (exploration 0319). The decorative art lives in
// BrickArt.astro so the same inline SVG backs both this hero and the
// blog-index preview card; this component layers the post's title/deck over it.
import BrickArt from './BrickArt.astro'

interface Props {
title: string
deck: string
date: string
readingMinutes: number
tags: string[]
}
const { title, deck, date, readingMinutes, tags } = Astro.props
---

<section class="relative overflow-hidden border-b border-border bg-surface/20 dark:bg-[#080d1a]">
<BrickArt />

<div class="relative mx-auto max-w-3xl px-6 pb-14 pt-44 lg:pt-52">
<div class="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-sky-200/80">
<time datetime={date}>{date}</time>
<span aria-hidden="true">·</span>
<span>{readingMinutes} min read</span>
{
tags.map((tag) => (
<span class="rounded-full border border-sky-400/30 bg-sky-500/10 px-2 py-0.5 font-mono lowercase text-sky-200">
{tag}
</span>
))
}
</div>
<h1 class="mt-4 text-4xl font-bold tracking-tight text-white drop-shadow lg:text-5xl">
{title}
</h1>
<p class="mt-4 max-w-2xl text-lg leading-relaxed text-gray-300">{deck}</p>
</div>
</section>
64 changes: 64 additions & 0 deletions site/src/components/blog/HonestBrick.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
// The honesty beat for post #15 (exploration 0319), modeled on HonestVault /
// HonestWorkshop. An essay that says "one coupling for all your data, and you
// decide who plays" owes the reader the six places where the metaphor stops
// working — starting with the one no architecture can fix: a grip is not a
// weld, and reading is copying. The rows concede the strongest standing
// objections rather than dodging them.
const rows = [
{
isnt: 'We won’t pretend revocation can reach into the past.',
is: 'Once someone has read a piece of data, they have it — decryption is copying, and no protocol can un-show something. Revoking a grant stops every future change from reaching them; it cannot repossess what they already saw. Anything that promised more would be DRM wearing a nicer coat, and the grip is deliberately not a weld.'
},
{
isnt: 'We won’t pretend a shared namespace makes people agree.',
is: 'Two apps sharing a Contact still have to agree on what a contact is, and that agreement is social, not technical — the Semantic Web spent a decade proving it. Lenses, overlays, and sidecars make disagreement cheap enough to live with; they don’t make coordination free. Anyone can mint a schema, which also means anyone can mint a duplicate.'
},
{
isnt: 'We won’t pretend a frozen interface maintains itself.',
is: 'LEGO froze one moulding geometry and iterates everything else; a protocol pays for that promise continuously. When we hardened one detail of our merge rule, the change rippled through every conformance kernel and golden vector we keep. Backwards compatibility is a standing engineering bill, not a patent you file once.'
},
{
isnt: 'We won’t pretend a general-purpose substrate is fast for free.',
is: 'A silo optimises for exactly one access pattern; a store that must answer any question about any shape pays a tax the silo never sees. Making the open substrate feel app-fast has taken months of unglamorous query and hydration work, and that ledger is still open.'
},
{
isnt: 'We won’t pretend nobody has to run anything.',
is: 'Moxie Marlinspike is right that people do not want to run their own servers. Your replica is primary and local, and peers can sync directly — but an always-reachable relay is what makes sharing feel instant, and someone hosts and pays for it. Ours is optional and replaceable; it is not imaginary.'
},
{
isnt: 'We won’t pretend permissions solve moderation or discovery.',
is: 'Access control decides who may read and write; it says nothing about what deserves an audience. Spaces give every community its own table and its own rules, which works at the scale of a table — network-scale discovery, spam, and abuse in a world with no central operator remain honestly unsolved, here and everywhere.'
}
]
---

<section class="not-prose my-12">
<div class="rounded-2xl border border-border bg-surface/30 p-6 dark:bg-surface/40 lg:p-8">
<h3 class="text-xl font-bold tracking-tight text-gray-900 dark:text-white">
Where the metaphor stops
</h3>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
Six things a universal coupling cannot do, conceded in plain sight —
because the essays that skip this section are the ones you were right
not to trust.
</p>

<ul class="mt-6 space-y-5">
{
rows.map((row) => (
<li class="grid gap-3 sm:grid-cols-[1fr_1.3fr] sm:gap-5">
<div class="flex gap-2.5">
<span class="select-none text-gray-400 dark:text-gray-600">✕</span>
<p class="text-sm leading-relaxed text-gray-500 dark:text-gray-400">{row.isnt}</p>
</div>
<div class="flex gap-2.5">
<span class="select-none text-sky-500">✓</span>
<p class="text-sm leading-relaxed text-gray-700 dark:text-gray-200">{row.is}</p>
</div>
</li>
))
}
</ul>
</div>
</section>
18 changes: 18 additions & 0 deletions site/src/data/blog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,24 @@ export interface BlogPost {
}

const posts: BlogPost[] = [
{
slug: 'clutch-power',
title: 'Clutch Power',
description:
'On 28 January 1958 the LEGO Group patented not a brick but a ' +
'coupling — stud and tube, and with them clutch power: a grip firm ' +
'enough to build with that still comes apart by hand. The web never ' +
'got a coupling for data, so every app moulds pieces that fit only ' +
'its own set, and the APIs that promised otherwise were drawbridges. ' +
'On the four frozen interfaces xNet ships instead — one node shape, ' +
'one namespace anyone can mint into, one merge rule, one permission ' +
'algebra — and why the grip matters as much as the snap: nobody ' +
'plays with your bricks unless you say so.',
pubDate: '2026-07-14T17:00:00Z',
authors: ['crs48', 'claude'],
tags: ['essay', 'protocol', 'decentralization', 'philosophy'],
readingMinutes: 14
},
{
slug: 'weights-you-can-hold',
title: 'Weights You Can Hold',
Expand Down
10 changes: 10 additions & 0 deletions site/src/data/changelog/2026-07-14-new-essay-clutch-power.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"id": "2026-07-14-new-essay-clutch-power",
"date": "July 14, 2026",
"title": "New essay: Clutch Power",
"summary": "Blog post #15 frames xNet as LEGO for data on the web: one coupling — node shape, namespace, merge rule, permission algebra — and why the grip that lets go matters as much as the snap.",
"highlights": [],
"tags": [
"platform"
]
}
Loading
Loading