|
| 1 | +# Nostr Git Browser - Design Document |
| 2 | + |
| 3 | +A browser-based git repository viewer and sync client using Nostr for real-time updates. |
| 4 | + |
| 5 | +## Status: ✅ Implemented |
| 6 | + |
| 7 | +Working prototype in `index.html` (~750 lines, single file, no build step). |
| 8 | + |
| 9 | +**Features working:** |
| 10 | +- Clone repositories to IndexedDB |
| 11 | +- Browse files with expandable tree |
| 12 | +- View files (raw text or HTML preview) |
| 13 | +- HTML preview with local JSON injection (fetch interception) |
| 14 | +- Auto-sync on Nostr 30617 events from trusted pubkeys |
| 15 | +- Multi-relay WebSocket connections with status indicators |
| 16 | +- Add/remove repositories via modal |
| 17 | +- Configuration persisted to localStorage |
| 18 | + |
| 19 | +## Overview |
| 20 | + |
| 21 | +This project brings the functionality of [nostr-git-sync](https://github.com/JavaScriptSolidServer/nostr-git-sync) to the browser, allowing users to: |
| 22 | + |
| 23 | +1. Subscribe to multiple git repositories via Nostr (NIP-34) |
| 24 | +2. Clone/sync repositories to browser IndexedDB |
| 25 | +3. Browse repository files with a visual UI |
| 26 | +4. Auto-update when new commits are published |
| 27 | + |
| 28 | +``` |
| 29 | +┌─────────────────┐ WebSocket ┌─────────────────┐ |
| 30 | +│ │◄──────────────────►│ Nostr Relays │ |
| 31 | +│ Browser │ └─────────────────┘ |
| 32 | +│ │ HTTP/fetch ┌─────────────────┐ |
| 33 | +│ IndexedDB │◄──────────────────►│ Git Server │ |
| 34 | +│ /sync/repo1/ │ (clone/pull) │ (JSS, GitHub) │ |
| 35 | +│ /sync/repo2/ │ └─────────────────┘ |
| 36 | +└─────────────────┘ |
| 37 | +``` |
| 38 | + |
| 39 | +## Core Concepts |
| 40 | + |
| 41 | +### NIP-34 Events |
| 42 | + |
| 43 | +**Kind 30617** - Repository Announcement |
| 44 | +```json |
| 45 | +{ |
| 46 | + "kind": 30617, |
| 47 | + "tags": [ |
| 48 | + ["d", "repo-id"], |
| 49 | + ["name", "repo-name"], |
| 50 | + ["clone", "https://server.com/repo"], |
| 51 | + ["refs/heads/main", "abc123..."] |
| 52 | + ], |
| 53 | + "content": "Latest commit: message", |
| 54 | + "pubkey": "publisher-pubkey" |
| 55 | +} |
| 56 | +``` |
| 57 | + |
| 58 | +When a trusted pubkey publishes a 30617 event for a tracked repo, the browser syncs. |
| 59 | + |
| 60 | +### Storage Structure |
| 61 | + |
| 62 | +Uses IndexedDB via LightningFS (isomorphic-git's filesystem): |
| 63 | + |
| 64 | +``` |
| 65 | +IndexedDB: nostr-git-browser |
| 66 | +├── /sync/ |
| 67 | +│ ├── /20260202/ |
| 68 | +│ │ ├── .git/ |
| 69 | +│ │ ├── index.html |
| 70 | +│ │ ├── gitmark.html |
| 71 | +│ │ ├── webledgers.json |
| 72 | +│ │ └── .well-known/txo/txo.json |
| 73 | +│ └── /another-repo/ |
| 74 | +│ └── ... |
| 75 | +``` |
| 76 | + |
| 77 | +### Configuration |
| 78 | + |
| 79 | +Stored in localStorage as `nostr-git-config`: |
| 80 | + |
| 81 | +```json |
| 82 | +{ |
| 83 | + "relays": [ |
| 84 | + "wss://relay.damus.io", |
| 85 | + "wss://nos.lol", |
| 86 | + "wss://melvin.me/relay" |
| 87 | + ], |
| 88 | + "repos": { |
| 89 | + "20260202": { |
| 90 | + "cloneUrl": "https://melvin.me/public/git/sync/20260202", |
| 91 | + "branch": "gh-pages", |
| 92 | + "trusted": ["d769d2b81c051d2f2c0b437d0ffe39e00ff0f7161b520f0bd30811f4c057795f"] |
| 93 | + } |
| 94 | + } |
| 95 | +} |
| 96 | +``` |
| 97 | + |
| 98 | +## Architecture |
| 99 | + |
| 100 | +### Single-File Implementation |
| 101 | + |
| 102 | +The entire application is contained in `index.html` with inline CSS and JavaScript: |
| 103 | + |
| 104 | +``` |
| 105 | +index.html (~750 lines) |
| 106 | +├── <style> # CSS (layout, components, modals) |
| 107 | +├── <script type="module"> |
| 108 | +│ ├── Imports # Preact, htm, isomorphic-git, LightningFS |
| 109 | +│ ├── Config # localStorage load/save |
| 110 | +│ ├── Git Ops # clone, pull, listFiles, readFile |
| 111 | +│ ├── Nostr # WebSocket relay connections, subscriptions |
| 112 | +│ ├── Components # Preact functional components |
| 113 | +│ │ ├── Header # App header with relay status |
| 114 | +│ │ ├── Sidebar # Repository list |
| 115 | +│ │ ├── FileTree # Expandable file browser |
| 116 | +│ │ ├── FileView # File content viewer with preview |
| 117 | +│ │ ├── AddRepoModal |
| 118 | +│ │ └── Toast # Notifications |
| 119 | +│ └── App # Main component, state management |
| 120 | +``` |
| 121 | + |
| 122 | +### Dependencies |
| 123 | + |
| 124 | +All loaded via CDN (esm.sh) - **no build step required**: |
| 125 | + |
| 126 | +```javascript |
| 127 | +import { h, render } from 'https://esm.sh/preact@10.19.3'; |
| 128 | +import { useState, useEffect, useCallback, useMemo } from 'https://esm.sh/preact@10.19.3/hooks'; |
| 129 | +import htm from 'https://esm.sh/htm@3.1.1'; |
| 130 | +import git from 'https://esm.sh/isomorphic-git@1.25.3'; |
| 131 | +import http from 'https://esm.sh/isomorphic-git@1.25.3/http/web'; |
| 132 | +import LightningFS from 'https://esm.sh/@isomorphic-git/lightning-fs@4.6.0'; |
| 133 | +``` |
| 134 | + |
| 135 | +**Why Preact + htm:** |
| 136 | +- No build tools (Webpack, Vite, etc.) |
| 137 | +- Single HTML file |
| 138 | +- JSX-like syntax via tagged templates |
| 139 | +- Tiny footprint (3kb vs React's 40kb+) |
| 140 | +- Hooks support |
| 141 | + |
| 142 | +## Key Features |
| 143 | + |
| 144 | +### 1. Git Operations |
| 145 | + |
| 146 | +```javascript |
| 147 | +// Clone repository |
| 148 | +await git.clone({ |
| 149 | + fs, http, dir, |
| 150 | + url: cloneUrl, |
| 151 | + ref: branch, |
| 152 | + singleBranch: true, |
| 153 | + depth: 1 |
| 154 | +}); |
| 155 | + |
| 156 | +// Pull updates |
| 157 | +await git.pull({ |
| 158 | + fs, http, dir, |
| 159 | + ref: branch, |
| 160 | + singleBranch: true, |
| 161 | + author: { name: 'browser', email: 'browser@local' } |
| 162 | +}); |
| 163 | +``` |
| 164 | + |
| 165 | +### 2. Nostr Relay Connections |
| 166 | + |
| 167 | +```javascript |
| 168 | +function connectRelays(relays, repoIds, trusted, onEvent) { |
| 169 | + for (const url of relays) { |
| 170 | + const ws = new WebSocket(url); |
| 171 | + ws.onopen = () => { |
| 172 | + // Subscribe to kind 30617 for tracked repos |
| 173 | + ws.send(JSON.stringify([ |
| 174 | + "REQ", "sync", |
| 175 | + { kinds: [30617], "#d": repoIds, authors: trusted } |
| 176 | + ])); |
| 177 | + }; |
| 178 | + ws.onmessage = (msg) => { |
| 179 | + const [type, , event] = JSON.parse(msg.data); |
| 180 | + if (type === 'EVENT') onEvent(event); |
| 181 | + }; |
| 182 | + } |
| 183 | +} |
| 184 | +``` |
| 185 | + |
| 186 | +### 3. HTML Preview with Fetch Interception |
| 187 | + |
| 188 | +When viewing HTML files, the app injects a script that intercepts `fetch()` calls for local JSON files: |
| 189 | + |
| 190 | +```javascript |
| 191 | +// Scan HTML for fetch() patterns |
| 192 | +const fetchMatches = content.matchAll(/fetch\s*\(\s*['"]([^'"?]+)/g); |
| 193 | +for (const match of fetchMatches) { |
| 194 | + const fetchPath = match[1]; |
| 195 | + const json = await readFile(repoId, fetchPath); |
| 196 | + localFiles[fetchPath] = JSON.parse(json); |
| 197 | +} |
| 198 | + |
| 199 | +// Inject fetch wrapper into HTML before rendering in iframe |
| 200 | +const fetchWrapper = '<script>' + |
| 201 | + 'window.__localFiles = ' + JSON.stringify(localFiles) + ';' + |
| 202 | + 'const _fetch = window.fetch;' + |
| 203 | + 'window.fetch = (url, ...args) => {' + |
| 204 | + ' const name = String(url).split("?")[0];' + |
| 205 | + ' if (window.__localFiles[name] !== undefined) {' + |
| 206 | + ' return Promise.resolve(new Response(JSON.stringify(window.__localFiles[name])));' + |
| 207 | + ' }' + |
| 208 | + ' return _fetch(url, ...args);' + |
| 209 | + '};' + |
| 210 | + '<' + '/script>'; |
| 211 | +``` |
| 212 | + |
| 213 | +This allows HTML files like `index.html` (WebLedger) and `gitmark.html` to display real data from their companion JSON files stored in IndexedDB. |
| 214 | + |
| 215 | +### 4. File Tree with Lazy Loading |
| 216 | + |
| 217 | +```javascript |
| 218 | +function FileTree({ files, selected, expanded, onSelect, onToggle, depth = 0 }) { |
| 219 | + return html` |
| 220 | + ${files.map(entry => html` |
| 221 | + <div key=${entry.path}> |
| 222 | + <div |
| 223 | + class="file-entry ${selected === entry.path ? 'selected' : ''}" |
| 224 | + style="padding-left: ${12 + depth * 16}px" |
| 225 | + onClick=${() => entry.type === 'dir' ? onToggle(entry.path) : onSelect(entry.path)} |
| 226 | + > |
| 227 | + <span>${entry.type === 'dir' ? (expanded.has(entry.path) ? '📂' : '📁') : '📄'}</span> |
| 228 | + <span>${entry.name}</span> |
| 229 | + </div> |
| 230 | + ${entry.type === 'dir' && expanded.has(entry.path) && entry.children && html` |
| 231 | + <${FileTree} files=${entry.children} ... depth=${depth + 1} /> |
| 232 | + `} |
| 233 | + </div> |
| 234 | + `)} |
| 235 | + `; |
| 236 | +} |
| 237 | +``` |
| 238 | + |
| 239 | +## User Interface |
| 240 | + |
| 241 | +### Layout |
| 242 | + |
| 243 | +``` |
| 244 | +┌─────────────────────────────────────────────────────────────────┐ |
| 245 | +│ 🔗 Nostr Git Browser ● nos.lol ● damus ● melvin │ |
| 246 | +├────────────────────┬────────────────────────────────────────────┤ |
| 247 | +│ │ │ |
| 248 | +│ REPOSITORIES │ index.html [Raw] │ |
| 249 | +│ ─────────────── │ ───────────────────────────────────────── │ |
| 250 | +│ │ │ |
| 251 | +│ 📁 20260202 │ ┌─────────────────────────────────────┐ │ |
| 252 | +│ 📂 .well-known │ │ WebLedger │ │ |
| 253 | +│ 📂 txo │ │ 2026-02-02 | Updated: 4:53 PM │ │ |
| 254 | +│ txo.json │ │ │ │ |
| 255 | +│ gitmark.html │ │ ENTRIES │ │ |
| 256 | +│ index.html ◄ │ │ did:nostr:de7ecd... 1,242,035 │ │ |
| 257 | +│ mark.sh │ │ │ │ |
| 258 | +│ sync.sh │ │ TOTAL: 1,242,035 sats │ │ |
| 259 | +│ SYSTEM.md │ └─────────────────────────────────────┘ │ |
| 260 | +│ webledgers.json│ │ |
| 261 | +│ │ │ |
| 262 | +│ [+ Add Repository]│ │ |
| 263 | +└────────────────────┴────────────────────────────────────────────┘ |
| 264 | +``` |
| 265 | + |
| 266 | +### Status Indicators |
| 267 | + |
| 268 | +- 🟢 Connected to relay (green dot in header) |
| 269 | +- 🟡 Syncing (spinner on repo) |
| 270 | +- Toast notifications for sync events |
| 271 | + |
| 272 | +### Interactions |
| 273 | + |
| 274 | +1. **Add Repo**: Click [+ Add Repository] → Modal → Enter details → Clone starts |
| 275 | +2. **Browse Files**: Click folder to expand, click file to view |
| 276 | +3. **View HTML**: Toggle Raw/Preview button for rendered view |
| 277 | +4. **Auto-sync**: Nostr events from trusted pubkeys trigger pull |
| 278 | + |
| 279 | +## Technical Considerations |
| 280 | + |
| 281 | +### CORS |
| 282 | + |
| 283 | +Git servers must allow CORS for browser fetch: |
| 284 | +- JSS has CORS enabled by default |
| 285 | +- GitHub raw URLs work |
| 286 | +- May need CORS proxy for some servers |
| 287 | + |
| 288 | +### Storage Limits |
| 289 | + |
| 290 | +IndexedDB limits vary by browser: |
| 291 | +- Chrome: ~80% of available disk |
| 292 | +- Firefox: ~50% of available disk |
| 293 | +- Safari: ~1GB default |
| 294 | + |
| 295 | +### Branch Handling |
| 296 | + |
| 297 | +The app uses the configured branch for both clone and pull operations: |
| 298 | +```javascript |
| 299 | +await git.clone({ ..., ref: branch, singleBranch: true }); |
| 300 | +await git.pull({ ..., ref: branch, singleBranch: true }); |
| 301 | +``` |
| 302 | + |
| 303 | +### Security |
| 304 | + |
| 305 | +- Only sync from trusted pubkeys (configurable per repo) |
| 306 | +- HTML previews run in sandboxed iframe with `sandbox="allow-scripts"` |
| 307 | +- Fetch interception only provides read access to IndexedDB files |
| 308 | + |
| 309 | +## Running |
| 310 | + |
| 311 | +Just open `index.html` in a browser: |
| 312 | + |
| 313 | +```bash |
| 314 | +cd nostr-git-browser |
| 315 | +python -m http.server 3006 |
| 316 | +# or |
| 317 | +npx serve . |
| 318 | +``` |
| 319 | + |
| 320 | +Then visit `http://localhost:3006` |
| 321 | + |
| 322 | +No build step, no npm install, no configuration files needed. |
| 323 | + |
| 324 | +## Example: Add a Repository |
| 325 | + |
| 326 | +1. Click **"+ Add Repository"** |
| 327 | +2. Enter: |
| 328 | + - **ID**: `20260202` |
| 329 | + - **URL**: `https://melvin.me/public/git/sync/20260202` |
| 330 | + - **Branch**: `gh-pages` |
| 331 | + - **Trusted**: `d769d2b81c051d2f2c0b437d0ffe39e00ff0f7161b520f0bd30811f4c057795f` |
| 332 | +3. Click **Add** |
| 333 | +4. Repository clones to IndexedDB |
| 334 | +5. Browse files, view index.html with live WebLedger data |
| 335 | + |
| 336 | +## Future Improvements |
| 337 | + |
| 338 | +- [ ] Delete repository button |
| 339 | +- [ ] Settings panel for relay configuration |
| 340 | +- [ ] Syntax highlighting for code files |
| 341 | +- [ ] Commit history viewer |
| 342 | +- [ ] Diff viewer |
| 343 | +- [ ] Export/import configuration |
| 344 | +- [ ] Service worker for offline support |
| 345 | +- [ ] Storage usage indicator |
| 346 | + |
| 347 | +## References |
| 348 | + |
| 349 | +- [isomorphic-git](https://isomorphic-git.org/) - Git implementation in JavaScript |
| 350 | +- [LightningFS](https://github.com/isomorphic-git/lightning-fs) - IndexedDB filesystem |
| 351 | +- [NIP-34](https://github.com/nostr-protocol/nips/blob/master/34.md) - Git over Nostr |
| 352 | +- [nostr-git-sync](https://github.com/JavaScriptSolidServer/nostr-git-sync) - Server-side equivalent |
| 353 | +- [Preact](https://preactjs.com/) - Fast 3kB React alternative |
| 354 | +- [htm](https://github.com/developit/htm) - JSX-like syntax with template literals |
| 355 | + |
| 356 | +## License |
| 357 | + |
| 358 | +MIT |
0 commit comments