Contributor · Reference
Device protocol#
What the Supernote does when it syncs, and what Inkhold must answer. Everything here was learnt from a real tablet, a Nomad, by watching it and by reading the decompiled apps; where a fact is an assumption rather than an observation, it says so. The routes themselves are in the HTTP API.
What the tablet can and cannot be told#
- A failure is an HTTP 200 with a coded body, for example
QUOTA_EXCEEDED. Any 4xx or 5xx status reads to the tablet as the network failing in the middle of a sync. - Never answer 401 on a byte route. A 401 signs the tablet out. A download with a bad capability token gets a 403 instead.
- Never answer 404 from delete, move, copy or create. A 404 aborts the sync, or makes the tablet download notes it had deleted. An id that is unknown, belongs to someone else, or is already in the bin gets a success answer with the payload left out. That also hides other people’s id spaces.
File sync#
- The tablet has one way to delete,
delete_folder_v3, which it uses for files too: one entity per request, children before parents, with retries. move_v3andcopy_v3carry the full destination path including the file name. Copy is how the tablet resolves a conflict: it makes<name>_CONFLICT_<timestamp>.notebeside the original.- Without
create_folder_v2, empty folders never sync. - A sync walks the tree with
list_folder, folder by folder, and compares each file’s md5. Inkhold computes paths in SQL with a recursive query over folders only; materialising the whole library was three to five times slower. query/summary/*is not file sync. It is the Digest app’s protocol, found in the decompiledKnowledgeApiService.- The tablet’s sync records survive signing out. It keeps one record per
synced file: its path, its md5 and the server’s id for it. A record whose
file is missing from the server reads as “deleted in the cloud”, and the
tablet deletes its own copy. It drops those records only at sign-in, and only
when the
userIdoruniqueMachineIdthatcheck/exists/serverreports, or the email, differs from what it stored; an empty stored value counts as a match. Inkhold reports the account’s id and the library’s id (thelibrarytable, schema v18). So a new library id makes every tablet sync from scratch: it merges and uploads, and never deletes. This was decompiled from the Settings app (firmware Chauvet.E103.2606141001), and the deletion was seen on a Nomad switched to an empty server under the same email.
Sign-in and devices#
- Sign-in sends
password = sha256_hex(md5_hex(password) + randomCode). - The kind of client comes from the
equipmentfield (1 is the web, 2 the phone app, 3 a tablet), not fromequipmentNo: the phone app signs in with no serial number. A tablet’s sign-in gets its token in the response body. bindEquipmentarrives without a token. The Nomad sends it a few milliseconds after signing in, before it has stored the token. Inkhold accepts it only when a live tablet session for that serial number belongs to the named account. File calls usually arrive first and bind the serial number with a placeholder name; the bind call then replaces the placeholder. ThetotalCapacityit carries is the tablet’s own storage, in KB, and is ignored: a tablet cannot raise its own allotment.equipment/unlinkbefore sign-in, with no caller, is a no-op that answers success. A real unbind there would let anyone on the network sign a tablet out by the serial number printed on it.
Bytes in and out#
- Downloads:
download_v3mints a?t=token (128 bits, about five minutes, usable more than once so retries and Range requests work) because the GET that follows carries no access token./oss/getanswers raw bytes with aContent-Dispositionfile name; without that name the tablet cannot write the note. - Uploads are three calls.
upload/applybinds the path and name, which are authoritative./oss/putcarries the bytes as multipart, in parts when the file is large; the tablet sends parts one after another.upload/finishchecks that the md5 equals thecontent_hashthe tablet declared. The size charged to the allotment is what landed on disk, not what was declared; the cap applies to the whole session, not to each part, and a retried part is credited back. get_space_usageis in bytes, confirmed on the tablet’s screen.user/query.totalCapacityis in bytes by assumption.
Auto Sync#
- Auto Sync is a Socket.IO 2 / Engine.IO 3 websocket at
/socket.io/. The tablet’s token travels in the query string; thesign=HMAC uses a key from the firmware and is not checked. - While the socket is up, the tablet uploads each local change at once, and runs a full sync when it connects.
- The tablet sends no Engine.IO pings. The heartbeat is the server’s
ratta_pingevery 20 seconds, which the tablet answers. - A change made elsewhere goes out as a
ServerMessage,{"code":"200","data":[MessageBean]}. The tablet re-reads each message’s file byidand reconciles, so only three things in the message matter: the id, a type it acts on (MODIFYFILE,DELETEFILE, and so on), and a path inside a synced folder. Its own uploads come back to it too, as a no-op. STARTSYNCis honoured only before the first full sync succeeds, so it cannot carry later changes.
To-Do and Digest#
- The To-Do and Digest apps have their own protocol (
schedule/*andquery/summary/*), and theirto-doanddigestevents share the Auto Sync socket. - Neither may ever get an empty or made-up answer. The To-Do app deletes
every list and task it once synced that the server does not list. The Digest
app imports whatever the server lists as collections: when
query/summary/grouppresented folders as groups, it imported every folder (seen on hardware, 2026-11-05). A refusal issuccess: falseat HTTP 200 (401, 403 and 404 take other paths in the apps), and the app stops its sync before comparing anything. - To-Do sync (schema v20,
services/todo.zig,handlers/device/todo.zig) stores lists and tasks exactly as the app sends them, keyed by the tablet’s own ids per account. The app decides every merge itself. A full sync starts at token 0 and reads absence as deletion, so a listing returns everything stored and the incomingnextSyncTokennever narrows it. A row leaves only by an explicit delete, which always succeeds (one failed delete aborts the app’s whole sync), or with its account. Deleting a list takes its tasks. - To-Do sync is behind
config.todo_sync, off by default; while off, every call is refused. - The browser edits the same rows (
services/todo_web.zig,/api/controlplane/todo,webapp/src/pages/todo/) the way the tablet’s app does: the server stamps the times, a new or moved task goes on top (sort 0, the newest sort time), and a reorder is a set of positions plus one sort time. “Waiting for the tablet” is schema v21: a browser edit marks the row and the account, and the tablet’s next task listing clears the mark. - A task lassoed from a notebook page shows its handwriting in the browser
(
webapp/src/pages/todo/todoInk.ts). The task’s page is fetched and drawn once; the words it was lassoed from are found in the page’s recognised text; only the strokes of that crop are kept, with paper and template lines made transparent and the crop trimmed to the ink, in the persistent thumbnail store under the page’s version. It is shown as a mask painted in the theme’s ink colour, in a chip at the row’s edge that also opens the note. Those words are schema v22’slink_text: the title the task had when its link arrived, kept through renames so the match survives them. - The library id does not protect to-dos. The reset at sign-in
(
doLogin: clear table of private cloud,clearSyncTable) clears file records only (seen on hardware, 2026-11-05). What protects them is a prompt the same sign-in shows whenever the tablet remembers a previous account, the same account included. Retain locally marks every to-do unsynced, so it uploads. Delete locally (todo/delete_synced_data) removes the synced ones, losing any newer than a restored snapshot. A restore or a reinstall always forces that sign-in (sessions revoked, tokens invalid), which is why the docs, the restore command and the config all say to choose Retain. - Tested on a Nomad, 2026-11-05: creating, completing, renaming, moving and
deleting tasks and lists all synced. After a restore to a snapshot older than
one synced task, Retain kept that task on the tablet and uploaded it again.
Not yet verified: whether a tablet that was unregistered first
(
hasUnregister) skips the prompt and keeps its marks. - Digest stays refused.