Skip to content

Commit d8b957f

Browse files
Expand documentation: 56 pages with new guides and architecture
Expanded project pages: - SolidOS: Panes, architecture, live collaboration features - Noskey: All output formats, vanity keys, security notes - Ditto: Mostr bridge, Mastodon API, Bluesky integration - FedBox: GoActivityPub ecosystem, storage backends, API examples - MicroFed: Zero-dependency philosophy, edge deployment use cases - MCP: Three primitives, SAND integration examples, SDKs - LWS: W3C timeline, scope, relationship to Solid - rdflib.js: UpdateManager, real-time collaboration, provenance New pages: - Architecture overview with protocol flow diagrams - Production deployment guide (systemd, Docker, nginx, SSL) - Multi-protocol integration guide (patterns, examples) - nostr-tools library reference - solid-client-authn library reference Total: 56 pages
1 parent 2cf6a91 commit d8b957f

14 files changed

Lines changed: 2485 additions & 95 deletions

File tree

docs/concepts/architecture.md

Lines changed: 238 additions & 0 deletions
Large diffs are not rendered by default.

docs/guides/multi-protocol.md

Lines changed: 383 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,383 @@
1+
---
2+
sidebar_position: 7
3+
title: Multi-Protocol Integration
4+
description: Build apps that use Solid, Nostr, and ActivityPub together
5+
---
6+
7+
# Multi-Protocol Integration
8+
9+
**Build applications that leverage multiple SAND protocols.** Combine strengths for powerful apps.
10+
11+
## Why Multi-Protocol?
12+
13+
Each protocol has strengths:
14+
15+
| Protocol | Strength | Use When |
16+
|----------|----------|----------|
17+
| **Solid** | Structured data, permissions | Storing personal data |
18+
| **Nostr** | Real-time, censorship-resistant | Public broadcasts, messaging |
19+
| **ActivityPub** | Federation, social features | Reaching Fediverse users |
20+
| **DID** | Portable identity | Cross-platform auth |
21+
22+
Combining them creates apps that couldn't exist with just one.
23+
24+
## Pattern 1: Solid Storage + Nostr Identity
25+
26+
Use Nostr keys for authentication, Solid pods for storage.
27+
28+
```javascript
29+
import { finalizeEvent, getPublicKey } from 'nostr-tools';
30+
import { getSolidDataset, saveSolidDatasetAt } from '@inrupt/solid-client';
31+
32+
// User's Nostr identity
33+
const secretKey = /* user's nsec */;
34+
const pubkey = getPublicKey(secretKey);
35+
36+
// Create NIP-98 auth event for Solid pod
37+
async function authenticatedFetch(url, options = {}) {
38+
const authEvent = finalizeEvent({
39+
kind: 27235,
40+
created_at: Math.floor(Date.now() / 1000),
41+
tags: [
42+
['u', url],
43+
['method', options.method || 'GET']
44+
],
45+
content: ''
46+
}, secretKey);
47+
48+
return fetch(url, {
49+
...options,
50+
headers: {
51+
...options.headers,
52+
'Authorization': `Nostr ${btoa(JSON.stringify(authEvent))}`
53+
}
54+
});
55+
}
56+
57+
// Read from Solid pod with Nostr auth
58+
const dataset = await getSolidDataset(podUrl, {
59+
fetch: authenticatedFetch
60+
});
61+
```
62+
63+
## Pattern 2: Solid Archive + Nostr Broadcast
64+
65+
Store data privately in Solid, broadcast publicly via Nostr.
66+
67+
```javascript
68+
import { saveSolidDatasetAt, createThing, setThing } from '@inrupt/solid-client';
69+
import { finalizeEvent, Relay } from 'nostr-tools';
70+
71+
async function publishPost(content, solidPodUrl, nostrRelays) {
72+
const timestamp = Date.now();
73+
74+
// 1. Save to Solid pod (permanent archive)
75+
let post = createThing({ name: `post-${timestamp}` });
76+
post = addStringNoLocale(post, SCHEMA.text, content);
77+
post = addDatetime(post, SCHEMA.dateCreated, new Date());
78+
79+
let dataset = createSolidDataset();
80+
dataset = setThing(dataset, post);
81+
await saveSolidDatasetAt(
82+
`${solidPodUrl}/posts/${timestamp}.ttl`,
83+
dataset,
84+
{ fetch }
85+
);
86+
87+
// 2. Broadcast to Nostr (real-time distribution)
88+
const event = finalizeEvent({
89+
kind: 1,
90+
created_at: Math.floor(timestamp / 1000),
91+
tags: [
92+
['solid', `${solidPodUrl}/posts/${timestamp}.ttl`] // Link to Solid
93+
],
94+
content
95+
}, secretKey);
96+
97+
for (const relayUrl of nostrRelays) {
98+
const relay = await Relay.connect(relayUrl);
99+
await relay.publish(event);
100+
relay.close();
101+
}
102+
103+
return { solidUrl: `${solidPodUrl}/posts/${timestamp}.ttl`, nostrId: event.id };
104+
}
105+
```
106+
107+
## Pattern 3: Nostr Events + ActivityPub Federation
108+
109+
Bridge Nostr to the Fediverse (like Ditto does).
110+
111+
```javascript
112+
// Pseudo-code for the concept
113+
114+
// When receiving a Nostr event
115+
async function onNostrEvent(event) {
116+
if (event.kind === 1) { // Text note
117+
// Convert to ActivityPub
118+
const activity = {
119+
'@context': 'https://www.w3.org/ns/activitystreams',
120+
type: 'Create',
121+
actor: `https://bridge.example/users/${event.pubkey}`,
122+
object: {
123+
type: 'Note',
124+
content: event.content,
125+
published: new Date(event.created_at * 1000).toISOString(),
126+
attributedTo: `https://bridge.example/users/${event.pubkey}`
127+
}
128+
};
129+
130+
// Deliver to ActivityPub followers
131+
for (const follower of await getAPFollowers(event.pubkey)) {
132+
await deliverToInbox(follower.inbox, activity);
133+
}
134+
}
135+
}
136+
137+
// When receiving an ActivityPub activity
138+
async function onAPActivity(activity) {
139+
if (activity.type === 'Create' && activity.object.type === 'Note') {
140+
// Convert to Nostr event
141+
const event = {
142+
kind: 1,
143+
content: activity.object.content,
144+
created_at: Math.floor(new Date(activity.object.published).getTime() / 1000),
145+
tags: [
146+
['activitypub', activity.id] // Reference original
147+
]
148+
};
149+
150+
// Broadcast to Nostr relays
151+
await broadcastToRelays(event);
152+
}
153+
}
154+
```
155+
156+
## Pattern 4: DID as Universal Identity
157+
158+
Use DID to link identities across protocols.
159+
160+
```javascript
161+
// DID Document linking multiple identities
162+
const didDocument = {
163+
'@context': ['https://www.w3.org/ns/did/v1'],
164+
id: 'did:web:alice.example',
165+
166+
// Verification methods
167+
verificationMethod: [
168+
{
169+
id: 'did:web:alice.example#nostr',
170+
type: 'SchnorrSecp256k1VerificationKey2019',
171+
controller: 'did:web:alice.example',
172+
publicKeyHex: '...' // Nostr public key
173+
},
174+
{
175+
id: 'did:web:alice.example#solid',
176+
type: 'Ed25519VerificationKey2020',
177+
controller: 'did:web:alice.example',
178+
publicKeyMultibase: '...'
179+
}
180+
],
181+
182+
// Service endpoints
183+
service: [
184+
{
185+
id: 'did:web:alice.example#solid-pod',
186+
type: 'SolidPod',
187+
serviceEndpoint: 'https://alice.pod.example/'
188+
},
189+
{
190+
id: 'did:web:alice.example#nostr',
191+
type: 'NostrRelayList',
192+
serviceEndpoint: ['wss://relay.damus.io', 'wss://relay.nostr.band']
193+
},
194+
{
195+
id: 'did:web:alice.example#activitypub',
196+
type: 'ActivityPubActor',
197+
serviceEndpoint: 'https://social.example/@alice'
198+
}
199+
]
200+
};
201+
```
202+
203+
## Pattern 5: Real-Time Collaboration
204+
205+
Use Solid for state, Nostr for real-time updates.
206+
207+
```javascript
208+
// Collaborative document editing
209+
210+
class CollaborativeDocument {
211+
constructor(solidUrl, nostrRelays, secretKey) {
212+
this.solidUrl = solidUrl;
213+
this.relays = nostrRelays;
214+
this.secretKey = secretKey;
215+
this.pubkey = getPublicKey(secretKey);
216+
}
217+
218+
async load() {
219+
// Load current state from Solid
220+
this.dataset = await getSolidDataset(this.solidUrl, { fetch });
221+
this.content = getStringNoLocale(
222+
getThing(this.dataset, this.solidUrl),
223+
SCHEMA.text
224+
);
225+
226+
// Subscribe to real-time updates via Nostr
227+
for (const relayUrl of this.relays) {
228+
const relay = await Relay.connect(relayUrl);
229+
relay.subscribe([{
230+
kinds: [30023], // Long-form content
231+
'#d': [this.solidUrl] // Tag referencing this document
232+
}], {
233+
onevent: (event) => this.handleRemoteUpdate(event)
234+
});
235+
}
236+
}
237+
238+
async update(newContent) {
239+
// Optimistic local update
240+
this.content = newContent;
241+
242+
// Broadcast change via Nostr (instant)
243+
const event = finalizeEvent({
244+
kind: 30023,
245+
created_at: Math.floor(Date.now() / 1000),
246+
tags: [['d', this.solidUrl]],
247+
content: newContent
248+
}, this.secretKey);
249+
250+
for (const relayUrl of this.relays) {
251+
const relay = await Relay.connect(relayUrl);
252+
await relay.publish(event);
253+
relay.close();
254+
}
255+
256+
// Save to Solid (durable)
257+
let thing = getThing(this.dataset, this.solidUrl);
258+
thing = setStringNoLocale(thing, SCHEMA.text, newContent);
259+
this.dataset = setThing(this.dataset, thing);
260+
await saveSolidDatasetAt(this.solidUrl, this.dataset, { fetch });
261+
}
262+
263+
handleRemoteUpdate(event) {
264+
if (event.pubkey !== this.pubkey) {
265+
this.content = event.content;
266+
this.onUpdate?.(this.content);
267+
}
268+
}
269+
}
270+
```
271+
272+
## Architecture: Multi-Protocol App
273+
274+
```
275+
┌─────────────────────────────────────────────────────────────┐
276+
│ Your Application │
277+
├─────────────────────────────────────────────────────────────┤
278+
│ │
279+
│ ┌─────────────────────────────────────────────────────┐ │
280+
│ │ Data Layer │ │
281+
│ │ ┌───────────┐ ┌───────────┐ ┌───────────────────┐ │ │
282+
│ │ │ Solid │ │ Nostr │ │ ActivityPub │ │ │
283+
│ │ │ Client │ │ Client │ │ Client │ │ │
284+
│ │ └─────┬─────┘ └─────┬─────┘ └─────────┬─────────┘ │ │
285+
│ └────────┼─────────────┼─────────────────┼────────────┘ │
286+
│ │ │ │ │
287+
│ ┌────────▼─────────────▼─────────────────▼────────────┐ │
288+
│ │ Unified Store │ │
289+
│ │ (merge data from all protocols) │ │
290+
│ └──────────────────────┬──────────────────────────────┘ │
291+
│ │ │
292+
│ ┌──────────────────────▼──────────────────────────────┐ │
293+
│ │ UI │ │
294+
│ └─────────────────────────────────────────────────────┘ │
295+
│ │
296+
└─────────────────────────────────────────────────────────────┘
297+
```
298+
299+
## Best Practices
300+
301+
### 1. Choose the Right Protocol for Each Task
302+
303+
| Task | Best Protocol | Why |
304+
|------|---------------|-----|
305+
| Store private data | Solid | Access control |
306+
| Broadcast publicly | Nostr | Censorship-resistant |
307+
| Social features | ActivityPub | Mature ecosystem |
308+
| Identity | DID | Portable |
309+
310+
### 2. Link Data Across Protocols
311+
312+
Always include references:
313+
- Nostr events → tag with Solid URLs
314+
- Solid resources → include Nostr event IDs
315+
- ActivityPub activities → reference DID
316+
317+
### 3. Handle Consistency
318+
319+
Protocols may have different states:
320+
- Use timestamps to resolve conflicts
321+
- Treat Solid as source of truth for structured data
322+
- Treat Nostr as source of truth for events
323+
324+
### 4. Graceful Degradation
325+
326+
Not all users will be on all protocols:
327+
- Solid-only users still get data
328+
- Nostr-only users still get broadcasts
329+
- ActivityPub-only users still get federation
330+
331+
## Example: Social Bookmarking App
332+
333+
A complete example combining all protocols:
334+
335+
```javascript
336+
class SocialBookmarks {
337+
// Save bookmark to Solid, broadcast via Nostr, federate via AP
338+
async saveBookmark(url, title, tags) {
339+
const id = crypto.randomUUID();
340+
341+
// 1. Store in Solid (structured, private)
342+
const bookmark = {
343+
'@type': 'Bookmark',
344+
id,
345+
url,
346+
title,
347+
tags,
348+
created: new Date().toISOString()
349+
};
350+
await this.solid.save(`/bookmarks/${id}`, bookmark);
351+
352+
// 2. Broadcast via Nostr (public, real-time)
353+
await this.nostr.publish({
354+
kind: 30017, // Bookmarks
355+
content: JSON.stringify({ url, title }),
356+
tags: [
357+
['d', id],
358+
['r', url],
359+
...tags.map(t => ['t', t])
360+
]
361+
});
362+
363+
// 3. Federate via ActivityPub (social reach)
364+
await this.activitypub.post({
365+
type: 'Create',
366+
object: {
367+
type: 'Note',
368+
content: `Bookmarked: ${title}\n${url}`,
369+
tag: tags.map(t => ({ type: 'Hashtag', name: t }))
370+
}
371+
});
372+
373+
return id;
374+
}
375+
}
376+
```
377+
378+
## See Also
379+
380+
- [SAND Architecture](/concepts/architecture) — How protocols fit together
381+
- [Authentication](/guides/authentication) — Auth across protocols
382+
- [Ditto](/projects/ditto) — Production bridge implementation
383+
- [MCP](/projects/mcp) — Connect AI to multiple protocols

0 commit comments

Comments
 (0)