Skip to main content

max / makenotwork

31.0 KB · 975 lines History Blame Raw
1 <!DOCTYPE html>
2 <html lang="en">
3 <head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <title>MNW SyncKit — Pitch</title>
7 <style>
8 @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=Lato:wght@300;400;700&family=Young+Serif&display=swap');
9
10 :root {
11 --bg: #ede8e1;
12 --bg-warm: #e4ddd3;
13 --bg-deep: #d8cfc3;
14 --bg-code: #2d2a26;
15 --text: #3d3530;
16 --text-secondary: #5d524a;
17 --text-muted: #8a7e74;
18 --accent: #6c5ce7;
19 --accent-light: #8577ed;
20 --green: #4caf50;
21 --red: #c0392b;
22 --border: #c9c0b4;
23 --border-dark: #a89d91;
24 --code-text: #e8e0d6;
25 --code-keyword: #c792ea;
26 --code-string: #c3e88d;
27 --code-comment: #7a7062;
28 --code-type: #ffcb6b;
29 --code-fn: #82aaff;
30 }
31
32 * { margin: 0; padding: 0; box-sizing: border-box; }
33
34 @page { size: letter; margin: 0; }
35
36 body {
37 font-family: 'Lato', sans-serif;
38 color: var(--text);
39 background: var(--bg);
40 line-height: 1.6;
41 -webkit-print-color-adjust: exact;
42 print-color-adjust: exact;
43 }
44
45 .page {
46 width: 8.5in;
47 min-height: 11in;
48 margin: 0 auto;
49 padding: 0.55in 0.75in;
50 background: var(--bg);
51 page-break-after: always;
52 position: relative;
53 }
54
55 .page:last-child { page-break-after: auto; }
56
57 /* ── Hero ── */
58
59 .hero {
60 text-align: center;
61 padding: 0.25in 0 0.2in;
62 border-bottom: 2px solid var(--border);
63 margin-bottom: 0.22in;
64 }
65
66 .hero h1 {
67 font-family: 'Young Serif', serif;
68 font-size: 42px;
69 font-weight: 400;
70 color: var(--text);
71 margin-bottom: 2px;
72 letter-spacing: -0.5px;
73 }
74
75 .hero h1 .mark { color: var(--accent); }
76
77 .hero .tagline {
78 font-family: 'IBM Plex Mono', monospace;
79 font-size: 13.5px;
80 color: var(--text-secondary);
81 letter-spacing: 0.2px;
82 }
83
84 .hero .sub {
85 font-size: 12px;
86 color: var(--text-muted);
87 margin-top: 6px;
88 }
89
90 /* ── Sections ── */
91
92 h2 {
93 font-family: 'IBM Plex Mono', monospace;
94 font-size: 13.5px;
95 font-weight: 600;
96 text-transform: uppercase;
97 letter-spacing: 1.5px;
98 color: var(--accent);
99 margin-bottom: 10px;
100 padding-bottom: 4px;
101 border-bottom: 1px solid var(--border);
102 }
103
104 h3 {
105 font-family: 'IBM Plex Mono', monospace;
106 font-size: 12px;
107 font-weight: 600;
108 color: var(--text);
109 margin-bottom: 4px;
110 margin-top: 12px;
111 }
112
113 h3:first-child { margin-top: 0; }
114
115 .intro {
116 font-size: 14px;
117 line-height: 1.7;
118 margin-bottom: 0.2in;
119 }
120
121 /* ── Stats ── */
122
123 .stats {
124 display: grid;
125 grid-template-columns: repeat(4, 1fr);
126 gap: 10px;
127 margin-bottom: 0.22in;
128 }
129
130 .stat {
131 text-align: center;
132 background: var(--bg-warm);
133 border: 1px solid var(--border);
134 border-radius: 6px;
135 padding: 10px 6px;
136 }
137
138 .stat .num {
139 font-family: 'Young Serif', serif;
140 font-size: 26px;
141 color: var(--accent);
142 display: block;
143 line-height: 1;
144 margin-bottom: 3px;
145 }
146
147 .stat .label {
148 font-family: 'IBM Plex Mono', monospace;
149 font-size: 9px;
150 color: var(--text-muted);
151 text-transform: uppercase;
152 letter-spacing: 0.4px;
153 }
154
155 /* ── Code block ── */
156
157 .code-block {
158 background: var(--bg-code);
159 border-radius: 6px;
160 padding: 14px 16px;
161 margin-bottom: 0.2in;
162 overflow-x: auto;
163 border: 1px solid #444;
164 }
165
166 .code-block pre {
167 font-family: 'IBM Plex Mono', monospace;
168 font-size: 10.5px;
169 line-height: 1.55;
170 color: var(--code-text);
171 white-space: pre;
172 margin: 0;
173 }
174
175 .code-block .kw { color: var(--code-keyword); }
176 .code-block .str { color: var(--code-string); }
177 .code-block .cmt { color: var(--code-comment); }
178 .code-block .ty { color: var(--code-type); }
179 .code-block .fn { color: var(--code-fn); }
180
181 .code-label {
182 font-family: 'IBM Plex Mono', monospace;
183 font-size: 10px;
184 color: var(--text-muted);
185 text-transform: uppercase;
186 letter-spacing: 0.5px;
187 margin-bottom: 4px;
188 display: block;
189 }
190
191 /* ── Feature grid ── */
192
193 .features {
194 display: grid;
195 grid-template-columns: 1fr 1fr;
196 gap: 12px;
197 margin-bottom: 0.2in;
198 }
199
200 .feature-card {
201 background: var(--bg-warm);
202 border: 1px solid var(--border);
203 border-radius: 6px;
204 padding: 11px 13px;
205 }
206
207 .feature-card h3 {
208 margin: 0 0 4px 0;
209 font-size: 11.5px;
210 color: var(--accent);
211 }
212
213 .feature-card ul { list-style: none; padding: 0; }
214
215 .feature-card li {
216 font-size: 11px;
217 line-height: 1.4;
218 color: var(--text-secondary);
219 padding: 1.5px 0 1.5px 12px;
220 position: relative;
221 }
222
223 .feature-card li::before {
224 content: '';
225 position: absolute;
226 left: 0;
227 top: 7px;
228 width: 5px;
229 height: 5px;
230 background: var(--accent);
231 border-radius: 50%;
232 }
233
234 /* ── Feature section (full width, 2-col) ── */
235
236 .feature-section { margin-bottom: 0.18in; }
237
238 .feature-section ul {
239 list-style: none;
240 padding: 0;
241 columns: 2;
242 column-gap: 22px;
243 }
244
245 .feature-section li {
246 font-size: 11.5px;
247 line-height: 1.4;
248 color: var(--text-secondary);
249 padding: 2px 0 2px 12px;
250 position: relative;
251 break-inside: avoid;
252 }
253
254 .feature-section li::before {
255 content: '';
256 position: absolute;
257 left: 0;
258 top: 7.5px;
259 width: 5px;
260 height: 5px;
261 background: var(--accent);
262 border-radius: 50%;
263 }
264
265 /* ── Highlight box ── */
266
267 .highlight-box {
268 background: var(--bg-warm);
269 border-left: 3px solid var(--accent);
270 padding: 10px 14px;
271 margin-bottom: 0.18in;
272 border-radius: 0 6px 6px 0;
273 }
274
275 .highlight-box p {
276 font-size: 12px;
277 line-height: 1.55;
278 color: var(--text-secondary);
279 }
280
281 .highlight-box strong { color: var(--text); }
282
283 /* ── API table ── */
284
285 .api-table {
286 width: 100%;
287 border-collapse: collapse;
288 margin-bottom: 0.18in;
289 font-size: 10.5px;
290 }
291
292 .api-table th {
293 font-family: 'IBM Plex Mono', monospace;
294 font-size: 9.5px;
295 font-weight: 600;
296 text-align: left;
297 text-transform: uppercase;
298 letter-spacing: 0.5px;
299 padding: 6px 7px;
300 background: var(--bg-deep);
301 color: var(--text);
302 border-bottom: 1px solid var(--border-dark);
303 }
304
305 .api-table td {
306 padding: 4px 7px;
307 border-bottom: 1px solid var(--border);
308 color: var(--text-secondary);
309 vertical-align: top;
310 }
311
312 .api-table tr:last-child td { border-bottom: none; }
313
314 .api-table code {
315 font-family: 'IBM Plex Mono', monospace;
316 font-size: 10px;
317 color: var(--text);
318 background: var(--bg-deep);
319 padding: 1px 4px;
320 border-radius: 3px;
321 }
322
323 /* ── Comparison table ── */
324
325 .comparison {
326 width: 100%;
327 border-collapse: collapse;
328 margin-bottom: 0.18in;
329 font-size: 11px;
330 }
331
332 .comparison th {
333 font-family: 'IBM Plex Mono', monospace;
334 font-size: 10px;
335 font-weight: 600;
336 text-align: left;
337 padding: 6px 7px;
338 background: var(--bg-deep);
339 color: var(--text);
340 border-bottom: 1px solid var(--border-dark);
341 }
342
343 .comparison td {
344 padding: 5px 7px;
345 border-bottom: 1px solid var(--border);
346 color: var(--text-secondary);
347 vertical-align: top;
348 }
349
350 .comparison .check { color: var(--green); font-weight: 700; font-size: 12px; }
351 .comparison .dash { color: var(--text-muted); }
352 .comparison tr:last-child td { border-bottom: none; }
353
354 /* ── Crypto diagram ── */
355
356 .crypto-flow {
357 display: grid;
358 grid-template-columns: repeat(3, 1fr);
359 gap: 10px;
360 margin-bottom: 0.15in;
361 }
362
363 .crypto-step {
364 background: var(--bg-warm);
365 border: 1px solid var(--border);
366 border-radius: 6px;
367 padding: 10px 12px;
368 text-align: center;
369 }
370
371 .crypto-step .step-num {
372 font-family: 'IBM Plex Mono', monospace;
373 font-size: 9px;
374 color: var(--accent);
375 text-transform: uppercase;
376 letter-spacing: 0.5px;
377 display: block;
378 margin-bottom: 3px;
379 }
380
381 .crypto-step .step-title {
382 font-family: 'IBM Plex Mono', monospace;
383 font-size: 11px;
384 font-weight: 600;
385 color: var(--text);
386 display: block;
387 margin-bottom: 4px;
388 }
389
390 .crypto-step .step-detail {
391 font-size: 10px;
392 color: var(--text-muted);
393 line-height: 1.35;
394 }
395
396 /* ── Pricing ── */
397
398 .pricing {
399 display: grid;
400 grid-template-columns: repeat(4, 1fr);
401 gap: 10px;
402 margin-bottom: 0.18in;
403 }
404
405 .tier {
406 background: var(--bg-warm);
407 border: 1px solid var(--border);
408 border-radius: 6px;
409 padding: 12px 10px;
410 text-align: center;
411 }
412
413 .tier.featured {
414 border-color: var(--accent);
415 border-width: 2px;
416 }
417
418 .tier .tier-name {
419 font-family: 'IBM Plex Mono', monospace;
420 font-size: 10px;
421 font-weight: 600;
422 color: var(--text);
423 text-transform: uppercase;
424 letter-spacing: 0.5px;
425 display: block;
426 margin-bottom: 4px;
427 }
428
429 .tier .tier-price {
430 font-family: 'Young Serif', serif;
431 font-size: 24px;
432 color: var(--accent);
433 display: block;
434 line-height: 1;
435 margin-bottom: 4px;
436 }
437
438 .tier .tier-period {
439 font-size: 10px;
440 color: var(--text-muted);
441 display: block;
442 margin-bottom: 6px;
443 }
444
445 .tier .tier-desc {
446 font-size: 9.5px;
447 color: var(--text-secondary);
448 line-height: 1.35;
449 }
450
451 /* ── Two-column ── */
452
453 .two-col {
454 display: grid;
455 grid-template-columns: 1fr 1fr;
456 gap: 12px;
457 margin-bottom: 0.18in;
458 }
459
460 .two-col .col {
461 background: var(--bg-warm);
462 border: 1px solid var(--border);
463 border-radius: 6px;
464 padding: 11px 13px;
465 }
466
467 .two-col .col h3 {
468 margin: 0 0 4px 0;
469 font-size: 11.5px;
470 color: var(--accent);
471 }
472
473 .two-col .col p {
474 font-size: 11px;
475 line-height: 1.45;
476 color: var(--text-secondary);
477 }
478
479 /* ── Footer ── */
480
481 .footer {
482 position: absolute;
483 bottom: 0.4in;
484 left: 0.75in;
485 right: 0.75in;
486 display: flex;
487 justify-content: space-between;
488 align-items: center;
489 padding-top: 8px;
490 border-top: 1px solid var(--border);
491 }
492
493 .footer .left, .footer .right {
494 font-family: 'IBM Plex Mono', monospace;
495 font-size: 10px;
496 color: var(--text-muted);
497 }
498
499 .footer-inline {
500 display: flex;
501 justify-content: space-between;
502 align-items: center;
503 padding-top: 8px;
504 border-top: 1px solid var(--border);
505 margin-top: auto;
506 }
507
508 .footer-inline .left, .footer-inline .right {
509 font-family: 'IBM Plex Mono', monospace;
510 font-size: 10px;
511 color: var(--text-muted);
512 }
513
514 @media print { body { background: white; } .page { box-shadow: none; } }
515 @media screen { body { background: #ccc; padding: 20px 0; } .page { box-shadow: 0 2px 20px rgba(0,0,0,0.15); margin-bottom: 20px; } }
516 </style>
517 </head>
518 <body>
519
520 <!-- ═══════════════════════════════════════════════════════════════════════ -->
521 <!-- PAGE 1 -->
522 <!-- ═══════════════════════════════════════════════════════════════════════ -->
523 <div class="page">
524
525 <div class="hero">
526 <h1>MNW SyncKit<span class="mark">.</span></h1>
527 <div class="tagline">E2E encrypted cloud sync for indie apps. Ship it in an afternoon.</div>
528 <div class="sub">A Rust SDK + hosted API. Cursor-based changelog sync, blob storage, device management. Zero-knowledge server.</div>
529 </div>
530
531 <div class="stats">
532 <div class="stat">
533 <span class="num">20+</span>
534 <span class="label">SDK methods</span>
535 </div>
536 <div class="stat">
537 <span class="num">E2E</span>
538 <span class="label">Encrypted by default</span>
539 </div>
540 <div class="stat">
541 <span class="num">3</span>
542 <span class="label">Apps shipping today</span>
543 </div>
544 <div class="stat">
545 <span class="num">0</span>
546 <span class="label">Plaintext on server</span>
547 </div>
548 </div>
549
550 <div class="intro">
551 SyncKit is developer infrastructure for cloud sync. You bring your own schema &mdash; table names, row IDs, and data shapes are opaque to the server. Every payload is encrypted client-side with XChaCha20-Poly1305 before it leaves the device. The server stores ciphertext. No schema migrations on our end, no data access requests to worry about, no plaintext in your database logs.
552 </div>
553
554 <span class="code-label">Five lines to sync</span>
555 <div class="code-block"><pre><span class="kw">let</span> client = <span class="ty">SyncKitClient</span>::<span class="fn">new</span>(<span class="ty">SyncKitConfig</span> {
556 server_url: <span class="str">"https://makenot.work"</span>.into(),
557 api_key: <span class="str">"your-api-key"</span>.into(),
558 });
559 <span class="kw">let</span> (user_id, app_id) = client.<span class="fn">authenticate</span>(<span class="str">"user@example.com"</span>, <span class="str">"pass"</span>).<span class="kw">await</span>?;
560 client.<span class="fn">setup_encryption_new</span>(<span class="str">"pass"</span>).<span class="kw">await</span>?; <span class="cmt">// generates + wraps master key</span>
561 <span class="kw">let</span> device = client.<span class="fn">register_device</span>(<span class="str">"MacBook"</span>, <span class="str">"macos"</span>).<span class="kw">await</span>?;
562 <span class="kw">let</span> cursor = client.<span class="fn">push</span>(device.id, changes).<span class="kw">await</span>?; <span class="cmt">// encrypted, retried, done</span></pre></div>
563
564 <h2>What You Get</h2>
565
566 <div class="features">
567 <div class="feature-card">
568 <h3>Changelog Sync</h3>
569 <ul>
570 <li>Push/pull with cursor-based pagination</li>
571 <li>INSERT, UPDATE, DELETE operations</li>
572 <li>Any JSON payload &mdash; server is schema-agnostic</li>
573 <li>500 changes per push, 500 per pull page</li>
574 <li>Monotonic sequence numbers, deterministic ordering</li>
575 </ul>
576 </div>
577 <div class="feature-card">
578 <h3>E2E Encryption</h3>
579 <ul>
580 <li>XChaCha20-Poly1305 (256-bit, AEAD)</li>
581 <li>Argon2id key derivation (OWASP parameters)</li>
582 <li>Random 24-byte nonce per entry (no reuse risk)</li>
583 <li>40 bytes overhead per encrypted payload</li>
584 <li>Server never sees plaintext data or master key</li>
585 </ul>
586 </div>
587 <div class="feature-card">
588 <h3>Blob Storage</h3>
589 <ul>
590 <li>Encrypted file upload/download via presigned S3 URLs</li>
591 <li>Content-addressed deduplication (SHA-256 hash)</li>
592 <li>3-step flow: request URL, upload, confirm</li>
593 <li>Up to 500 MB per blob</li>
594 <li>Binary encryption (no base64 overhead on wire)</li>
595 </ul>
596 </div>
597 <div class="feature-card">
598 <h3>Device &amp; Account Management</h3>
599 <ul>
600 <li>Register devices by name + platform (upsert semantics)</li>
601 <li>Per-device cursor tracking on client side</li>
602 <li>List and delete devices via API</li>
603 <li>Change password with automatic key re-wrapping</li>
604 <li>App registration for OAuth client credentials</li>
605 </ul>
606 </div>
607 </div>
608
609 <h2>Encryption Model</h2>
610
611 <div class="crypto-flow">
612 <div class="crypto-step">
613 <span class="step-num">Step 1</span>
614 <span class="step-title">Key Derivation</span>
615 <div class="step-detail">User password + random 32-byte salt. Argon2id (64 MB, 3 iterations). Produces a 256-bit wrapping key.</div>
616 </div>
617 <div class="crypto-step">
618 <span class="step-num">Step 2</span>
619 <span class="step-title">Key Wrapping</span>
620 <div class="step-detail">Random 256-bit master key encrypted with the wrapping key. Envelope (salt + nonce + ciphertext) stored on server. New salt per wrap.</div>
621 </div>
622 <div class="crypto-step">
623 <span class="step-num">Step 3</span>
624 <span class="step-title">Data Encryption</span>
625 <div class="step-detail">Each changelog entry encrypted with master key. Fresh random nonce per entry. Cached in OS keychain after first unlock.</div>
626 </div>
627 </div>
628
629 <div class="highlight-box">
630 <p><strong>Second device?</strong> Call <code>setup_encryption_existing(password)</code>. The SDK downloads the encrypted envelope from the server, derives the wrapping key from the password, unwraps the master key, and caches it in the OS keychain. One password prompt, then it's automatic.</p>
631 </div>
632
633 <div class="footer">
634 <div class="left">MNW SyncKit &mdash; v0.3.1</div>
635 <div class="right">makenot.work</div>
636 </div>
637
638 </div>
639
640 <!-- ═══════════════════════════════════════════════════════════════════════ -->
641 <!-- PAGE 2 -->
642 <!-- ═══════════════════════════════════════════════════════════════════════ -->
643 <div class="page">
644
645 <h2>Server API</h2>
646
647 <table class="api-table">
648 <tr>
649 <th style="width:12%">Method</th>
650 <th style="width:35%">Endpoint</th>
651 <th style="width:13%">Auth</th>
652 <th style="width:40%">Description</th>
653 </tr>
654 <tr>
655 <td><code>POST</code></td>
656 <td><code>/api/sync/auth</code></td>
657 <td>Public</td>
658 <td>Authenticate with MNW credentials + API key. Returns JWT (7-day expiry).</td>
659 </tr>
660 <tr>
661 <td><code>POST</code></td>
662 <td><code>/api/sync/push</code></td>
663 <td>JWT</td>
664 <td>Push up to 500 encrypted changelog entries. Returns new cursor.</td>
665 </tr>
666 <tr>
667 <td><code>POST</code></td>
668 <td><code>/api/sync/pull</code></td>
669 <td>JWT</td>
670 <td>Pull changes since cursor. Returns entries + new cursor + has_more flag.</td>
671 </tr>
672 <tr>
673 <td><code>GET</code></td>
674 <td><code>/api/sync/status</code></td>
675 <td>JWT</td>
676 <td>Total change count and latest cursor for this user/app.</td>
677 </tr>
678 <tr>
679 <td><code>POST</code></td>
680 <td><code>/api/sync/devices</code></td>
681 <td>JWT</td>
682 <td>Register or update a device. Upsert by (app, user, name).</td>
683 </tr>
684 <tr>
685 <td><code>GET</code></td>
686 <td><code>/api/sync/devices</code></td>
687 <td>JWT</td>
688 <td>List all devices for the authenticated user.</td>
689 </tr>
690 <tr>
691 <td><code>PUT</code></td>
692 <td><code>/api/sync/keys</code></td>
693 <td>JWT</td>
694 <td>Store encrypted master key envelope (max 4 KB).</td>
695 </tr>
696 <tr>
697 <td><code>GET</code></td>
698 <td><code>/api/sync/keys</code></td>
699 <td>JWT</td>
700 <td>Retrieve encrypted key envelope + version number.</td>
701 </tr>
702 <tr>
703 <td><code>POST</code></td>
704 <td><code>/api/sync/blobs/upload</code></td>
705 <td>JWT</td>
706 <td>Request presigned S3 upload URL. Returns already_exists flag.</td>
707 </tr>
708 <tr>
709 <td><code>POST</code></td>
710 <td><code>/api/sync/blobs/confirm</code></td>
711 <td>JWT</td>
712 <td>Confirm blob upload. Idempotent.</td>
713 </tr>
714 <tr>
715 <td><code>POST</code></td>
716 <td><code>/api/sync/blobs/download</code></td>
717 <td>JWT</td>
718 <td>Request presigned S3 download URL by content hash.</td>
719 </tr>
720 </table>
721
722 <h2>SDK Surface</h2>
723
724 <div class="two-col">
725 <div class="col">
726 <h3>Core Types</h3>
727 <p><code>SyncKitClient</code> &mdash; thread-safe (<code>Send + Sync</code>), share via <code>Arc</code>. All methods take <code>&amp;self</code>. Internal <code>parking_lot::RwLock</code> for session and master key. Single <code>reqwest::Client</code> with connection pooling.</p>
728 </div>
729 <div class="col">
730 <h3>Change Model</h3>
731 <p><code>ChangeEntry { table, op, row_id, timestamp, data }</code> &mdash; your app defines table names and row IDs. <code>ChangeOp</code>: Insert, Update, Delete. Data is <code>Option&lt;serde_json::Value&gt;</code>, automatically encrypted before push.</p>
732 </div>
733 </div>
734
735 <span class="code-label">Pull, process, push</span>
736 <div class="code-block"><pre><span class="cmt">// Pull remote changes (auto-decrypted)</span>
737 <span class="kw">let</span> (changes, new_cursor, has_more) = client.<span class="fn">pull</span>(device.id, cursor).<span class="kw">await</span>?;
738 <span class="kw">for</span> change <span class="kw">in</span> &amp;changes {
739 <span class="kw">match</span> change.op {
740 <span class="ty">ChangeOp</span>::Insert =&gt; db.<span class="fn">upsert</span>(&amp;change.table, &amp;change.row_id, &amp;change.data),
741 <span class="ty">ChangeOp</span>::Update =&gt; db.<span class="fn">upsert</span>(&amp;change.table, &amp;change.row_id, &amp;change.data),
742 <span class="ty">ChangeOp</span>::Delete =&gt; db.<span class="fn">delete</span>(&amp;change.table, &amp;change.row_id),
743 }
744 }
745
746 <span class="cmt">// Push local changes (auto-encrypted)</span>
747 <span class="kw">let</span> local_changes = db.<span class="fn">pending_changes_since</span>(last_push);
748 <span class="kw">let</span> cursor = client.<span class="fn">push</span>(device.id, local_changes).<span class="kw">await</span>?;</pre></div>
749
750 <h2>Resilience</h2>
751
752 <div class="feature-section">
753 <ul>
754 <li>Automatic retry with exponential backoff (1s, 2s, 4s) for transient failures</li>
755 <li>Transient: network errors, 5xx, 429 rate limits</li>
756 <li>Permanent: 4xx client errors, auth failures, crypto errors &mdash; returned immediately</li>
757 <li>30-second JWT expiry buffer (proactive re-auth before token expires mid-request)</li>
758 <li>HTTP connection pooling (5 idle per host, 90s pool timeout)</li>
759 <li>OS keychain integration (macOS Keychain, Linux secret-service, Windows Credential Manager)</li>
760 <li>Master key zeroized on drop (volatile memory writes)</li>
761 </ul>
762 </div>
763
764 <h2>Blob Workflow</h2>
765
766 <div class="code-block"><pre><span class="cmt">// Upload: hash locally, request URL, upload encrypted, confirm</span>
767 <span class="kw">let</span> resp = client.<span class="fn">blob_upload_url</span>(&amp;hash, size).<span class="kw">await</span>?;
768 <span class="kw">if</span> !resp.already_exists {
769 client.<span class="fn">blob_upload</span>(&amp;resp.upload_url, data).<span class="kw">await</span>?; <span class="cmt">// encrypted on wire</span>
770 }
771 client.<span class="fn">blob_confirm</span>(&amp;hash, size).<span class="kw">await</span>?;
772
773 <span class="cmt">// Download: request URL, download + decrypt</span>
774 <span class="kw">let</span> url = client.<span class="fn">blob_download_url</span>(&amp;hash).<span class="kw">await</span>?;
775 <span class="kw">let</span> plaintext = client.<span class="fn">blob_download</span>(&amp;url).<span class="kw">await</span>?;</pre></div>
776
777 <div class="highlight-box">
778 <p>Blobs are content-addressed by SHA-256 hash. If <code>already_exists</code> returns true, the file is already on the server &mdash; skip the upload. Binary encryption uses raw bytes (no base64 wrapping), so overhead is exactly 40 bytes per file regardless of size.</p>
779 </div>
780
781 <div class="footer">
782 <div class="left">MNW SyncKit &mdash; v0.3.1</div>
783 <div class="right">makenot.work</div>
784 </div>
785
786 </div>
787
788 <!-- ═══════════════════════════════════════════════════════════════════════ -->
789 <!-- PAGE 3 -->
790 <!-- ═══════════════════════════════════════════════════════════════════════ -->
791 <div class="page" style="display: flex; flex-direction: column;">
792
793 <h2>Integration Pattern</h2>
794
795 <span class="code-label">App startup (session restore)</span>
796 <div class="code-block"><pre><span class="cmt">// Try keychain first (no password prompt needed)</span>
797 <span class="kw">if</span> client.<span class="fn">try_load_key_from_keychain</span>().<span class="fn">is_ok</span>() {
798 <span class="cmt">// Master key cached from previous session &mdash; ready to sync</span>
799 } <span class="kw">else if</span> client.<span class="fn">has_server_key</span>().<span class="kw">await</span>? {
800 <span class="cmt">// Existing account on new device &mdash; ask for password once</span>
801 client.<span class="fn">setup_encryption_existing</span>(&amp;password).<span class="kw">await</span>?;
802 } <span class="kw">else</span> {
803 <span class="cmt">// First device ever &mdash; generate master key, wrap with password</span>
804 client.<span class="fn">setup_encryption_new</span>(&amp;password).<span class="kw">await</span>?;
805 }
806
807 <span class="cmt">// OAuth2 PKCE also supported (browser-based auth)</span>
808 <span class="kw">let</span> url = client.<span class="fn">build_authorize_url</span>(port, state, challenge);
809 <span class="cmt">// ... redirect user, receive code ...</span>
810 <span class="kw">let</span> (user_id, app_id) = client.<span class="fn">authenticate_with_code</span>(&amp;code, &amp;verifier, port).<span class="kw">await</span>?;</pre></div>
811
812 <h2>What Ships Today</h2>
813
814 <div class="features">
815 <div class="feature-card">
816 <h3>Three Apps Shipping</h3>
817 <ul>
818 <li>GoingsOn: tasks, projects, events, contacts, email accounts</li>
819 <li>Balanced Breakfast: feeds, read/star state, preferences</li>
820 <li>audiofiles: sample metadata, VFS trees, tags, collections</li>
821 </ul>
822 </div>
823 <div class="feature-card">
824 <h3>Developer Experience</h3>
825 <ul>
826 <li>Typical integration: under a day from zero to syncing</li>
827 <li>Schema-agnostic &mdash; no server-side migrations needed</li>
828 <li>OAuth2 PKCE for browser-based auth (desktop + mobile)</li>
829 <li>Password auth also supported for headless / CLI apps</li>
830 </ul>
831 </div>
832 </div>
833
834 <h2>How It Compares</h2>
835
836 <table class="comparison">
837 <tr>
838 <th style="width:24%">Capability</th>
839 <th style="width:15.2%">SyncKit</th>
840 <th style="width:15.2%">Firebase</th>
841 <th style="width:15.2%">Supabase</th>
842 <th style="width:15.2%">iCloud</th>
843 <th style="width:15.2%">Custom</th>
844 </tr>
845 <tr>
846 <td>E2E encrypted</td>
847 <td><span class="check">Yes</span></td>
848 <td><span class="dash">&mdash;</span></td>
849 <td><span class="dash">&mdash;</span></td>
850 <td>Partial</td>
851 <td>You build it</td>
852 </tr>
853 <tr>
854 <td>Zero-knowledge server</td>
855 <td><span class="check">Yes</span></td>
856 <td><span class="dash">&mdash;</span></td>
857 <td><span class="dash">&mdash;</span></td>
858 <td><span class="dash">&mdash;</span></td>
859 <td>You build it</td>
860 </tr>
861 <tr>
862 <td>Schema-agnostic</td>
863 <td><span class="check">Yes</span></td>
864 <td>doc-store schema (Firestore enforces document model)</td>
865 <td>Postgres</td>
866 <td>CloudKit</td>
867 <td>You build it</td>
868 </tr>
869 <tr>
870 <td>Blob storage</td>
871 <td><span class="check">S3</span></td>
872 <td><span class="check">Yes</span></td>
873 <td><span class="check">Yes</span></td>
874 <td><span class="check">Yes</span></td>
875 <td>You build it</td>
876 </tr>
877 <tr>
878 <td>Rust SDK</td>
879 <td><span class="check">Yes</span></td>
880 <td><span class="dash">&mdash;</span></td>
881 <td><span class="dash">&mdash;</span></td>
882 <td><span class="dash">&mdash;</span></td>
883 <td>You build it</td>
884 </tr>
885 <tr>
886 <td>Cross-platform</td>
887 <td><span class="check">Yes</span></td>
888 <td>Mobile focus</td>
889 <td><span class="check">Yes</span></td>
890 <td>Apple only</td>
891 <td>You build it</td>
892 </tr>
893 <tr>
894 <td>No vendor lock-in</td>
895 <td><span class="check">Yes</span></td>
896 <td><span class="dash">&mdash;</span></td>
897 <td><span class="check">Yes</span></td>
898 <td><span class="dash">&mdash;</span></td>
899 <td><span class="check">Yes</span></td>
900 </tr>
901 <tr>
902 <td>Source-available</td>
903 <td><span class="check">Yes</span></td>
904 <td><span class="dash">&mdash;</span></td>
905 <td><span class="check">Yes</span></td>
906 <td><span class="dash">&mdash;</span></td>
907 <td>Yours</td>
908 </tr>
909 <tr>
910 <td>No per-user pricing</td>
911 <td><span class="check">Yes</span></td>
912 <td>Pay per use</td>
913 <td>Pay per use</td>
914 <td>Free (Apple)</td>
915 <td>Your infra</td>
916 </tr>
917 <tr>
918 <td>Time to integrate</td>
919 <td>Hours</td>
920 <td>Hours</td>
921 <td>Hours</td>
922 <td>Days</td>
923 <td>Weeks</td>
924 </tr>
925 </table>
926
927 <h2>Pricing</h2>
928
929 <p style="margin-bottom: 16px;">SyncKit is a B2B service: customers are app developers, not end users. Pricing is usage-based on storage and transfer — no per-user fees, no per-seat licenses. Your end users authenticate with their own MNW accounts at no cost to you.</p>
930
931 <p style="margin-bottom: 20px;"><strong>Monthly cost = (GB stored &times; $0.15) + (burst multiplier &times; GB stored &times; $0.03)</strong></p>
932
933 <p style="margin-bottom: 16px;"><em>Burst</em> is the transfer budget relative to storage. <strong>Simple mode</strong> uses a default burst of 5&times;; <strong>Builder mode</strong> lets you choose any multiplier. Same billing engine for both.</p>
934
935 <div class="pricing">
936 <div class="tier">
937 <span class="tier-name">Config / state sync</span>
938 <span class="tier-price">$0.30</span>
939 <span class="tier-period">/ month</span>
940 <div class="tier-desc">Example: 1 GB stored, 5&times; burst. Settings, read state, light metadata.</div>
941 </div>
942 <div class="tier featured">
943 <span class="tier-name">Productivity app</span>
944 <span class="tier-price">$3.00</span>
945 <span class="tier-period">/ month</span>
946 <div class="tier-desc">Example: 10 GB stored, 5&times; burst. Tasks, contacts, light files.</div>
947 </div>
948 <div class="tier">
949 <span class="tier-name">Media metadata</span>
950 <span class="tier-price">$15.00</span>
951 <span class="tier-period">/ month</span>
952 <div class="tier-desc">Example: 50 GB stored, 5&times; burst. Sample libraries, playlists.</div>
953 </div>
954 <div class="tier">
955 <span class="tier-name">Large file sync</span>
956 <span class="tier-price">$60.00</span>
957 <span class="tier-period">/ month</span>
958 <div class="tier-desc">Example: 200 GB stored, 5&times; burst. Audio, video, courses.</div>
959 </div>
960 </div>
961
962 <div class="highlight-box">
963 <p><strong>No per-user fees. No per-request fees. No egress charges.</strong> Your end users authenticate with their own MNW accounts. Pricing scales with what you actually store and transfer &mdash; not with how many users or devices sync through your app. No overages: the bill is known before the billing period starts; at limits, sync degrades but the bill does not change. Source-available under PolyForm Noncommercial 1.0.0.</p>
964 </div>
965
966 <div class="footer-inline" style="margin-top: 12px;">
967 <div class="left">MNW SyncKit &mdash; v0.3.1 &mdash; Rust SDK + hosted API</div>
968 <div class="right">makenot.work</div>
969 </div>
970
971 </div>
972
973 </body>
974 </html>
975