Skip to content

Releases: Manoo07/Synora

0.1.44

Choose a tag to compare

@Manoo07 Manoo07 released this 14 Oct 17:53

Fixes two faults introduced by the parallel uploads in 0.1.43.

"12 edits could not be synced and were dropped"

An operation whose stored envelope had gone missing was removed from the queue and reported as lost, asking you to re-save the file by hand.

That was far too harsh. Only the encrypted envelope was missing — the content it was built from is still sitting in your vault. It's now rebuilt from the file on disk and sent normally. An edit is only given up on when its file is genuinely gone.

Nothing was ever actually lost in this case: those notes were still in your vault, and re-saving would have re-queued them. But you should never have been asked to.

"Synora sync failed: HTTP 503"

Every backend request opened a database connection pool of five, while every route runs its queries strictly one after another — so four of those were never used.

Once the client started uploading four files at once, that became four requests × five connections, hitting Hyperdrive's origin limit of twenty exactly. Any pull or status check happening alongside was refused, and surfaced as a 503. The pool is now two.

Transient failures also say so now. Everything stays queued and retries; "sync failed" on its own reads like work was lost.

Requires the matching backend deploy, which is already live.

0.1.43

Choose a tag to compare

@Manoo07 Manoo07 released this 14 Oct 17:46

Uploads are much faster, and now tell you how long they'll take

Uploading was three sequential round trips per file, one file at a time. With 435 files queued that's 1,305 serialized round trips — roughly six or seven minutes of pure waiting before counting a single byte of actual content.

For comparison, I measured the client-side work: all the hashing and encryption for a 25 MB file comes to 533 ms. The crypto was never the slow part.

One of the three round trips did nothing. The upload-slot request existed only to be told a random id — which this side can pick itself. Removed.

Uploads now run four at a time. Commits deliberately stay sequential: uploading touches nothing shared, but committing mutates the queue and local state, and those are read-modify-write over one file. Run two at once and one write erases the other, which could drop a queued conflict copy.

Attachments over 4 MB still upload one at a time — four 25 MB buffers in flight is 100 MB held at once, and this runs on phones too.

Progress and ETA

The status bar now reads Uploading 12/196 · about 2m left, with the rate measured over the actual run rather than assumed, since the honest answer depends entirely on your connection.

Nothing is estimated for the first second and a half. An ETA that lurches from "3 hours" to "20 seconds" is worse than no ETA.

0.1.42

Choose a tag to compare

@Manoo07 Manoo07 released this 14 Oct 16:38

Vaults can be named

A hex id tells you nothing about which vault holds your notes, and an account can easily end up with several. You can now give each one a name in Settings.

The name is encrypted with your vault key. A vault name is text you wrote — "Health journal", "Work — Acme" — so storing it readable would hand the server exactly the kind of thing this design exists to keep from it. It's encrypted on your device; the server stores bytes it cannot read, the same as your notes.

One honest consequence: only a device holding the vault key can display a name. A vault this device has no access to still shows as its id, because it genuinely knows nothing about it.

Names show up where you actually choose

The settings row now reads Personal notes · 412 files · 36 MB instead of a truncated id.

More importantly, so does the "This device already has access to" list — the screen where you pick a vault to attach to. Each row now shows its name and file count rather than two near-identical hex ids. That screen is where a device ended up attached to a one-file vault while 877 changes sat in another one.

Also

Fixed --local being ignored by the migration scripts. .env loaded first, so DATABASE_URL overrode the one flag whose entire purpose is to say "not that database".

0.1.41

Choose a tag to compare

@Manoo07 Manoo07 released this 14 Oct 16:21

Sign out and switch accounts

There was no way to use a different account on a device short of deleting the plugin's data file by hand. Settings now has Use a different account → Sign out.

It clears the device's key pair along with the token. That key is this device's identity to the server, so keeping it while signed into another account would leave two accounts believing they own the same device.

Your notes are never touched. Nothing is removed server-side either, so the old account still lists this device until you revoke it from somewhere else — the same contract Disconnect already had. If you have edits that haven't uploaded yet, the confirmation tells you how many, since those are the one thing that can't be recovered.

Disconnect no longer leaves the old vault's state behind

A real bug, found while adding the above. Disconnect cleared which vault you were on, but kept the sync cursor and file records — and those belong to the vault you just left.

The cursor is a position in that vault's history. Carried into a different vault, the device asks for changes after a sequence the new vault has never reached, finds nothing, and reports itself fully synced over an empty folder.

This is the same symptom as the vault mix-up fixed in 0.1.40, reached by a different route. Both paths now clear that state properly.

0.1.40

Choose a tag to compare

@Manoo07 Manoo07 released this 14 Oct 16:17

You can now see which vault you're actually on

An account can hold more than one vault — creating one on a device that meant to recover into an existing one is a single mis-click. Until now the settings panel identified your vault by a truncated id and nothing else, so there was no way to tell the one holding your notes from an empty one.

This bit someone for real: a device sat attached to a vault containing a single file, reporting "✓ All files synced", while 877 changes waited in a different vault on the same account. Every signal on screen was technically correct — pull genuinely had nothing to fetch. The storage line even read 36 MB, because that figure is account-wide rather than per-vault, so it looked like data that ought to be arriving.

The vault row now reads bd644b1a… · 1 file · 4 KB, and when the account has more than one vault, hovering shows the others with their file counts.

Counts and sizes only. The server already holds these as metadata and still cannot read a name, a path, or a byte of content.

Requires the matching backend deploy, which is already live.

0.1.39

Choose a tag to compare

@Manoo07 Manoo07 released this 14 Oct 16:08

The sync status only ever looked at uploads

"✓ All files synced" was computed purely from the upload queue. It said nothing about whether this device had actually received anything.

A device you've just added has precisely that shape: nothing waiting to upload, and an entire vault still to come down. So it reported everything was synced while the vault sat empty — which reads as "my notes are gone", not "nothing has arrived yet".

The status now shows both directions, kept separate rather than added together, because 5 files to upload and 5 changes to download mean very different things when you're trying to work out where your notes are.

0.1.38

Choose a tag to compare

@Manoo07 Manoo07 released this 14 Oct 05:56

Audited the product requirements against what the code actually does. Most requirements were already met; four were not, and the one bug the requirements flagged turned out to be in the written spec rather than the system.

Downloads are now verified against their declared hash

The change feed has always carried a hash for each version, and the client never checked it — the field was only ever computed when uploading.

The encryption already catches altered bytes, and the authenticated data already catches a blob replayed into a different file's slot. Neither catches an earlier version of the same file being served as the current one: those bytes are genuinely yours, correctly signed, correctly bound to that file. Only the declared hash tells them apart. Silently reverting a note to an older copy is precisely what this system exists to prevent, so a mismatch is now refused rather than applied.

Deleting on one device no longer beats an unsynced edit elsewhere

Previously, if you deleted a note on your laptop while your phone had an edit it hadn't synced yet, the phone merged your text against the tombstone's empty payload — writing an empty file over the note and pushing the real content into a conflict copy.

Now the edit wins: the note is restored with your content and the restoration propagates back to the device that deleted it. Re-deleting takes a second; writing that never synced is gone for good. If both devices deleted it, it simply stays deleted.

Notes from a newer plugin are refused instead of guessed at

The envelope layer already rejected an unknown format, but the inner decoder read the version field and ignored it. A note written by a newer Synora would have been decoded on a guess — likely yielding a plausible-looking note with content quietly missing, then written back as the current version over the real one. It now stops and tells you to update.

Smaller

  • Merge commits record both versions they reconciled, so a merge is distinguishable from an ordinary edit in the history.
  • Attachments are sealed under their own encryption context rather than sharing the one used for notes. Existing files are unaffected — every blob carries the context it was sealed with.

Requires the matching backend deploy, which is already live.

0.1.37

Choose a tag to compare

@Manoo07 Manoo07 released this 14 Oct 04:58

Cuts the database query load of an idle vault by roughly thirty times.

The problem

Query usage was approaching the free-tier ceiling without the vault being especially busy, because the cost scaled with devices × uptime rather than with anything you actually did.

Every authenticated request spent two queries before doing any work — one to load the device, one to check vault membership. The idle sync loop made two such requests every interval, a change-feed read and an ack. So a device sitting there doing nothing cost six queries every 30 seconds, about 17,000 a day, each.

What changed

The device and membership checks are now one query. They were two round trips asking about the same request. Kept as a separate helper rather than folded into the shared membership check, because the recovery endpoints deliberately run without an active device and would have broken.

Acks are only sent when the cursor actually moved. An idle vault was re-sending the same sequence number every interval, forever, and each one is an authenticated write.

Polling backs off when idle. After a couple of quiet minutes the interval eases from 30 seconds out to 5, and snaps straight back on any local edit. Explicit triggers — startup, attaching a vault, resuming a paused file — bypass the backoff entirely, so nothing you do ever waits on it.

before after
idle queries per device per day ~17,000 under 600

Active use is unchanged: edits still push on the next cycle, and a vault being worked in never slows down. The only thing that gets slower is noticing a remote change to a vault that has been untouched for several minutes.

Requires the matching backend deploy, which is already live.

Also adds docs/architecture.md — why the system is built the way it is, the tradeoffs, and the production failures that shaped it.

0.1.36

Choose a tag to compare

@Manoo07 Manoo07 released this 14 Oct 04:00

Four fixes from extending the sync re-verification to the delete, rename, preferences and attachment paths.

Every delete was echoed back as a phantom file

Applying a delete from the change feed removed the file without telling the loop protection, and the delete handler never consulted it at all — deletes were simply exempt. So the delete Obsidian fires for the plugin's own removal got re-sent as a fresh one. By then the local record is gone, so it carried no file id, and a commit without one always creates a new file.

The result: every device that received any delete silently committed a phantom file, version, change event and R2 blob — each billed against your storage and never reclaimed. The same ran behind every rename's old-path cleanup, which is part of why change feeds grew fast enough to hit the paging bug fixed in 0.1.35.

Deleting a file then recreating it resurrected the old one

The committed delete left the local record behind, so a file later created at that path inherited the dead file's identity and committed as another version of it instead of as a new file.

Preference bundles could write outside the config folder

applyPreferencesBundle wrote whatever paths a bundle contained. The exclusion protecting this plugin's own folder is a string comparison, so plugins/other/../synora-sync/data.json walked straight past it, and .. or an absolute path escaped .obsidian/ entirely — defeating the rule the whole feature is built on, that one device's identity is never overwritten by another's.

Attachments no longer thrash data.json

Queued operations stored the entire encrypted file inline, in the blob Obsidian rewrites in full on every write — so each queue change re-serialised every other pending attachment too, in memory.

before after
20 photos at 3MB 1.56 GB written 0.1 MB
5 PDFs at 25MB 0.81 GB written 0.0 MB

Payloads now live one file each under the plugin's own folder, written once and deleted once. Anything queued by an older version is migrated on upgrade rather than dropped, so un-synced edits survive the update. Files over the upload cap are now reported up front instead of being encrypted and written to disk before the server rejects them.

No backend change — works against the already-deployed server.

0.1.35

Choose a tag to compare

@Manoo07 Manoo07 released this 14 Oct 03:37

Two fixes found while re-verifying the 0.1.34 merge fix.

Automatic merging stopped working on vaults with more than ~500 saves

Conflict resolution looked up versions by scanning the change feed from the beginning, capped at 500 entries and ordered oldest-first. Every save is a change event, so once a vault's history outgrew a single page, the versions it needed were never in the window — and every concurrent edit forked a conflict copy instead of merging, silently and permanently.

This affected exactly the vaults that need merging most: the active ones. If you've been seeing conflict copies pile up where a clean merge should have happened, this was why.

Fixed by looking up the two versions directly instead of scanning history. Also removes a 500-row fetch on every conflict.

A second case where an edit could be silently dropped

0.1.34 fixed the merge dropping one side's change around repeated identical lines, but its guard checked whether the deleted block repeated elsewhere. A diff can re-express one edit as a completely different set of hunks wherever repeated lines allow it, so a block that looks unique can still be assembled from lines the alignment could have taken from anywhere — and one deletion still went missing. The check now works line by line.

Rare (about 1 in 350,000 merges) but the same failure mode as 0.1.34: the dropped edit re-diverges and the devices keep re-resolving the same conflict.

Verification

An invariant suite covering identity, symmetry, idempotence, convergence and per-line content counts, across 346k structured and ~360k random cases — no failures. Realistic concurrent note edits still merge cleanly 89.7% of the time, with the remainder falling back to safe conflict copies.

Requires the matching backend deploy, which is already live.