Artifact a5f5c43b29d0d6978635c273b7d7f1c2b72a0005dfd4be66616934d662f56d14:
- File
docs/site/layouts/index.html
— part of check-in
[d59e2dece9]
at
2026-04-26 15:20:20
on branch trunk
— Merge pull request #78 from danmestas/add-docs-site
docs: Hugo+hextra docs site with Cloudflare Workers deploy (user: [email protected], size: 24137) [annotate] [blame] [check-ins using]
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="description" content="EdgeSync — NATS-native sync engine for Fossil SCM repositories. Mesh topology, optional iroh tunneling, bidirectional messaging built in."> <meta name="color-scheme" content="light dark"> <title>EdgeSync — NATS-native Fossil sync engine</title> <link rel="icon" type="image/svg+xml" href="/favicon.svg"> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Geist+Mono:wght@400;500;700&display=swap" rel="stylesheet"> <style> :root { --paper: #f5f1e6; --paper-2:#ece7d8; --ink: #14110b; --mute: #5e5b53; --rule: #14110b; --ember: #c63a0f; --measure: 76ch; --gap: clamp(2.25rem, 5vw, 3.5rem); --mono: "Geist Mono", ui-monospace, "SF Mono", "Cascadia Mono", "Roboto Mono", Consolas, "Courier New", monospace; } *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } html { font-size: 16px; -webkit-text-size-adjust: 100%; } body { background: var(--paper); color: var(--ink); font: 400 clamp(15px, 1.05vw, 17px)/1.7 var(--mono); font-feature-settings: "tnum" 1, "calt" 1, "ss01" 1; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; } .skip { position: absolute; left: -9999px; } .skip:focus { position: fixed; top: 0.75rem; left: 0.75rem; background: var(--ember); color: var(--paper); padding: 0.5rem 0.75rem; z-index: 10; text-decoration: none; } header.topbar { border-bottom: 1px solid var(--rule); padding: clamp(0.625rem, 2.5vw, 0.75rem) clamp(1rem, 4vw, 1.5rem); display: flex; justify-content: space-between; align-items: baseline; gap: 1rem; flex-wrap: wrap; font-size: 0.78125rem; letter-spacing: 0.04em; } .topbar a { color: inherit; text-decoration: none; border-bottom: 1px solid currentColor; } .topbar a:hover { color: var(--ember); } .topbar .name { font-weight: 700; letter-spacing: 0.18em; text-transform: uppercase; } .topbar .meta { color: var(--mute); display: flex; gap: 1.25rem; flex-wrap: wrap; } .topbar .meta span { display: inline-flex; gap: 0.4rem; } .topbar .meta b { color: var(--ink); font-weight: 500; } main { max-width: 88ch; margin: 0 auto; padding: clamp(2rem, 4vw, 3rem) clamp(1rem, 4vw, 1.5rem) clamp(3rem, 6vw, 4.5rem); overflow-wrap: anywhere; } .title { display: grid; grid-template-columns: 1fr; gap: 1.25rem; padding-bottom: clamp(2rem, 4vw, 2.75rem); margin-bottom: clamp(2rem, 4vw, 3rem); border-bottom: 1px solid var(--rule); } .title h1 { font-size: clamp(2.75rem, 13vw, 6.5rem); font-weight: 700; letter-spacing: -0.05em; line-height: 0.92; } .title .lead { font-size: clamp(1rem, 1.2vw, 1.0625rem); max-width: var(--measure); color: var(--ink); line-height: 1.6; } .spec { width: 100%; border-collapse: collapse; font-size: 0.78125rem; margin-top: 0.5rem; letter-spacing: 0.02em; } .spec td { padding: 0.35rem 0; border-top: 1px dashed var(--rule); vertical-align: top; } .spec td:first-child { width: 12rem; color: var(--mute); text-transform: uppercase; letter-spacing: 0.14em; font-size: 0.6875rem; padding-top: 0.5rem; } .spec tr:first-child td { border-top: 1px solid var(--rule); } .spec tr:last-child td { border-bottom: 1px solid var(--rule); } section { margin-top: var(--gap); } section > .num { display: block; font-size: 0.6875rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--mute); margin-bottom: 0.4rem; } h2 { font-size: clamp(1.125rem, 1.6vw, 1.3125rem); font-weight: 700; letter-spacing: -0.01em; margin-bottom: 1.25rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--rule); display: flex; justify-content: space-between; align-items: baseline; gap: 1rem; flex-wrap: wrap; } h2 .anchor { font-size: 0.6875rem; font-weight: 400; color: var(--mute); letter-spacing: 0.14em; text-transform: uppercase; } p { max-width: var(--measure); margin-bottom: 1rem; } p:last-child { margin-bottom: 0; } strong { font-weight: 700; } a { color: var(--ink); text-decoration: none; border-bottom: 1px solid var(--ink); } a:hover { color: var(--ember); border-bottom-color: var(--ember); } code { font: inherit; font-size: 0.94em; background: var(--paper-2); padding: 0.05em 0.35em; } ol, ul { padding-left: 0; max-width: var(--measure); list-style: none; } ol li, ul li { margin-bottom: 0.875rem; padding-left: 2.75rem; position: relative; line-height: 1.65; } ol { counter-reset: item; } ol li { counter-increment: item; } ol li::before { content: counter(item, decimal-leading-zero); position: absolute; left: 0; top: 0.05em; font-weight: 700; font-size: 0.78125rem; letter-spacing: 0.04em; color: var(--ember); width: 2rem; } ul li::before { content: "—"; position: absolute; left: 0.5rem; color: var(--mute); } .code-block { margin-bottom: 1rem; border: 1px solid var(--rule); background: var(--paper-2); } .code-bar { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; padding: 0.35rem 0.5rem 0.35rem 0.85rem; border-bottom: 1px solid var(--rule); background: var(--paper); font-size: 0.625rem; letter-spacing: 0.16em; text-transform: uppercase; color: var(--mute); } pre { padding: 0.875rem 1.25rem; font-size: 0.85rem; line-height: 1.65; overflow-x: auto; white-space: pre; font-family: var(--mono); color: var(--ink); } pre.ascii { background: transparent; border: 1px solid var(--rule); padding: clamp(0.75rem, 2.5vw, 1.25rem); font-size: clamp(0.7rem, 1.7vw, 0.8125rem); line-height: 1.45; color: var(--ink); margin-bottom: 1rem; overflow-x: auto; } .diagram { display: block; width: 100%; min-width: 30rem; height: auto; margin-bottom: 0.5rem; font-family: var(--mono); color: var(--ink); } .diagram .rule { stroke: var(--rule); stroke-width: 1; opacity: 0.5; } .diagram .box { fill: none; stroke: var(--rule); stroke-width: 1; } .diagram .head { font-size: 11px; letter-spacing: 1.4px; text-transform: uppercase; fill: var(--mute); } .diagram .item { font-size: 13px; fill: var(--ink); } .diagram .stack { font-size: 12px; fill: var(--mute); font-style: italic; } .diagram .flow line { stroke: var(--ember); stroke-width: 1.5; fill: none; } .diagram .arrowhead { fill: var(--ember); } .diagram .flow-label { font-size: 10px; fill: var(--ember); letter-spacing: 0.6px; text-transform: uppercase; } .diagram .peer-dot { fill: var(--ember); opacity: 0.8; } .diagram .mesh-link { stroke: var(--ember); stroke-width: 1; stroke-dasharray: 3 3; fill: none; opacity: 0.7; } .copy-btn { background: transparent; color: var(--ink); border: 1px solid var(--ink); font: inherit; font-size: 0.625rem; letter-spacing: 0.1em; text-transform: uppercase; padding: 0.15rem 0.55rem; cursor: pointer; flex-shrink: 0; } .copy-btn:hover, .copy-btn:focus-visible { background: var(--ink); color: var(--paper); outline: none; } .copy-btn[data-state="copied"] { color: var(--ember); border-color: var(--ember); background: var(--paper); } .table-scroll { margin: 0 calc(-1 * clamp(0rem, 2vw, 0.5rem)); overflow-x: auto; -webkit-overflow-scrolling: touch; } table.cmp { width: 100%; min-width: 30rem; border-collapse: collapse; font-size: 0.875rem; margin-bottom: 0.75rem; } table.cmp th, table.cmp td { text-align: left; padding: 0.625rem 0.875rem; vertical-align: top; border-bottom: 1px dashed var(--rule); } table.cmp thead th { border-bottom: 1px solid var(--rule); border-top: 1px solid var(--rule); font-size: 0.6875rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.14em; background: var(--paper-2); } table.cmp td:first-child, table.cmp th:first-child { padding-left: 0.875rem; width: 30%; font-weight: 500; color: var(--mute); text-transform: uppercase; letter-spacing: 0.08em; font-size: 0.75rem; } table.cmp td:last-child, table.cmp th:last-child { padding-right: 0.875rem; } table.cmp .col-self { color: var(--ember); font-weight: 700; } table.cmp tbody tr:last-child td { border-bottom: 1px solid var(--rule); } .fineprint { font-size: 0.75rem; color: var(--mute); margin-top: 0.5rem; letter-spacing: 0.02em; } footer { border-top: 1px solid var(--rule); margin-top: var(--gap); padding: 1.5rem 0; font-size: 0.78125rem; color: var(--mute); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 1rem; letter-spacing: 0.04em; } footer a { color: inherit; border-bottom-color: var(--mute); } @media (prefers-color-scheme: dark) { :root { --paper: #15140f; --paper-2:#1f1d17; --ink: #ece8dc; --mute: #908b80; --rule: #ece8dc; --ember: #ff5a26; } } @media print { body { background: white; color: black; font-size: 11pt; line-height: 1.5; } .code-block, pre { background: white; color: black; border: 1px solid black; } a { color: black; border-bottom: 0; text-decoration: underline; } a::after { content: " (" attr(href) ")"; font-size: 0.85em; color: #555; } .copy-btn, .skip { display: none; } section { break-inside: avoid; } } @media (max-width: 540px) { header.topbar { flex-direction: column; align-items: flex-start; gap: 0.5rem; } .topbar .meta { gap: 0.75rem; row-gap: 0.35rem; } .spec td:first-child { width: 8rem; font-size: 0.625rem; } .spec td { padding: 0.4rem 0; } table.cmp { font-size: 0.6875rem; min-width: 0; table-layout: auto; } table.cmp th, table.cmp td { padding: 0.45rem 0.4rem; } table.cmp tbody td { white-space: normal; word-break: normal; overflow-wrap: break-word; } table.cmp thead th { font-size: 0.5rem; letter-spacing: 0.06em; padding: 0.5rem 0.3rem; white-space: nowrap; } table.cmp td:first-child, table.cmp th:first-child { padding-left: 0.55rem; width: 28%; font-size: 0.5625rem; letter-spacing: 0.04em; } table.cmp td:last-child, table.cmp th:last-child { padding-right: 0.55rem; } ol li, ul li { padding-left: 2.25rem; } ol li::before { width: 1.75rem; } footer { flex-direction: column; gap: 0.5rem; align-items: flex-start; } .copy-btn { font-size: 0.5625rem; padding: 0.1rem 0.4rem; } pre { font-size: 0.78rem; padding: 0.75rem 0.9rem; } .code-bar { padding: 0.3rem 0.4rem 0.3rem 0.7rem; } } @media (max-width: 380px) { .topbar .name { letter-spacing: 0.12em; } .spec td:first-child { width: 7rem; } table.cmp { font-size: 0.625rem; } table.cmp th, table.cmp td { padding: 0.4rem 0.25rem; } table.cmp thead th { font-size: 0.4375rem; padding: 0.45rem 0.2rem; } table.cmp td:first-child, table.cmp th:first-child { font-size: 0.5rem; padding-left: 0.4rem; width: 26%; } } </style> </head> <body> <a href="#main" class="skip">skip to content</a> <header class="topbar"> <div class="name">edgesync</div> <div class="meta"> <span>v <b>{{ .Site.Params.version }}</b></span> <span>status <b>alpha</b></span> <span><a href="https://github.com/danmestas/EdgeSync">github</a></span> <span><a href="/docs/">docs</a></span> </div> </header> <main id="main"> <section class="title"> <h1>edgesync</h1> <p class="lead">A NATS-native sync engine for Fossil SCM repositories. Mesh topology. Optional iroh tunneling. Bidirectional messaging built in.</p> <table class="spec"> <tbody> <tr><td>Project</td> <td>edgesync — NATS-native Fossil sync engine</td></tr> <tr><td>Language</td> <td>Go (modules <code>github.com/danmestas/EdgeSync/{leaf,bridge}</code>)</td></tr> <tr><td>Built on</td> <td><a href="https://github.com/danmestas/libfossil">libfossil</a> · <a href="https://nats.io">NATS</a> embedded · <a href="https://iroh.computer">iroh</a> (optional)</td></tr> <tr><td>Binaries</td> <td>3 (edgesync · leaf · bridge)</td></tr> <tr><td>Status</td> <td>v{{ .Site.Params.version }} · alpha · pre-release · wire protocol stable, CLI may shift</td></tr> </tbody> </table> </section> <section> <span class="num">§01</span> <h2>Install <span class="anchor">install</span></h2> <div class="code-block"> <div class="code-bar"> <span>shell</span> <button type="button" class="copy-btn" data-copy="install" aria-label="Copy install command">copy</button> </div> <pre id="install">$ go install github.com/danmestas/EdgeSync/cmd/edgesync@latest $ edgesync doctor</pre> </div> <p>One static binary, no CGo. The <code>doctor</code> subcommand checks your environment (Go version, SQLite driver, NATS reachability) and prints a clean report.</p> </section> <section> <span class="num">§02</span> <h2>What it is <span class="anchor">overview</span></h2> <p>EdgeSync is a sync engine for <a href="https://fossil-scm.org">Fossil SCM</a> repositories that swaps Fossil's HTTP-based <code>/xfer</code> protocol for NATS messaging. The leaf agent reads and writes <code>.fossil</code> SQLite files via the <a href="https://github.com/danmestas/libfossil">libfossil</a> library, and exchanges blobs with peer agents over an embedded NATS mesh.</p> <p>There is no central server in the default topology — each agent runs its own NATS server in-process, and gossip happens leaf-to-leaf. An optional <strong>bridge</strong> process translates between NATS and HTTP <code>/xfer</code> for talking to unmodified upstream Fossil servers.</p> <ul> <li><strong>Mesh-first sync.</strong> Every leaf is both client and server. Add a peer with one CLI flag.</li> <li><strong>libfossil under the hood.</strong> <code>.fossil</code> files stay readable by the upstream <code>fossil</code> binary — nothing proprietary in the storage format.</li> <li><strong>Optional iroh tunneling.</strong> Two leaves behind separate NATs find each other over QUIC. No port forwarding, no relay infrastructure to operate.</li> <li><strong>Notify channel.</strong> A separate, project-scoped pub-sub for human-in-the-loop messaging. Pair a phone with a QR code, get push alerts on commits, deploys, or agent questions.</li> </ul> </section> <section> <span class="num">§03</span> <h2>Why you might want it <span class="anchor">why</span></h2> <p>Three shapes of problem where EdgeSync is the right tool.</p> <p><strong>You're running many small Fossil repos across edge devices.</strong> Robotics fleets, retail kiosks, CDN nodes, on-prem appliances — anywhere the topology is "many small sites, intermittent connectivity, no central server." EdgeSync's mesh-first design means a new device pairs with one peer and converges with the rest of the fleet from there.</p> <p><strong>You need bidirectional messaging alongside repo sync.</strong> Most version-control stacks bolt chat or notifications on as a separate service. EdgeSync ships <code>notify</code> as a first-class channel using the same NATS mesh: an agent commits and the human's phone gets a push, the human replies and the agent acts on it.</p> <p><strong>You want zero infrastructure.</strong> Embedded NATS server + iroh transport = no broker to operate, no central server to scale, no CDN to point clients at. Two laptops on a coffee-shop wifi can sync to each other directly.</p> </section> <section> <span class="num">§04</span> <h2>How it fits together <span class="anchor">architecture</span></h2> <div class="table-scroll"> <svg class="diagram" viewBox="0 0 640 280" role="img" aria-labelledby="es-arch-title es-arch-desc" preserveAspectRatio="xMidYMid meet"> <title id="es-arch-title">EdgeSync architecture</title> <desc id="es-arch-desc">Apps (cli, daemon, mobile) call the leaf agent, which embeds libfossil and a NATS server. The agent persists to a .fossil SQLite repo and exchanges sync rounds with peer leaves over NATS, optionally tunneled through iroh QUIC, with a bridge translating to HTTP for legacy Fossil servers.</desc> <defs> <marker id="es-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"> <path class="arrowhead" d="M0,0 L10,5 L0,10 Z"/> </marker> </defs> <g class="rule"> <line x1="40" y1="38" x2="160" y2="38"/> <line x1="240" y1="38" x2="400" y2="38"/> <line x1="480" y1="38" x2="600" y2="38"/> </g> <g class="head"> <text x="100" y="28" text-anchor="middle">apps</text> <text x="320" y="28" text-anchor="middle">leaf agent</text> <text x="540" y="28" text-anchor="middle">mesh</text> </g> <g class="item"> <text x="100" y="76" text-anchor="middle">cli</text> <text x="100" y="100" text-anchor="middle">daemon</text> <text x="100" y="124" text-anchor="middle">mobile</text> </g> <rect class="box" x="240" y="60" width="160" height="100"/> <g class="item"> <text x="320" y="88" text-anchor="middle">libfossil</text> <text x="320" y="112" text-anchor="middle">NATS server</text> <text x="320" y="136" text-anchor="middle">notify</text> </g> <rect class="box" x="490" y="60" width="100" height="100"/> <g class="item"> <text x="540" y="88" text-anchor="middle">peer</text> <text x="540" y="112" text-anchor="middle">iroh</text> <text x="540" y="136" text-anchor="middle">bridge</text> </g> <rect class="box" x="240" y="200" width="160" height="40"/> <g class="item"> <text x="320" y="225" text-anchor="middle">.fossil (SQLite)</text> </g> <g class="flow"> <line x1="160" y1="100" x2="234" y2="100" marker-end="url(#es-arrow)"/> <line x1="400" y1="88" x2="486" y2="88" marker-end="url(#es-arrow)"/> <line x1="490" y1="136" x2="404" y2="136" marker-end="url(#es-arrow)"/> <line x1="320" y1="160" x2="320" y2="196" marker-end="url(#es-arrow)"/> </g> <g class="flow-label"> <text x="197" y="92" text-anchor="middle">calls</text> <text x="443" y="80" text-anchor="middle">sync</text> <text x="447" y="154" text-anchor="middle">events</text> <text x="334" y="183" text-anchor="start">persist</text> </g> </svg> </div> <p class="fineprint">The leaf agent embeds <code>libfossil</code> (Fossil repo I/O), a full NATS server (peer/hub/leaf role configurable), and the notify pub-sub. Mesh column lists the three transport options — pick any combination per agent. Persistence is always a regular <code>.fossil</code> SQLite file readable by the upstream <code>fossil</code> binary.</p> </section> <section> <span class="num">§05</span> <h2>Compared to neighbors <span class="anchor">comparison</span></h2> <div class="table-scroll"> <table class="cmp"> <thead> <tr> <th></th> <th class="col-self">EdgeSync</th> <th>upstream fossil server</th> <th>git + chat stack</th> </tr> </thead> <tbody> <tr><td>Topology</td> <td class="col-self">mesh (peer/hub/leaf)</td> <td>client/server</td> <td>client/server</td></tr> <tr><td>NAT traversal</td> <td class="col-self">iroh QUIC (built-in)</td> <td>none</td> <td>depends on host</td></tr> <tr><td>Bidir messaging</td><td class="col-self">notify (built-in)</td> <td>forum/wiki (in-repo)</td> <td>separate service</td></tr> <tr><td>Storage format</td> <td class="col-self">.fossil (compatible)</td> <td>.fossil</td> <td>.git</td></tr> <tr><td>Static binary</td> <td class="col-self">yes</td> <td>yes</td> <td>varies</td></tr> <tr><td>Runtime deps</td> <td class="col-self">none</td> <td>none</td> <td>varies</td></tr> <tr><td>API stability</td> <td class="col-self">alpha</td> <td>stable</td> <td>stable</td></tr> </tbody> </table> </div> <p class="fineprint">EdgeSync is not a Fossil replacement — it reuses the <code>.fossil</code> format and the <code>/xfer</code> wire protocol via libfossil. The comparison above is about <em>operating shape</em>, not feature parity with the upstream Fossil server (which still wins on web UI, wiki, forum, tickets).</p> </section> <section> <span class="num">§06</span> <h2>What it is not <span class="anchor">limits</span></h2> <p><strong>Not a Fossil replacement.</strong> EdgeSync moves <code>.fossil</code> blobs around. The format and protocol come from the upstream Fossil project; EdgeSync just changes the envelope. Run the upstream <code>fossil</code> binary alongside if you need the web UI, wiki, forum, or ticket system.</p> <p><strong>Alpha.</strong> The wire protocol (Fossil <code>/xfer</code> cards) is stable, but EdgeSync's CLI flags, agent config schema, and notify message shape may change before v1.</p> <p><strong>Not a database sync engine.</strong> EdgeSync replicates <code>.fossil</code> repositories, not arbitrary SQLite databases. If you want general SQLite replication, look at <a href="https://litestream.io">Litestream</a> or <a href="https://github.com/superfly/litefs">LiteFS</a>.</p> <p><strong>No web UI yet.</strong> The leaf agent serves <code>/xfer</code> for sync but does not render HTML. Use upstream <code>fossil</code> bound to a local repo if you need a UI.</p> </section> <section> <span class="num">§07</span> <h2>Get going <span class="anchor">start</span></h2> <div class="code-block"> <div class="code-bar"> <span>shell</span> <button type="button" class="copy-btn" data-copy="quickstart" aria-label="Copy quickstart commands">copy</button> </div> <pre id="quickstart">$ go install github.com/danmestas/EdgeSync/cmd/edgesync@latest $ edgesync new my-project.fossil # create a repo $ edgesync sync start --repo my-project.fossil --serve-http :9000 $ edgesync notify init --project my-project # enable messaging</pre> </div> <p>Walkthrough: <a href="/docs/quickstart/">/docs/quickstart</a> · Concepts: <a href="/docs/concepts/">/docs/concepts</a> · Architecture: <a href="/docs/architecture/">/docs/architecture</a> · Deployment: <a href="/docs/deployment/">/docs/deployment</a> · Notify: <a href="/docs/notify/">/docs/notify</a> · Source: <a href="https://github.com/danmestas/EdgeSync">github.com/danmestas/EdgeSync</a>.</p> </section> </main> <footer> <div>edgesync · v{{ .Site.Params.version }} · alpha · 2026 · <a href="https://github.com/danmestas/EdgeSync">github.com/danmestas/EdgeSync</a></div> </footer> <script> (function () { var btns = document.querySelectorAll(".copy-btn"); btns.forEach(function (b) { b.addEventListener("click", function () { var t = document.getElementById(b.dataset.copy); if (!t) return; var s = t.textContent.replace(/^\s*\$\s?/gm, "").replace(/\s+#.*$/gm, "").trim(); var done = function () { b.dataset.state = "copied"; b.textContent = "copied"; setTimeout(function () { b.dataset.state = ""; b.textContent = "copy"; }, 1500); }; if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(s).then(done).catch(function () { b.textContent = "select+copy"; }); } else { var ta = document.createElement("textarea"); ta.value = s; ta.style.position = "fixed"; ta.style.left = "-9999px"; document.body.appendChild(ta); ta.select(); try { document.execCommand("copy"); done(); } catch (e) { b.textContent = "select+copy"; } document.body.removeChild(ta); } }); }); })(); </script> </body> </html>