Skip to main content

max / makenotwork

10.0 KB · 385 lines History Blame Raw
1 # SyncKit Cloud Sync
2
3 SyncKit provides cloud sync and encrypted data storage for desktop applications: device registration, changelog-based sync, E2E encryption, and content-addressed blob storage, all through a REST API backed by your Makenotwork account.
4
5 ## Concepts
6
7 - **Sync App**: A registered application on Makenotwork. Each app has its own API key, data namespace, and device list.
8 - **Device**: A named installation of your app (e.g., "Alice's MacBook"). Each device syncs independently.
9 - **Changelog**: An append-only log of changes. Each entry records a table name, operation, row ID, timestamp, and encrypted data blob.
10 - **Cursor**: An opaque position in the changelog. Pull from cursor to get only new changes.
11 - **Blob**: A content-addressed encrypted file stored in S3. Referenced by SHA-256 hash.
12
13 ## Creating a Sync App
14
15 From the dashboard, go to Settings and create a new SyncKit app. You receive an API key (shown only once; regenerating it invalidates existing clients). Optionally link the app to a project or item.
16
17 ## Authentication
18
19 ### Direct Authentication
20
21 For desktop apps where the user enters their Makenotwork credentials:
22
23 ```
24 POST /api/sync/auth
25 Content-Type: application/json
26
27 {
28 "email": "user@example.com",
29 "password": "their-password",
30 "api_key": "your-app-api-key"
31 }
32 ```
33
34 Response:
35
36 ```json
37 {
38 "token": "eyJ...",
39 "user_id": "550e8400-...",
40 "app_id": "660f9500-..."
41 }
42 ```
43
44 Direct auth does not work for accounts with 2FA enabled. [OAuth2 PKCE]./oauth.md is recommended for all apps (browser-based login, no credential handling). The resulting access token works with all SyncKit endpoints.
45
46 ## Device Registration
47
48 Register a device before pushing or pulling data:
49
50 ```
51 POST /api/sync/devices
52 Authorization: Bearer <token>
53 Content-Type: application/json
54
55 {
56 "device_name": "Alice's MacBook",
57 "platform": "macos"
58 }
59 ```
60
61 Platform values: `macos`, `windows`, `linux`, `ios`, `android`, `web`.
62
63 Response:
64
65 ```json
66 {
67 "id": "770a0600-...",
68 "device_name": "Alice's MacBook",
69 "created_at": "2026-03-13T10:00:00Z"
70 }
71 ```
72
73 If the same app + user + device_name combination already exists, the existing device is returned (upsert behavior).
74
75 ### Listing and Removing Devices
76
77 ```
78 GET /api/sync/devices
79 Authorization: Bearer <token>
80 ```
81
82 ```
83 DELETE /api/sync/devices/{device_id}
84 Authorization: Bearer <token>
85 ```
86
87 ## Push/Pull Sync
88
89 ### Pushing Changes
90
91 Send local changes to the server:
92
93 ```
94 POST /api/sync/push
95 Authorization: Bearer <token>
96 Content-Type: application/json
97
98 {
99 "device_id": "770a0600-...",
100 "batch_id": "a1b2c3d4-...",
101 "changes": [
102 {
103 "table": "tasks",
104 "op": "insert",
105 "row_id": "task-001",
106 "timestamp": "2026-03-13T10:05:00Z",
107 "data": "<encrypted-blob>"
108 }
109 ]
110 }
111 ```
112
113 Response:
114
115 ```json
116 {
117 "cursor": "abc123..."
118 }
119 ```
120
121 The `batch_id` ensures idempotent pushes. If the same ID is submitted twice, the server returns the existing cursor without re-inserting. Generate a unique `batch_id` per push and retry with the same ID on network failure.
122
123 Changes per push are capped at 500 (the server returns an error if exceeded). Table names are limited to 100 characters (alphanumeric and underscores only). Row IDs are limited to 255 characters. The server validates device ownership.
124
125 ### Pulling Changes
126
127 Fetch changes from other devices:
128
129 ```
130 POST /api/sync/pull
131 Authorization: Bearer <token>
132 Content-Type: application/json
133
134 {
135 "device_id": "770a0600-...",
136 "cursor": "abc123..."
137 }
138 ```
139
140 Response:
141
142 ```json
143 {
144 "changes": [
145 {
146 "table": "tasks",
147 "op": "update",
148 "row_id": "task-001",
149 "timestamp": "2026-03-13T10:10:00Z",
150 "data": "<encrypted-blob>"
151 }
152 ],
153 "cursor": "def456...",
154 "has_more": false
155 }
156 ```
157
158 Results are paginated. Keep pulling while `has_more` is `true`.
159
160 ### Checking Sync Status
161
162 ```
163 GET /api/sync/status
164 Authorization: Bearer <token>
165 ```
166
167 Response:
168
169 ```json
170 {
171 "total_changes": 1523,
172 "latest_cursor": "ghi789..."
173 }
174 ```
175
176 ## End-to-End Encryption
177
178 The server stores only encrypted blobs in the `data` field; it never sees plaintext user data. The client SDK uses ChaCha20-Poly1305 for encryption and Argon2 for key derivation. The encrypted master key envelope is stored server-side so users can set up new devices without re-entering a passphrase.
179
180 ### Key Storage
181
182 Store and retrieve the encrypted master key:
183
184 ```
185 PUT /api/sync/keys
186 Authorization: Bearer <token>
187 Content-Type: application/json
188
189 {
190 "encrypted_key": "<base64-encoded-encrypted-master-key>"
191 }
192 ```
193
194 ```
195 GET /api/sync/keys
196 Authorization: Bearer <token>
197 ```
198
199 Response:
200
201 ```json
202 {
203 "encrypted_key": "<base64-encoded-encrypted-master-key>",
204 "key_version": 1
205 }
206 ```
207
208 Maximum key size: 4KB. Returns 404 if no key has been stored yet.
209
210 ## Blob Storage
211
212 Content-addressed blob storage in S3, deduplicated by hash.
213
214 ### Upload Flow
215
216 1. Request a presigned upload URL:
217
218 ```
219 POST /api/sync/blobs/upload
220 Authorization: Bearer <token>
221 Content-Type: application/json
222
223 {
224 "hash": "sha256-hex-string",
225 "size_bytes": 1048576
226 }
227 ```
228
229 Response:
230
231 ```json
232 {
233 "upload_url": "https://s3.example.com/...",
234 "already_exists": false
235 }
236 ```
237
238 If `already_exists` is `true`, the blob is already stored. Skip the upload.
239
240 2. Upload the file directly to the presigned URL (PUT request to S3).
241
242 3. Confirm the upload:
243
244 ```
245 POST /api/sync/blobs/confirm
246 Authorization: Bearer <token>
247 Content-Type: application/json
248
249 {
250 "hash": "sha256-hex-string",
251 "size_bytes": 1048576
252 }
253 ```
254
255 Blob size is capped per tier. The server rejects oversized uploads.
256
257 ### Downloading Blobs
258
259 ```
260 POST /api/sync/blobs/download
261 Authorization: Bearer <token>
262 Content-Type: application/json
263
264 {
265 "hash": "sha256-hex-string"
266 }
267 ```
268
269 Response:
270
271 ```json
272 {
273 "download_url": "https://s3.example.com/..."
274 }
275 ```
276
277 The download URL is a presigned S3 URL valid for a limited time.
278
279 ## Real-Time Notifications (SSE)
280
281 Subscribe to Server-Sent Events instead of polling. The server pushes a notification when another device pushes changes.
282
283 ```
284 GET /api/sync/subscribe?app_id={app_id}
285 Authorization: Bearer <token>
286 ```
287
288 This is a long-lived SSE connection. Events:
289
290 | Event | Data | Meaning |
291 |-------|------|---------|
292 | `changed` | `{}` | Another device pushed changes. Call pull to catch up. |
293
294 Recommended pattern:
295
296 1. Open SSE connection on app launch
297 2. On `changed` event, call pull to fetch new changes
298 3. Reconnect on connection drop (with exponential backoff)
299 4. Fall back to periodic polling if SSE is unavailable
300
301 ## Key Rotation
302
303 Multi-step key rotation (e.g., after a suspected compromise):
304
305 1. **Begin rotation**: Store the new encrypted key alongside the old one:
306
307 ```
308 POST /api/sync/keys/rotate/begin
309 Authorization: Bearer <token>
310 Content-Type: application/json
311
312 { "new_encrypted_key": "<base64>" }
313 ```
314
315 2. **Fetch entries to re-encrypt**: Pull changelog entries encrypted with the old key:
316
317 ```
318 POST /api/sync/keys/rotate/entries
319 Authorization: Bearer <token>
320 Content-Type: application/json
321
322 { "cursor": "...", "limit": 100 }
323 ```
324
325 3. **Submit re-encrypted batch**: Upload entries re-encrypted with the new key:
326
327 ```
328 POST /api/sync/keys/rotate/batch
329 Authorization: Bearer <token>
330 Content-Type: application/json
331
332 { "entries": [...] }
333 ```
334
335 4. **Complete rotation**: Finalize once all entries are re-encrypted:
336
337 ```
338 POST /api/sync/keys/rotate/complete
339 Authorization: Bearer <token>
340 ```
341
342 During rotation, clients may receive a mix of old-key and new-key entries. Be prepared to decrypt with both keys until rotation completes.
343
344 ## Membership Gating
345
346 Configured per app. Apps linked to a free item (or no item) have unrestricted sync access. Apps linked to a paid item or membership tier require an active purchase or membership. If the membership lapses, push/pull return `403`. Device registration and key storage remain accessible.
347
348 ## Billing & Pricing
349
350 SyncKit bills the developer, not your end users. You buy a storage budget for the whole app and allocate it however you like; how you charge your own users (flat, freemium, usage-based, nothing) is your concern.
351
352 One rate: **{{ synckit.storage_usd_per_gb_month | money }} per GB per month**. Ingress and egress are included (absorbed in the rate's margin). API requests are unmetered. Every invoice is floored at **{{ synckit.invoice_floor_usd | money }}/month**, the smallest charge that clears Stripe's per-transaction fee.
353
354 Billing is configured from the app's dashboard panel, in one of two modes:
355
356 - **Bulk**: set one knob, `storage_gb_cap` (total GB). Price = `storage_gb_cap × $0.03`. At the cap, **uploads are refused** (`402`, `reason: "storage_limit_reached"`); existing data still reads.
357 - **Per-key**: set `key_cap` (max active SDK keys) and `gb_per_key` (allotment per key). Price = `key_cap × gb_per_key × $0.03`. A `claim_key` past the cap returns `402 key_limit_reached`; a key over its allotment returns `402 storage_limit_reached` with `dimension: "storage_per_key"`.
358
359 Caps are bounded: each mode's priced total must be between {{ synckit.min_priced_gb }} GB and {{ synckit.max_priced_display }} ({{ synckit.max_priced_gb }} GB). The bill is fixed for the period. There are no overages. Increasing a cap takes effect immediately; decreasing it applies at the next billing cycle. Cancellation takes effect at period end, and a standard export is available at any time.
360
361 First-party apps use a separate end-user subscription model and are not configured here.
362
363 ### Managing SDK keys
364
365 `POST /api/sync/keys/claim`, `/release`, and `/list` are server-to-server calls. They authenticate with the app's **keys secret**, not its API key:
366
367 ```
368 POST /api/sync/keys/claim
369 Content-Type: application/json
370
371 {
372 "app_secret": "<keys secret>",
373 "key": "<your SDK key>"
374 }
375 ```
376
377 Generate the secret from the app's dashboard panel ("Keys Secret"). It is shown once. Regenerating it invalidates the previous one immediately.
378
379 Keep it on a backend you control. The API key is a public client identifier: it ships inside your app binaries and can be read out of them, so it cannot be trusted to gate calls that spend your key cap. An app with no keys secret cannot call these three endpoints at all.
380
381 ## See Also
382
383 - [OTA Updates]./ota.md: auto-update your app through SyncKit
384 - [OAuth2 PKCE]./oauth.md: browser-based login for SyncKit apps
385