Skip to main content

max / makenotwork

7.9 KB · 355 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 Makenot.work account.
4
5 For the Rust client SDK, see the [API reference]/rustdoc/synckit_client/.
6
7 ## Concepts
8
9 - **Sync App**: A registered application on Makenot.work. Each app has its own API key, data namespace, and device list.
10 - **Device**: A named installation of your app (e.g., "Alice's MacBook"). Each device syncs independently.
11 - **Changelog**: An append-only log of changes. Each entry records a table name, operation, row ID, timestamp, and encrypted data blob.
12 - **Cursor**: An opaque position in the changelog. Pull from cursor to get only new changes.
13 - **Blob**: A content-addressed encrypted file stored in S3. Referenced by SHA-256 hash.
14
15 ## Creating a Sync App
16
17 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.
18
19 ## Authentication
20
21 ### Direct Authentication
22
23 For desktop apps where the user enters their Makenot.work credentials:
24
25 ```
26 POST /api/sync/auth
27 Content-Type: application/json
28
29 {
30 "email": "user@example.com",
31 "password": "their-password",
32 "api_key": "your-app-api-key"
33 }
34 ```
35
36 Response:
37
38 ```json
39 {
40 "token": "eyJ...",
41 "user_id": "550e8400-...",
42 "app_id": "660f9500-..."
43 }
44 ```
45
46 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.
47
48 ## Device Registration
49
50 Register a device before pushing or pulling data:
51
52 ```
53 POST /api/sync/devices
54 Authorization: Bearer <token>
55 Content-Type: application/json
56
57 {
58 "device_name": "Alice's MacBook",
59 "platform": "macos"
60 }
61 ```
62
63 Platform values: `macos`, `windows`, `linux`, `ios`, `android`, `web`.
64
65 Response:
66
67 ```json
68 {
69 "id": "770a0600-...",
70 "device_name": "Alice's MacBook",
71 "created_at": "2026-03-13T10:00:00Z"
72 }
73 ```
74
75 If the same app + user + device_name combination already exists, the existing device is returned (upsert behavior).
76
77 ### Listing and Removing Devices
78
79 ```
80 GET /api/sync/devices
81 Authorization: Bearer <token>
82 ```
83
84 ```
85 DELETE /api/sync/devices/{device_id}
86 Authorization: Bearer <token>
87 ```
88
89 ## Push/Pull Sync
90
91 ### Pushing Changes
92
93 Send local changes to the server:
94
95 ```
96 POST /api/sync/push
97 Authorization: Bearer <token>
98 Content-Type: application/json
99
100 {
101 "device_id": "770a0600-...",
102 "batch_id": "a1b2c3d4-...",
103 "changes": [
104 {
105 "table": "tasks",
106 "op": "insert",
107 "row_id": "task-001",
108 "timestamp": "2026-03-13T10:05:00Z",
109 "data": "<encrypted-blob>"
110 }
111 ]
112 }
113 ```
114
115 Response:
116
117 ```json
118 {
119 "cursor": "abc123..."
120 }
121 ```
122
123 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.
124
125 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.
126
127 ### Pulling Changes
128
129 Fetch changes from other devices:
130
131 ```
132 POST /api/sync/pull
133 Authorization: Bearer <token>
134 Content-Type: application/json
135
136 {
137 "device_id": "770a0600-...",
138 "cursor": "abc123..."
139 }
140 ```
141
142 Response:
143
144 ```json
145 {
146 "changes": [
147 {
148 "table": "tasks",
149 "op": "update",
150 "row_id": "task-001",
151 "timestamp": "2026-03-13T10:10:00Z",
152 "data": "<encrypted-blob>"
153 }
154 ],
155 "cursor": "def456...",
156 "has_more": false
157 }
158 ```
159
160 Results are paginated. Keep pulling while `has_more` is `true`.
161
162 ### Checking Sync Status
163
164 ```
165 GET /api/sync/status
166 Authorization: Bearer <token>
167 ```
168
169 Response:
170
171 ```json
172 {
173 "total_changes": 1523,
174 "latest_cursor": "ghi789..."
175 }
176 ```
177
178 ## End-to-End Encryption
179
180 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.
181
182 ### Key Storage
183
184 Store and retrieve the encrypted master key:
185
186 ```
187 PUT /api/sync/keys
188 Authorization: Bearer <token>
189 Content-Type: application/json
190
191 {
192 "encrypted_key": "<base64-encoded-encrypted-master-key>"
193 }
194 ```
195
196 ```
197 GET /api/sync/keys
198 Authorization: Bearer <token>
199 ```
200
201 Response:
202
203 ```json
204 {
205 "encrypted_key": "<base64-encoded-encrypted-master-key>",
206 "key_version": 1
207 }
208 ```
209
210 Maximum key size: 4KB. Returns 404 if no key has been stored yet.
211
212 ## Blob Storage
213
214 Content-addressed blob storage in S3, deduplicated by hash.
215
216 ### Upload Flow
217
218 1. Request a presigned upload URL:
219
220 ```
221 POST /api/sync/blobs/upload
222 Authorization: Bearer <token>
223 Content-Type: application/json
224
225 {
226 "hash": "sha256-hex-string",
227 "size_bytes": 1048576
228 }
229 ```
230
231 Response:
232
233 ```json
234 {
235 "upload_url": "https://s3.example.com/...",
236 "already_exists": false
237 }
238 ```
239
240 If `already_exists` is `true`, the blob is already stored. Skip the upload.
241
242 2. Upload the file directly to the presigned URL (PUT request to S3).
243
244 3. Confirm the upload:
245
246 ```
247 POST /api/sync/blobs/confirm
248 Authorization: Bearer <token>
249 Content-Type: application/json
250
251 {
252 "hash": "sha256-hex-string",
253 "size_bytes": 1048576
254 }
255 ```
256
257 Blob size is capped per tier. The server rejects oversized uploads.
258
259 ### Downloading Blobs
260
261 ```
262 POST /api/sync/blobs/download
263 Authorization: Bearer <token>
264 Content-Type: application/json
265
266 {
267 "hash": "sha256-hex-string"
268 }
269 ```
270
271 Response:
272
273 ```json
274 {
275 "download_url": "https://s3.example.com/..."
276 }
277 ```
278
279 The download URL is a presigned S3 URL valid for a limited time.
280
281 ## Real-Time Notifications (SSE)
282
283 Subscribe to Server-Sent Events instead of polling. The server pushes a notification when another device pushes changes.
284
285 ```
286 GET /api/sync/subscribe?app_id={app_id}
287 Authorization: Bearer <token>
288 ```
289
290 This is a long-lived SSE connection. Events:
291
292 | Event | Data | Meaning |
293 |-------|------|---------|
294 | `changed` | `{}` | Another device pushed changes. Call pull to catch up. |
295
296 Recommended pattern:
297
298 1. Open SSE connection on app launch
299 2. On `changed` event, call pull to fetch new changes
300 3. Reconnect on connection drop (with exponential backoff)
301 4. Fall back to periodic polling if SSE is unavailable
302
303 ## Key Rotation
304
305 Multi-step key rotation (e.g., after a suspected compromise):
306
307 1. **Begin rotation**: Store the new encrypted key alongside the old one:
308
309 ```
310 POST /api/sync/keys/rotate/begin
311 Authorization: Bearer <token>
312 Content-Type: application/json
313
314 { "new_encrypted_key": "<base64>" }
315 ```
316
317 2. **Fetch entries to re-encrypt**: Pull changelog entries encrypted with the old key:
318
319 ```
320 POST /api/sync/keys/rotate/entries
321 Authorization: Bearer <token>
322 Content-Type: application/json
323
324 { "cursor": "...", "limit": 100 }
325 ```
326
327 3. **Submit re-encrypted batch**: Upload entries re-encrypted with the new key:
328
329 ```
330 POST /api/sync/keys/rotate/batch
331 Authorization: Bearer <token>
332 Content-Type: application/json
333
334 { "entries": [...] }
335 ```
336
337 4. **Complete rotation**: Finalize once all entries are re-encrypted:
338
339 ```
340 POST /api/sync/keys/rotate/complete
341 Authorization: Bearer <token>
342 ```
343
344 During rotation, clients may receive a mix of old-key and new-key entries. Be prepared to decrypt with both keys until rotation completes.
345
346 ## Membership Gating
347
348 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.
349
350 ## See Also
351
352 - [OTA Updates]./ota.md: auto-update your app through SyncKit
353 - [OAuth2 PKCE]./oauth.md: browser-based login for SyncKit apps
354 - [SyncKit Client SDK]/rustdoc/synckit_client/: Rust client library documentation
355