OverviewFilesCommitsIssuesDocsReleasesPeers

wirecore commits / a2f2fbb

Document architectural decisions for Pear app refactor
All open questions resolved: 1. Content Storage → Hyperdrive (DECIDED) - Use Hyperdrive for ALL content (text + images) - P2P-native, sparse downloads, efficient - Extends existing Hyperdrive usage in Pear app - Consistent: one storage mechanism 2. Identity System → ANProto Only (DECIDED) - Remove IdentityKey (keet-identity-key) - Use only ANProto ed25519 keypair - Simpler: one identity system - More portable: pure ed25519 - Store in Hyperbee metadata 3. Migration Strategy → Fresh Start (DECIDED) - New version = new identities - No backwards compatibility burden - Clean slate for correct architecture - Alpha phase: acceptable to users 4. Keypair Storage → Hyperbee (DECIDED) - Store in metadata/ Hyperbee - Integrated with other settings - Backed up with Corestore - No separate file needed 5. Content Sync → Multi-Transport (DECIDED) - Hyperdrive cache (primary) - Hyperswarm Protomux (P2P) - Relay WebSocket/APDS (fallback) - Relay already supports content-by-hash Updated refactor plan: - Phase 1: Add ANProto (remove IdentityKey) - Phase 2: Add Hyperdrive content storage - Storage structure revised for Hyperdrive - Components updated (ContentDrive replaces ContentStore) Storage efficiency: - Before: ~2 KB per post (full JSON + image) - After: 132 bytes (signature) + lazy content fetch - Result: 93% bandwidth reduction for initial sync Files: - pear-app/DECISIONS.md - Complete decision rationale - pear-app/REFACTOR_PLAN.md - Updated implementation plan Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
a2f2fbb73353cf0841f78d5a650eaf17f25518bd
by plankatron <fleeky@gmail.com> · 2026-01-21T15:13:34.000Z

Changed files (2) +667 -87

pear-app/DECISIONS.md +427 -0
……@@ -0,0 +1,427 @@
1# Wirecore Pear App - Architecture Decisions
2
3**Date**: 2026-01-21
4**Status**: Finalized
5
6This document records the key architectural decisions made during refactor planning.
7
8---
9
10## Decision Summary
11
12| Decision | Choice | Rationale |
13|----------|--------|-----------|
14| **Content Storage** | Hyperdrive | P2P-native, sparse downloads, already in use |
15| **Identity System** | ANProto only | Simpler, portable, one system |
16| **Migration Strategy** | Fresh start | Clean slate, no backwards compat burden |
17| **Keypair Storage** | Hyperbee metadata | Integrated, backed up with Corestore |
18| **Content Sync** | Multi-transport | Hyperswarm + relay fallback |
19
20---
21
22## 1. Content Storage: Hyperdrive
23
24### The Question
25
26Where should actual content (YAML text + images) be stored?
27
28### Options Considered
29
30**A) Hyperdrive** ✅ CHOSEN
31- P2P-native replication
32- Sparse downloads (fetch only what you need)
33- Efficient for all sizes
34- Already using for images in Pear app
35
36**B) content.json** (original lib/core design)
37- Simple JSON file
38- Easy to debug
39- ❌ Not P2P-replicable
40- ❌ Entire file loaded into memory
41- ❌ Poor for large images
42
43**C) Hybrid**
44- Text in content.json, images in Hyperdrive
45- ❌ Two storage mechanisms
46- ❌ Inconsistent
47
48### Decision: Hyperdrive for ALL Content
49
50**Storage Structure**:
51```
52cores/content-drive/
53├── pZ/
54│ └── pZGm1Av0IEBKARczz7exkNYsZb8LzaMrV7J32a2fFG4=
55│ └── (YAML text content as Buffer)
56├── aB/
57│ └── aBcd2Ef3ghIjKlMnOpQrStUvWxYz123456789ABCDEF=
58│ └── (base64 image data as Buffer)
59```
60
61**Benefits**:
62- ✅ Consistent: one storage mechanism
63- ✅ P2P replication: content propagates via Hyperswarm
64- ✅ Sparse downloads: only fetch content being viewed
65- ✅ Efficient: Hyperdrive handles large files well
66- ✅ Extends existing: Pear app already uses Hyperdrive for images
67
68**Implementation**:
69```javascript
70// Initialize
71const contentDrive = new Hyperdrive(store, { name: 'content-drive' })
72await contentDrive.ready()
73
74// Store by hash
75const hashPath = `/${hash.substring(0, 2)}/${hash}`
76await contentDrive.put(hashPath, Buffer.from(yaml))
77
78// Retrieve by hash
79const content = await contentDrive.get(hashPath)
80```
81
82---
83
84## 2. Identity System: ANProto Only
85
86### The Question
87
88Should we keep both IdentityKey (Keet) and ANProto keypair, or just ANProto?
89
90### Current Setup
91
92- **IdentityKey**: From `keet-identity-key` package
93- Generates seed → derives ed25519 key
94- Stored in `cores/identity/` Hypercore
95- Used for displaying public key
96
97- **ANProto**: Pure ed25519 keypair
98- 132-character base64 string
99- Used for signing messages
100
101### Decision: ANProto Only
102
103**Remove**:
104- ❌ `import IdentityKey from 'keet-identity-key'`
105- ❌ `cores/identity/` Hypercore
106- ❌ IdentityKey from package.json
107
108**Keep**:
109- ✅ ANProto keypair in Hyperbee metadata
110
111**Storage**:
112```javascript
113// Generate on init
114const { keypair, publicKey, secretKey } = ANProto.gen()
115
116// Store in Hyperbee
117await metaBee.put('anproto-keypair', keypair) // 132-char string
118
119// Load on startup
120const keypair = (await metaBee.get('anproto-keypair')).value
121const { publicKey, secretKey } = ANProto.loadKeypair(keypair)
122```
123
124### Rationale
125
126| Aspect | ANProto Only | IdentityKey + ANProto |
127|--------|-------------|----------------------|
128| **Complexity** | ✅ One system | ❌ Two systems to maintain |
129| **Portability** | ✅ Pure ed25519, works anywhere | ⚠️ Tied to Keet ecosystem |
130| **Storage** | ✅ Just keypair in Hyperbee | ❌ Seed + core + keypair |
131| **Keet Compat** | ❌ No Keet features | ✅ Could integrate with Keet |
132| **Public Key Format** | ✅ Base64 (ANProto standard) | ❌ Two different formats |
133
134**Consequences**:
135- ✅ Simpler architecture: one identity mechanism
136- ✅ More portable: ANProto keypair works in browser, Node, Bare
137- ✅ Consistent: ANProto publicKey used everywhere
138- ❌ No Keet integration (acceptable - not using Keet features)
139- ⚠️ Breaking change (acceptable - fresh start migration)
140
141---
142
143## 3. Migration Strategy: Fresh Start
144
145### The Question
146
147How should existing users migrate to refactored version?
148
149### Options Considered
150
151**A) Fresh Start** ✅ CHOSEN
152- Ship new version as separate release
153- Users create new ANProto identities
154- Old data stays in old storage
155- Optional manual export/import later
156
157**B) Dual-Format Reader**
158- New code reads both formats
159- Old posts: Full JSON in blocks
160- New posts: Signatures + Hyperdrive
161- Gradual migration over time
162
163**C) Migration Command**
164- `pear run . cli migrate-to-anproto`
165- Converts old feeds to new format
166- Extracts content, creates new signature-only feed
167
168### Decision: Fresh Start (Option A)
169
170**Rationale**:
171- ✅ Clean slate: no backwards compatibility burden
172- ✅ Simpler implementation: no dual-format reader complexity
173- ✅ Better architecture: start with correct design from day 1
174- ✅ Alpha phase: acceptable to ask users to create new identity
175- ✅ Users control migration timing: old version continues to work
176
177**User Experience**:
1781. Old Pear app (current) continues to work
1792. New Pear app (refactored) is separate release
1803. Users choose when to try new version
1814. Create new identity in new version
1825. Optional: Export/import tools can be built later if needed
183
184**Alternative Use Cases**:
185- Development testing: Fresh identities for each test
186- Production deployment: New identity on first use
187- Manual migration: Export old data, manually create posts in new version
188
189---
190
191## 4. Keypair Storage: Hyperbee Metadata
192
193### The Question
194
195Where should the ANProto keypair be stored?
196
197### Options Considered
198
199**A) Hyperbee Metadata** ✅ CHOSEN
200- Store in `metadata/` Hyperbee
201- Key: `anproto-keypair`
202- Value: 132-char base64 string
203
204**B) Separate JSON File**
205- `anproto-keypair.json` in storage root
206- Not replicated with Hypercore
207- Requires manual backup
208
209### Decision: Hyperbee Metadata (Option A)
210
211**Implementation**:
212```javascript
213// Initialize
214const metaBee = new Hyperbee(store.get({ name: 'metadata' }), {
215 keyEncoding: 'utf-8',
216 valueEncoding: 'utf-8' // Store keypair as string
217})
218
219// Store
220await metaBee.put('anproto-keypair', keypair)
221
222// Load
223const node = await metaBee.get('anproto-keypair')
224const keypair = node.value
225```
226
227**Benefits**:
228- ✅ Integrated with other metadata (username, follows, config)
229- ✅ Replicated automatically via Hypercore
230- ✅ Backed up with Corestore
231- ✅ No separate file to manage
232- ✅ Consistent with other settings storage
233
234**Other Metadata in Hyperbee**:
235```
236metadata/
237├── anproto-keypair # "BSY7/er4VJIu08o39NaRAiPY..."
238├── profile # { username: "fleeky", bio: "...", avatarHash: "..." }
239├── follow:BSY7... # { name: "alice", feedKey: "..." }
240├── follow:xYz1... # { name: "bob", feedKey: "..." }
241└── config # { theme: "dark", ... }
242```
243
244---
245
246## 5. Content Sync: Multi-Transport
247
248### The Question
249
250How should content-by-hash be fetched when not cached locally?
251
252### Implementation
253
254**Multi-Transport Strategy**:
255
256```javascript
257async function getContent(hash) {
258 // 1. Try local Hyperdrive cache
259 const hashPath = `/${hash.substring(0, 2)}/${hash}`
260 let content = await contentDrive.get(hashPath)
261 if (content) return content
262
263 // 2. Try Hyperswarm peers (Protomux channel)
264 content = await contentSync.fetchViaHyperswarm(hash)
265 if (content) {
266 await contentDrive.put(hashPath, content) // Cache
267 return content
268 }
269
270 // 3. Try relay WebSocket (APDS protocol)
271 content = await contentSync.fetchViaRelay(hash)
272 if (content) {
273 await contentDrive.put(hashPath, content) // Cache
274 return content
275 }
276
277 // Not found anywhere
278 return null
279}
280```
281
282**Transport 1: Hyperswarm (Protomux)**
283
284```javascript
285// Protomux channel on Hyperswarm connection
286const channel = protomux.createChannel('wirecore-content-sync')
287
288// Send request
289channel.send('content-request', { id, hash })
290
291// Receive response
292channel.on('content-response', ({ id, hash, content, found }) => {
293 if (found) {
294 // Cache and resolve
295 }
296})
297```
298
299**Transport 2: Relay WebSocket (APDS)**
300
301The relay already implements content-by-hash via APDS:
302
303```javascript
304// Relay code (wirecore-relay.js)
305ws.on('message', async (data) => {
306 const msg = data.toString('utf8')
307
308 if (msg.length === 44) {
309 // Hash request - 44 chars = base64 hash
310 const blob = await apds.get(msg)
311 if (blob) ws.send(blob)
312 } else {
313 // New content - store by hash
314 const hash = await apds.make(msg)
315 await apds.add(msg)
316 }
317})
318```
319
320**Client requests via WebSocket**:
321```javascript
322// Connect to relay
323const ws = new WebSocket('wss://7rnx.net/relay')
324
325// Request content by hash
326ws.send(hash) // 44-char base64
327
328// Receive content
329ws.on('message', (data) => {
330 const content = data.toString()
331 // Cache in Hyperdrive
332})
333```
334
335### Benefits
336
337- ✅ **Redundancy**: Multiple ways to fetch content
338- ✅ **Relay already supports it**: APDS protocol
339- ✅ **Fast local lookups**: Hyperdrive cache first
340- ✅ **Direct P2P**: Hyperswarm for native users
341- ✅ **Fallback for web**: Relay WebSocket when P2P unavailable
342
343### Why Multiple Transports?
344
345**Hyperswarm (Primary)**:
346- Native P2P for CLI/Pear users
347- No relay dependency
348- Direct peer connections
349- Best for decentralization
350
351**Relay WebSocket (Fallback)**:
352- Helps with NAT traversal
353- Content discovery when peers offline
354- Bridges web users (future)
355- Central index for bootstrapping
356
357---
358
359## Summary: What Changed
360
361### Before (Current Implementation)
362
363```
364Storage:
365- cores/identity/ # IdentityKey seed
366- cores/my-feed/ # Full JSON messages (200-500 bytes)
367- cores/metadata/ # Username, follows
368
369Identity:
370- IdentityKey from keet-identity-key
371- Hex public key display
372
373Messages:
374- Full JSON in Hypercore blocks
375- No ANProto signatures
376- No content-by-hash
377```
378
379### After (Refactored)
380
381```
382Storage:
383- cores/my-feed/ # ANProto signatures ONLY (132 bytes)
384- cores/content-drive/ # ALL content by hash (YAML + images)
385- cores/metadata/ # anproto-keypair, username, follows
386
387Identity:
388- ANProto keypair only
389- Base64 public key display
390
391Messages:
392- Signatures in Hypercore (132 bytes)
393- Content in Hyperdrive (fetched on-demand)
394- YAML format with metadata
395- Wiredove compatible
396```
397
398### Storage Savings
399
400**Example post with small image**:
401
402| Component | Before | After |
403|-----------|--------|-------|
404| Hypercore block | ~2 KB (JSON + base64 image) | 132 bytes (signature) |
405| Content storage | In block | Hyperdrive (sparse) |
406| **P2P bandwidth** | 2 KB per post | 132 bytes initial + content on-demand |
407| **Efficiency gain** | - | **93% reduction** for initial sync |
408
409**Timeline with 100 posts**:
410- Before: ~200 KB to download all blocks
411- After: ~13 KB for signatures, content fetched as viewed
412
413---
414
415## Next Steps
416
4171. ✅ Decisions documented (this file)
4182. ✅ Refactor plan updated (REFACTOR_PLAN.md)
4193. ⏳ Begin Phase 1: ANProto layer implementation
4204. ⏳ Begin Phase 2: Hyperdrive content storage
4215. ⏳ Continue through Phase 6: Complete refactor
422
423---
424
425**Status**: Ready to implement
426**Approved**: 2026-01-21
427**Implementation Start**: Phase 1
pear-app/REFACTOR_PLAN.md +240 -87
……@@ -54,9 +54,10 @@ async function post(text) {
5454 const contentHash = ANProto.hashData(yaml)
5555 // → "pZGm1Av0IEBKARczz7exkNYsZb8LzaMrV7J32a2fFG4="
5656
57 // 3. Store content in local cache
58 await contentStore.put(contentHash, yaml)
59 // → Writes to {storage}/content.json
57 // 3. Store content in Hyperdrive by hash
58 const hashPath = `/${contentHash.substring(0, 2)}/${contentHash}`
59 await contentDrive.put(hashPath, Buffer.from(yaml))
60 // → Writes to cores/content-drive/{hash}
6061
6162 // 4. Sign the hash with ANProto
6263 const signature = ANProto.signHash(contentHash, anprotoKeypair)
……@@ -83,15 +84,19 @@ async function getTimeline(limit) {
8384 // 2. Verify signature and extract hash
8485 const { timestamp, hash } = ANProto.verifySignature(signature)
8586
86 // 3. Try to get content from local cache
87 let content = await contentStore.get(hash)
87 // 3. Try to get content from Hyperdrive
88 const hashPath = `/${hash.substring(0, 2)}/${hash}`
89 let content = await contentDrive.get(hashPath)
8890
8991 // 4. If not cached, request from peers
9092 if (!content) {
9193 content = await contentSync.fetch(hash)
92 await contentStore.put(hash, content)
94 // Cache in Hyperdrive
95 await contentDrive.put(hashPath, content)
9396 }
9497
98 content = content.toString()
99
95100 // 5. Parse YAML and extract metadata
96101 const { metadata, text } = YAMLHelper.parse(content)
97102
……@@ -115,38 +120,37 @@ async function getTimeline(limit) {
115120### Storage Architecture (Refactored)
116121
117122```
118{storage-path}/
119├── anproto-keypair.json # ANProto ed25519 keypair (132 chars)
120│ {
121│ "keypair": "BSY7/er4VJIu08o39NaRAiPY/MAvd7oQhlGCRDABjYU=tQa03kqUWG..."
122│ }
123{storage-path}/cores/ # Corestore (Hypercore feeds)
124│
125├── my-feed/ # Personal feed (writable)
126│ └── data # Contains ONLY ANProto signatures!
127│ # Each block: 132 bytes
123128│
124├── content.json # Content-by-hash cache
125│ {
126│ "pZGm1Av0...": "---\nname: fleeky\nimage: hash...\n---\nHello!",
127│ "anotherHash...": "---\nname: fleeky\n---\nAnother post",
128│ "imageHash...": "base64-encoded-image-data..."
129│ }
129├── {followed-feed-1}/ # Followed user's feed (read-only)
130│ └── data # Signatures only, sparse download
130131│
131├── cores/ # Corestore (Hypercore feeds)
132│ ├── identity/ # IdentityKey seed (for display name)
133│ │ └── (kept for compatibility)
134│ │
135│ ├── my-feed/ # Personal feed (writable)
136│ │ └── data # Contains ONLY ANProto signatures!
137│ │ # Each block: 132 bytes
138│ │
139│ ├── {followed-feed-1}/ # Followed user's feed (read-only)
140│ │ └── data # Signatures only, sparse download
141│ │
142│ └── {followed-feed-N}/ # More followed feeds
132├── {followed-feed-N}/ # More followed feeds
133│
134├── content-drive/ # Hyperdrive: Content by hash
135│ └── {hash-prefix}/ # Organized by first 2 chars
136│ └── {full-hash} # Each file = YAML content or image
137│ # Example:
138│ # pZ/pZGm1Av0... = "---\nname: fleeky\n---\nHello!"
139│ # aB/aBcd2Ef3... = <base64-image-data>
143140│
144141└── metadata/ # Hyperbee for user metadata
145 ├── profile # { username, bio, avatar }
142 ├── anproto-keypair # ANProto ed25519 keypair (132 chars)
143 ├── profile # { username, bio, avatarHash }
146144 ├── follow:{pubkey} # Follow list
147145 └── config # App settings
148146```
149147
148**Key Changes from Current**:
149- ❌ Removed: `identity/` core (IdentityKey)
150- ✅ Added: `content-drive/` (Hyperdrive for ALL content)
151- ✅ Added: `anproto-keypair` in metadata Hyperbee
152- ✅ Changed: my-feed stores signatures, not full messages
153
150154### Network Architecture
151155
152156```
……@@ -230,34 +234,70 @@ class YAMLHelper {
230234}
231235```
232236
233### 3. Content Store (`lib/content-store.js`)
237### 3. Content Drive (Hyperdrive Wrapper)
234238
235**Copy from**: `lib/core/content-store.js`
239**NEW MODULE**: `lib/content-drive.js`
240
241**Purpose**: Wrap Hyperdrive for content-by-hash storage
236242
237243**API**:
238244```javascript
239class ContentStore {
240 constructor(storagePath)
245class ContentDrive {
246 constructor(corestore, options)
241247
242 async init() // Load content.json
243 async get(hash) → content | null
248 async ready() // Initialize Hyperdrive
244249 async put(hash, content) → void
250 async get(hash) → Buffer | null
245251 async has(hash) → boolean
246252 async delete(hash) → void
247 async clear() → void
248 async size() → number
253 async list() → AsyncIterator<{ hash, size }>
254 async close() → void
249255}
250256```
251257
252**File Format** (`content.json`):
253```json
254{
255 "hashABC...": "yaml content here",
256 "hashDEF...": "another yaml content",
257 "hashGHI...": "base64-image-data..."
258**Implementation**:
259```javascript
260import Hyperdrive from 'hyperdrive'
261
262class ContentDrive {
263 constructor(corestore, options = {}) {
264 this.drive = new Hyperdrive(corestore, { name: 'content-drive' })
265 }
266
267 async ready() {
268 await this.drive.ready()
269 }
270
271 async put(hash, content) {
272 const path = `/${hash.substring(0, 2)}/${hash}`
273 await this.drive.put(path, content)
274 }
275
276 async get(hash) {
277 const path = `/${hash.substring(0, 2)}/${hash}`
278 return await this.drive.get(path)
279 }
280
281 async has(hash) {
282 const content = await this.get(hash)
283 return content !== null
284 }
285
286 // ... other methods
258287}
259288```
260289
290**Storage Format** (Hyperdrive):
291```
292content-drive/
293├── pZ/
294│ └── pZGm1Av0... (YAML content as Buffer)
295├── aB/
296│ └── aBcd2Ef3... (Image data as Buffer)
297└── xY/
298 └── xYz789... (More content)
299```
300
261301### 4. Content Sync Manager (`lib/content-sync.js`)
262302
263303**Copy from**: `lib/core/content-sync.js`
……@@ -317,48 +357,63 @@ Message Types:
317357- ✅ Create refactor plan
318358- ⏳ Get user approval on approach
319359
320### Phase 1: Add ANProto Layer (Non-Breaking)
321**Goal**: Add ANProto alongside existing code, no breaking changes
360### Phase 1: Add ANProto Layer (Breaking - Fresh Start)
361**Goal**: Replace IdentityKey with ANProto, fresh start for users
322362
323363**Tasks**:
3241. Copy `anproto.js` to pear-app
3252. Add ANProto keypair generation/loading
326 - Store in `anproto-keypair.json`
327 - Keep IdentityKey for backwards compat
3283. Add ANProto signing to post()
329 - Store signature in message metadata
330 - Keep full JSON in Hypercore (for now)
3314. Add ANProto verification to timeline reading
332 - Verify signatures when reading
333 - Log warnings if verification fails
3641. Copy `anproto.js` to pear-app/lib/
3652. Remove IdentityKey dependency
366 - Remove `import IdentityKey from 'keet-identity-key'`
367 - Remove `identity/` core usage
368 - Remove IdentityKey from package.json
3693. Add ANProto keypair generation
370 - Generate on `init`
371 - Store in Hyperbee metadata: `anproto-keypair`
372 - Load on startup
3734. Update `whoami` command to show ANProto publicKey (base64)
3745. Add ANProto signing to post()
375 - Hash content with ANProto
376 - Sign hash with ANProto
377 - Store signature in message metadata (for now)
378 - Keep full JSON in Hypercore (temporary)
3796. Add ANProto verification to timeline
380 - Extract signature from message
381 - Verify with ANProto
382 - Log warnings if fails
334383
335384**Testing**:
336- Existing posts still work
337- New posts have ANProto signatures
385- Init creates ANProto keypair
386- Keypair stored in Hyperbee
387- Whoami shows base64 publicKey
388- Posts include ANProto signatures
338389- Signatures verify correctly
339390
340**Rollback**: Remove anproto.js, revert changes
391**Note**: This is a BREAKING CHANGE - users must create new identities
392**Migration**: Fresh start (no data migration)
341393
342### Phase 2: Add Content Storage (Non-Breaking)
343**Goal**: Add content.json cache, still store full messages in Hypercore
394### Phase 2: Add Content Storage (Hyperdrive)
395**Goal**: Add Hyperdrive for content-by-hash, still store full messages in Hypercore
344396
345397**Tasks**:
3461. Copy `content-store.js` to pear-app
3472. Initialize ContentStore on startup
3483. Store content in content.json when posting
3981. Create `lib/content-drive.js` (Hyperdrive wrapper)
3992. Initialize ContentDrive on startup
400 - Create in Corestore: `{ name: 'content-drive' }`
401 - Ready after store initialization
4023. Store content in Hyperdrive when posting
349403 - Hash content
350 - Store in content.json
351 - Keep storing full message in Hypercore
3524. Read from content.json when available
353 - Try content cache first
404 - Store in Hyperdrive: `/{prefix}/{hash}`
405 - Keep storing full message in Hypercore (for now)
4064. Read from Hyperdrive when available
407 - Try Hyperdrive first
354408 - Fall back to Hypercore
355409
356410**Testing**:
357411- Posts work as before
358- Content cached in content.json
359- Timeline reads from cache
412- Content cached in Hyperdrive
413- Timeline reads from Hyperdrive
414- Hyperdrive replicates via Hyperswarm
360415
361**Rollback**: Remove content-store.js, revert changes
416**Benefit**: Content is now P2P-replicable (unlike content.json)
362417
363418### Phase 3: Add YAML Format (Non-Breaking)
364419**Goal**: Convert messages to YAML, dual-format reader
……@@ -588,27 +643,125 @@ pear-app/test/
588643
589644---
590645
591## Open Questions
646## Decisions Made
647
648**Updated: 2026-01-21**
649
650### 1. Content Storage: Hyperdrive (DECIDED ✓)
651
652**Decision**: Use Hyperdrive for ALL content (text + images)
653
654```
655{storage-path}/cores/
656├── my-feed/ # Hypercore: ANProto signatures only (132 bytes)
657├── content-drive/ # Hyperdrive: ALL content by hash
658│ ├── pZ/ # First 2 chars of hash
659│ │ └── pZGm1Av0IEBKARczz7exkNYsZb8LzaMrV7J32a2fFG4=
660│ └── aB/
661│ └── aBcd2Ef3...
662└── metadata/ # Hyperbee: username, follows, anproto-keypair
663```
664
665**Rationale**:
666- ✅ Pear app already uses Hyperdrive for images
667- ✅ Native P2P replication
668- ✅ Sparse downloads (fetch only needed content)
669- ✅ Efficient for all sizes (text and images)
670- ✅ Consistent: one storage mechanism for all content
592671
5931. **Storage Location**: Should anproto-keypair.json be separate or in Hyperbee?
594 - Separate: Easier to export/import
595 - Hyperbee: More integrated with other metadata
672**Alternative considered**: content.json (original lib/core design)
673**Why rejected**: Not P2P-native, poor for large files, loads everything into memory
596674
5972. **Image Storage**: Should large images use Hyperdrive or content.json?
598 - content.json: Simpler, consistent
599 - Hyperdrive: Better for large files, sparse downloads
675### 2. Identity: ANProto Only (DECIDED ✓)
600676
6013. **Migration**: Force migration or support dual-format forever?
602 - Force: Cleaner codebase
603 - Dual: Better UX, no breaking change
677**Decision**: Remove IdentityKey, use ONLY ANProto keypair
604678
6054. **Identity**: Keep both IdentityKey and ANProto keypair?
606 - Both: Backwards compatible
607 - ANProto only: Simpler, cleaner
679```javascript
680// Store ANProto keypair in Hyperbee metadata
681await metaBee.put('anproto-keypair', keypair) // 132-char string
682
683// Remove:
684// - cores/identity/ (IdentityKey seed)
685// - import IdentityKey from 'keet-identity-key'
686```
687
688**Rationale**:
689- ✅ Simpler: one identity system
690- ✅ More portable: pure ed25519, not Keet-specific
691- ✅ Consistent: ANProto publicKey used everywhere
692- ✅ Less storage: no identity core needed
693
694**Consequences**:
695- ❌ No Keet integration (we're not using it anyway)
696- ✅ Breaking change acceptable (fresh start migration)
697
698### 3. Migration Strategy: Fresh Start (DECIDED ✓)
699
700**Decision**: Ship refactored version as new release, users create new identities
701
702**Rationale**:
703- ✅ Clean slate: no backwards compatibility burden
704- ✅ Simpler implementation: no dual-format reader
705- ✅ Better architecture: start with correct design
706- ✅ Alpha phase: acceptable to ask users to restart
707
708**User Experience**:
709- Old version continues to work (frozen)
710- New version is separate release
711- Users choose when to migrate
712- Export/import tools for manual data porting (if needed later)
713
714### 4. Keypair Storage: Hyperbee (DECIDED ✓)
715
716**Decision**: Store ANProto keypair in Hyperbee metadata, not separate file
717
718```javascript
719// Store
720await metaBee.put('anproto-keypair', keypair)
721
722// Load
723const keypair = await metaBee.get('anproto-keypair')
724```
725
726**Rationale**:
727- ✅ Integrated with other metadata (username, follows)
728- ✅ Replicated with Hypercore
729- ✅ Backed up automatically with Corestore
730- ✅ No separate file to manage
731
732**Alternative considered**: anproto-keypair.json separate file
733**Why rejected**: Separate file requires manual backup, not integrated with Corestore
734
735### 5. Content Sync: Multi-Transport (DECIDED ✓)
736
737**Decision**: Use BOTH Hyperswarm and relay for content fetching
738
739**Implementation**:
740```javascript
741// 1. Try local Hyperdrive cache
742let content = await contentDrive.get(hashPath)
743
744// 2. If not cached, try Hyperswarm peers (Protomux)
745if (!content) {
746 content = await contentSync.fetchViaHyperswarm(hash)
747}
748
749// 3. If still missing, try relay WebSocket (APDS)
750if (!content) {
751 content = await contentSync.fetchViaRelay(hash)
752}
753
754// 4. Cache result
755if (content) {
756 await contentDrive.put(hashPath, content)
757}
758```
608759
6095. **Content Sync**: Should relay serve content-by-hash for web users?
610 - Yes: Better for web users (no P2P content transport)
611 - No: Keep relay simple (signatures only)
760**Rationale**:
761- ✅ Relay already serves content-by-hash (APDS protocol)
762- ✅ Fallback mechanism for content discovery
763- ✅ Web users benefit from relay gateway
764- ✅ Native users get direct P2P via Hyperswarm
612765
613766---
614767