TetherBay 0.2.650
· Updated 2026-09-13
TetherBay 0.2.650 packages the latest documented beta improvements across relay resilience, network fallback, storage, and encrypted sync.
Highlights
- A clip sent to one device could stop that device receiving any clipboard at all.
Addressed clips are served ahead of broadcast ones so that an earlier send cannot be skipped while it waits for its recipient. The query that does this dropped the cursor bound entirely, so an addressed clip the recipient neither applied nor dismissed was re-served on every poll and hid every later clip from that device until it aged out of the ten-event history. Clients in exactly that state are the norm rather than the exception: any build older than the dismissal route only advances its own c
- A clip this device cannot decrypt no longer blocks the ones behind it forever.
An event whose account key had not arrived held the cursor so a late key could still open it. Since addressed clips are served first, an event that would never open sat at the head of the queue indefinitely. The wait is kept but now ends after ten minutes, at which point the event is retired and the feed drains.
- Contact Clipboard failures are now reported instead of swallowed.
Every failure path in the feature was invisible once it left a developer's machine: the launcher logged breadcrumbs that never become issues and kept only the exception type name, and the server turned a database outage, a blob-storage failure or an unreadable stored grant into the same retryable error with nothing recorded anywhere. Infrastructure failures on both sides are now captured. Input validation stays quiet on purpose.
- A share link that fails on your own machine no longer blames the server.
When this device could not read or write the record that makes a link upload safe to retry, the app reported a server error code for a request it had never sent, and filed it alongside real relay outages. It now says that the link could not be prepared on this device, and is counted separately.
Fixes
- A clip sent to one device could stop that device receiving any clipboard at all. Addressed clips are served ahead of broadcast ones so that an earlier send cannot be skipped while it waits for its recipient. The query that does this dropped the cursor bound entirely, so an addressed clip the recipient neither applied nor dismissed was re-served on every poll and hid every later clip from that device until it aged out of the ten-event history. Clients in exactly that state are the norm rather than the exception: any build older than the dismissal route only advances its own cursor, and a phone dismiss reports nothing. A cursor already past the event now counts as the client's terminal answer. The desktop advances its cursor only after a disposition is recorded, so its retry loop is unchanged. A regression test covers the recipient that answers neither way.
- A clip this device cannot decrypt no longer blocks the ones behind it forever. An event whose account key had not arrived held the cursor so a late key could still open it. Since addressed clips are served first, an event that would never open sat at the head of the queue indefinitely. The wait is kept but now ends after ten minutes, at which point the event is retired and the feed drains.
- Contact Clipboard failures are now reported instead of swallowed. Every failure path in the feature was invisible once it left a developer's machine: the launcher logged breadcrumbs that never become issues and kept only the exception type name, and the server turned a database outage, a blob-storage failure or an unreadable stored grant into the same retryable error with nothing recorded anywhere. Infrastructure failures on both sides are now captured. Input validation stays quiet on purpose.
- A share link that fails on your own machine no longer blames the server. When this device could not read or write the record that makes a link upload safe to retry, the app reported a server error code for a request it had never sent, and filed it alongside real relay outages. It now says that the link could not be prepared on this device, and is counted separately.
- A share link's download limit now counts recipients, not fifteen-minute windows. Repeat requests from the same client inside a fifteen-minute window are deliberately not charged, so that a resumed browser download does not burn a slot. That exemption was keyed on the transport address — and every route is reverse-proxied to loopback in production, so *every* recipient of a link shared one bucket. After a single charged download, everyone else's download for the next fifteen minutes took the exempt path, which skips the limit check entirely and records nothing. A link offered "up to 10 times" could be fetched by any number of people while its count moved at most once per window. The grant is now keyed on the resolved caller — the rightmost X-Forwarded-For entry, the one our own proxy appends — which is what the rate limiter has always used and documents. Also applies to the view counter, which collapsed the same way.
- Blocking from an offer says the contact was dropped even when the file could not be removed. The half-landed case — block succeeded, declining the file did not — kept its own honest sentence about the file still being listed, but never mentioned that the person had left your contacts. It was missed when the offer door was first covered; the wording test now enumerates every toast raised after a successful block rather than naming them one at a time.
- The share-link footnote now describes the limit the product actually applies. It said "The file can be downloaded up to 10 times." What is capped is ten *grants*, where repeat downloads from the same network inside fifteen minutes count as one — deliberately, so that a browser resuming an interrupted transfer does not burn a slot (a single one-kilobyte range request used to consume two of the ten). The consequence is that one recipient, or a whole office behind one gateway, can download the file any number of times against a single count; a sender reading the old sentence as "ten copies leave my hands and then it stops" did not get that. The sentence now says so, and because it quotes the fifteen-minute window, a test ties it to the server constant so the two cannot drift apart.
- Blocking someone from an incoming file offer now says they also left your contacts. Block has two doors — a contact's card, and Block on an incoming offer in Transfers — and both call the same thing, so both drop the person from your contacts as well as stopping them reaching you. Only the first one said so. The offer version reported just "{0} is blocked and the file was deleted. They can't send you anything new.", so a contact quietly disappeared with nothing to explain it. Blocking deliberately has no confirmation step — it is the protective action and must stay the fastest control there — which makes that one sentence the only account of it anyone gets. It now discloses the removal too, and the test that has guarded the wording since the contacts-card version of this bug was fixed now covers every door instead of one.
- A contact you were once refused can become sendable again without restarting. When the relay permanently refuses a send to a contact — they removed you, or blocked you — the picker stops offering that person, so the next attempt is not walked into the same dead end. That mark was only ever lifted by a send that landed, and while it stood the contact could not be selected to send to, so the one event that cleared it was the one event it prevented. Relationships get repaired: the person accepts you again, the block comes off, and the relay reports both directions active while the sender's screen still refuses them. Until now the only way out was quitting the app, which nothing on screen suggested, even though the row itself says "right now". The refusal now expires after five minutes — a landed send still clears it at once — and, because the row's enabled state was being written onto list items that a contacts refresh rebuilt a moment later, it could also flip back to clickable while still captioned "They can't receive files from you right now"; the caption and the button now come from the same place and cannot disagree.
- Send no longer stays lit on a file you were just told it refused. Choose a file, then choose one the app rejects — too large, a folder, something that has since moved. The rejection sentence appears and the file card empties, so the screen says no file is chosen. The Send button underneath it stayed enabled anyway, on the file accepted a moment earlier and no longer visible anywhere: the rejection path cleared the view's own state but never told the draft, so its "can I submit?" answer was still yes. Every other path that drops a selection already cleared both; this one is now consistent with them, and the rejection branch is reachable from a test for the first time.
- Transfers stops claiming a source is unavailable when none is. With clipboard sharing off — the shipped default — GET /clipboard/recent correctly answers 204 No Content, and the client parsed that empty body anyway, recorded the source as failed, and left the Transfers screen permanently showing the red "Transfers partially refreshed · Some sources were unavailable" banner with a Retry that could never succeed. Nothing was wrong, and the consequence was worse than the false alarm: the empty-state card is gated on a fully successful load, so a brand-new user could never see "No transfers yet" at all. A 204 is now read as an empty source that succeeded. While the path was open, a genuinely failed source also began reporting to telemetry instead of only to launcher-user.log, which is a no-op in shipped builds — so this failure was previously invisible in the field even as the user was being told about it.
- Folders say which way they sync again. The IN/OUT badge that tells you whether a folder's files arrive on this device or leave it had been removed from the Folders page and the Home folder list, leaving "Owner — you" as the only hint — which answers a different question, since a folder you own can still have arrived from another of your own devices. It is back on all three row layouts, and the contract test that was meant to protect it (and had been inverted into a test that the badge must be *absent*) now pins it in both the wide and the collapsed layout.
- Phones and tablets can reach their own folders again. A relay change meant to fix which devices count as sync peers was applied to the wrong gate, and every companion device — which browses and fetches single files but never keeps a full copy — was refused the file list and every download on every folder. A collaborator whose device had no explicit per-folder setting stopped syncing for the same reason. Only a folder you have explicitly switched off for a device blocks it now, which is what it always said it did.
- A folder can no longer report itself finished when nothing was sent. When the relay refused an upload the app was retrying, the transfer still counted the file as delivered, so the progress bar walked to 100% and the folder settled as done while the file was still waiting. The file is now left counted as pending — which is what it is.
- "Clean up" asks again if you leave it. Permanently deleting a recovered file takes two clicks, but the armed state never expired: come back later and the click you read as your first destroyed the file. The confirmation now lapses after ten seconds and the button says so. Changing language while a row was armed also reset the button to "Clean up" without disarming it, so the very next click deleted with no confirmation on screen.
- Reviewing a very long list of local removals no longer overpromises. Lists above 5,000 entries are shown a page at a time, but acknowledging one page claimed the folder would stop reporting them shortly. It now tells you how many of the total you have reviewed and that reopening the list gets you the rest.
- An unreadable settings file no longer costs you your setup. The app has always kept a copy of the previous settings beside the live one, but never read it — so a corrupted file fell through to defaults, and the next save wrote those defaults over your device identity, server address and folder list. The copy is now used to recover them.
- One bad timestamp no longer hides a folder's whole history. A single unreadable date in stored activity made the folder's file history fail to load entirely instead of showing the other events.
- Two links now describe themselves accurately. "Link a device" said the link "can add up to 10 devices" — it adds exactly one, and the server refuses the second attempt with "This link was already used". That 10 was the *email sign-in* link's budget, quoted under the wrong credential; the join screen had been telling you the truth all along. Separately, the Send page called a download link "one-time" when it allows ten downloads, which the dialog and the link counter both already showed. A person setting up a third device no longer follows the app's own advice into a dead end, and nobody assumes a shared download link is already spent.
- Blocking someone now says what it actually does. The confirmation promised "They can't send you files", but a file they had already offered stays in your Transfers list — blocking stops new sends, it does not retract an offer already made. It now says "anything new". The leftover offer costs you nothing: it is held against the *sender's* relay quota, never yours, and nothing reaches your disk unless you accept it.
- Blocking someone now leaves a record of itself. The relay had a column for when a block began and never wrote it, so the one control a person has over who may send them files kept no audit trail at all. It is stamped when the block starts and cleared when the block is lifted, so it always answers "since when is this in force" rather than "was this person ever blocked". Nothing reads it yet and no behaviour changes.
- A file that fails to send can no longer be repainted as sent. While an earlier file was still waiting for someone to accept it, that older transfer kept checking in — and when a newer send was refused, the check-in overwrote the red failure with a green "Sent … 100%" describing the *older* file. The refusal raises no other notice, so that stomped line was the only sign anything had gone wrong; worse, the false success also cleared the picker, silently discarding the file that had been armed to retry. A check-in now updates the send panel only while the panel is still describing its own transfer.
- A folder never claims to be encrypted with a key this device could not make. If the keyring refused at the moment a folder was created, the app turned folder encryption on anyway — and then had no key to encrypt with, so it quietly held every upload back while the folder read as encrypted and up to date. Nothing could undo it afterwards. The app now leaves encryption off when it cannot mint the key, keeps syncing, and reports the failure.
- Blocking someone no longer erases their name from what you already exchanged. Transfer history took its labels from the list of people you may send to, so blocking somebody rewrote every past transfer with them into a raw account id like "user-1a2b3c4d". Blocking stops new sends; it no longer rewrites the record of old ones.
- The admin contact route stops inventing contacts. Adding a contact the owner had blocked answered "200 OK" with a made-up id that pointed at no record — and raised a false internal alarm about database corruption on a path that was working exactly as designed. It now answers 409 and says the account is blocked.
- Blocking a contact now actually blocks them. The button was there and the relay had enforced the blocked state for weeks, but the app asked for it the wrong way and the request was refused every single time — with a message suggesting you try again, which could never work. Blocking works now, and if a relay ever does refuse it the app says so plainly instead of inviting you to retry.
- A file taken on one of your devices stops nagging the others. When someone sent you a file and you accepted it on your laptop, your other devices kept a "Transfers 1" badge pointing at an empty page until the app was restarted. Any device now notices the file is gone, whichever screen it happens to be showing.
- Inviting a person no longer reports that a device joined. The window that hands over a folder invitation announced "Device joined!" when the person accepted — the same wording used when you add one of your own devices, which is exactly the event you are told to watch out for. It now names the folder they joined.
- Removing a contact tells you about a file still on its way to them. Removing someone does not recall a file they have not picked up yet — they can still accept it. The confirmation now says so, and points at Withdraw.
- A failed send no longer looks like a successful one. The progress bar was green whatever it was reporting, including directly under the word "Failed". Sending, delivered and failed now each have their own colour. If someone has made themselves unreachable to you, the picker remembers the refusal you were already shown rather than letting you walk into it again.
- Sending a file no longer leaves it armed. The file you just sent stayed selected with Send still enabled, so returning to the page put a duplicate one click away — a duplicate that now lands in another person's transfers, not just on your own laptop.
- Live QA evidence now fails closed when candidate identities drift. The evidence validator checks the manifest, coverage matrix, verdict, packaged artifact hashes, screenshots, and every app instance claimed by a full live pass. A three-client result can no longer be attached to a later two-client delta build without the mismatch being reported, and the evidence contract is now exercised by the main CI workflow.
- The README no longer promises a notarized Mac build when the available beta is ad-hoc signed. Its build badge now reflects the real cross-platform workflow, its version is current, and the Mac download row directs readers to the release-specific signing and first-launch status.
- After you're given Editor access, saving the file again really does share it. A file you changed while you only had read-only access is kept local for safety, and the app tells you to "edit, save, or rename each file again to share it". Renaming worked; saving did nothing at all, however many times you tried, because most editors save by writing a new file and moving it into place. Saving now publishes it. A file you have not touched since is still kept back — being given access does not push out an old edit you may not want sent.
- You can now take back a file you sent to a contact. Transfer history offered Open and Reveal, both of which act on your own copy — there was no way to recall a file sent to the wrong person, so it sat readable on our servers until it expired. Sent offers that nobody has picked up yet now have a Withdraw button, and using it removes the file from the relay.
- You can now block a contact. Removing a contact was never a block — the app said so itself, since the person can simply add you again — and there was nothing else to reach for. Blocking someone stops them sending you files and stops them sharing folders with you. Blocked people stay listed, marked as blocked, so you can undo it.
- Invitations you emailed are visible again — and can be revoked. An invitation sent to someone without a TetherBay account appeared nowhere: not in Contacts, not on the folder, not in Manage access, which is where the app itself tells you to go to revoke one. A mistyped address could not be undone for the invitation's whole week. Manage access now lists outstanding invitations for folders you own, each with the same ✕ as anyone else.
- Sending to a contact appears within minutes of the relay offering it. The app asked once and remembered a "no" for as long as it stayed open, so a desktop left running when the feature was switched on kept the option hidden — and, on the receiving side, never picked up files people had sent. A "no" is now re-checked periodically.
- Your transfer list now says what became of each file you sent. Delivered, declined, withdrawn, waiting and expired all used to read the same word — "sent" — which was nearly true when the only destination was your own device, and stopped being true the moment you could send to a person. A file someone refused looked exactly like one that arrived.
- Taking a file back no longer tells the other person it "expired". Withdrawing an offer was recorded as an expiry, so someone pressing Accept a moment too late was told they had been too slow, when in fact the sender had changed their mind. The two are now different events with different words.
- Transfers use the name you gave a contact. A row could show a raw account id for somebody your Contacts page names perfectly well, because it used the name that person set for themselves rather than the one you chose for them.
- Send-to-a-contact no longer disappears until you restart the app. If the relay ever answered "not available" — an older relay, or the feature switched off at that moment — the app remembered that answer for the rest of the session, even after you pointed it at a relay that does offer it. The answer is now remembered per relay.
- An invitation emailed to an address can no longer get stuck retrying. When the folder behind it had lost its owner, the claim re-ran on every contacts poll for the invitation's whole lifetime and re-added both people to each other's contacts each time. Nothing is written now until the claim can actually succeed, and a claim that has to wait backs off instead of retrying every 30 seconds.
- Re-adding a contact can no longer quietly undo a block.
- You can now send a file to a contact, not just to your own devices. Pick a file, pick a person, send. Their app shows it as an offer waiting for a decision, and nothing lands on their disk until they accept — one of their devices then downloads it into its Received folder. You can withdraw an offer until it is claimed, and you always see what happened: waiting, accepted, declined, or expired. Files sent this way are encrypted in transit and at rest but are not end-to-end encrypted — the Send screen and the encryption page both say so; put it in a synced folder if you need end-to-end. Neither side ever learns anything about the other's devices. The relay operator enables this per deployment.
- The encryption page now lists one-off Sends. Its "what is and isn't end-to-end" table covered synced folders, clipboard and browser links but silently omitted device-to-device Send, which has never been end-to-end.
- Accepting a shared folder no longer starts with a trip through your file system. Pressing Accept opened a folder picker immediately, so a folder someone shared with you could not arrive until you went and found — or created — a location on your own disk. It now proposes your incoming folder (TetherBay Incoming/<folder name>), shows you the exact path before anything is downloaded, and keeps the picker behind "Change location…" for the times you want it somewhere else. "Not now" leaves the invitation waiting in Contacts, and backing out of the picker returns to the choice instead of deciding for you.
- A folder shared without a link can now actually be read. An encrypted folder's key only ever travelled inside an invite link, so anyone brought in another way — shared with as a contact, or claiming an invitation sent to their email — became a full member of a folder they could not decrypt: every file failed its integrity check, silently, with nothing shown to either side. The owner's app is now told when a member is waiting for the key and seals a copy for them, which the member's app collects on its own. Only the owner can do this, only for people who have already accepted, and the relay still cannot read any of it.
- A folder invitation sent to someone's email now finds them after they install the app. Sharing with a person who has no TetherBay account yet mails them a link, and until now that link was the only way in: someone who installed TetherBay and signed in with the very address the invitation was sent to saw no invitation anywhere, because none existed for their account to report. Signing in with an invited address now claims the invitation and shows it with Accept/Decline, and an invitation that arrives later reaches an already-signed-in recipient on the next refresh. It still grants nothing until they accept. Device-link invitations are deliberately excluded, and only an address the account has verified counts.
- The invitation email and the join page now say what to do after installing. Both previously assumed the reader already had the app — "open the invite", or "paste this link" — advice that does not survive an install, since the installer does not carry the link the recipient arrived from. Both now name the address to sign in with.
- Windows Setup no longer aborts an install over legacy-updater cleanup. Setup still stops and removes the obsolete auto-updater, but a cleanup step it cannot verify through WMI or Task Scheduler is now logged and the installation continues, instead of ending in a restart-and-retry message a clean machine could not act on.
- Beta upgrades now reach the permanent Paddle Sandbox checkout. The production relay and desktop client accept the official tetherbay.com pricing handoff when the server explicitly reports Sandbox mode, while temporary tunnel URLs remain restricted to loopback QA relays. Beta testers keep real email-backed accounts and durable entitlements; only the Paddle transaction uses test money.
- Fresh Windows installs no longer fail a legacy-updater check. Setup now treats an absent legacy scheduled task as the normal state without depending on a version-specific Task Scheduler error identifier. A detected enabled legacy task or a still-running updater continues to block installation.
- Windows upgrades no longer fail on harmless legacy cleanup leftovers. Setup checks only the named legacy scheduled task instead of enumerating every task on the machine, accepts a task that is already disabled, and treats a locked obsolete LocalAppData application tree as recoverable after proving that no legacy updater is running. An active updater or enabled legacy task still blocks the upgrade.
- RecruitLead release artifacts remain readable after atomic activation. The mirror publisher now normalizes every staged download to mode 0644 before moving it into Caddy's document root, so a restrictive SSH umask cannot turn a hash-verified release into public HTTP 403 responses.
- Windows setup now retries transient legacy-tree file locks. After stopping the obsolete updater, both Setup and the one-line installer retry removal of its LocalAppData application tree for up to 15 seconds. A short-lived archive or process handle can no longer turn a successful updater shutdown into an immediate restart-required installation failure.
- RecruitLead download instructions no longer depend on the primary domain. Pages reached through the CIS mirror now show mirror-hosted one-line installer URLs, and /get.ps1 plus /get.sh default their release-feed requests to that same mirror ingress and use the mirror artifact URL returned by the feed. Mirror deployment and release verification reject either script if it contains a primary-domain dependency.
- Windows upgrades now retire even an already-running legacy beta updater. Setup disables and removes the old scheduled task, terminates its active PowerShell/VBS process tree, verifies the scheduled task is absent or disabled, and makes a best-effort removal of the obsolete LocalAppData application copy. This closes the race that could restore and launch an old beta one second after a successful Program Files installation while preserving settings and synced folders.
- Windows one-line upgrades now complete cleanly over running beta clients. The PowerShell flow downloads a recognisably named installer, closes existing launcher and agent processes, waits for setup to finish, verifies the installed binary version, and explicitly opens the matching launcher from Program Files. The release packager also rejects stale launcher or agent payloads whose FileVersion does not match the installer version, so a current filename cannot conceal old application bytes.
- Sync server no longer slows down as the total number of synced files grows. The relay's metadata store became database-authoritative: per-write work is now proportional to what actually changed (indexed point/range queries) instead of the whole dataset, and the server no longer loads all records into memory at startup — so boot time and memory stay flat as the service grows.
- One busy account can no longer degrade everyone else. Write contention is now keyed per folder/account behind a new lock manager (opt-in sharded mode), with a fair-share soft throttle that *slows* an over-active tenant rather than blocking it, and per-tenant lock metrics on /metrics. Closes the single-account denial-of-service path that could previously wedge the whole relay.
- PostgreSQL is now a supported metadata backend for the relay (selectable by configuration), alongside the existing SQLite store which remains the default for local and self-hosted use. Includes a one-shot SQLite→Postgres data migration tool with row-count reconciliation.
- Cloudflare R2 (S3-compatible) blob storage is production-ready. The relay can store encrypted blobs in R2 with presigned zero-egress downloads, a live storage health/deep-probe check, and a global storage-cap safety valve; switching is a configuration change. Verified end-to-end against a real S3 server.
- Server startup no longer waits on the boot-time storage maintenance pass. The audit that repairs/cleans staged and orphaned blobs now runs after the server is listening instead of before it binds, so health checks come up immediately even on an object-store backend where that pass can take minutes. Controlled by TETHERBAY_STARTUP_AUDIT_MODE (background default, off to skip at boot, blocking for the legacy pre-bind behavior).
- Folder encryption now actually takes effect. The feature below shipped complete and never switched on: a device recorded its encryption key but was never marked as *able* to use it, so the check "do all devices in this folder support encryption?" could never pass, no folder was ever turned on, and file contents went to the relay in the clear — while the app said synced folders were end-to-end encrypted. Devices now declare that capability, and a folder you own turns encryption on as soon as every device in it can handle it, rather than only at the moment the folder is created. Same fail-closed rollout as below: a folder holds uploads rather than sending anything in the clear, and a folder is never turned on unless the device turning it on holds the key — so a folder that is already syncing cannot be stopped by enabling it.
- End-to-end encryption for synced folders and clipboard. File contents are encrypted on the sending device with AES-256-GCM (framed streaming AEAD) before upload; folder keys are shared device-to-device via sealed ECDH envelopes and never reach the relay. Clipboard items are sealed per-event with the account key. The relay stores ciphertext only and has no decryption path. Rollout is fail-closed — uploads are held rather than sent in the clear if any device in a folder cannot yet support E2E, so every device in a folder must be updated before content flows again. Clipboard E2E defaults ON since 2026-06-15.
- Remote file access hardened alongside folder encryption.
Known issues
- macOS beta packages are unsigned and not notarized; follow the documented Gatekeeper first-launch steps.
Availability
- win-x64 — Download
7e17c22cba447cb5f9ef98d5c566c7e8b0ce5b8b005c80392e9bb38c98e9e3aa - osx-arm64 — Download
229334f7bffc66b4ea68b687620930540c1496f33fb1771a78e51ddb49dcbf04 - osx-x64 — Download
7bad70e90df9822ecf41d459eb484cb84ab80ebe2ad2b91795f0e857caea6279 - linux-x64 — Download
37df3409c5ddd6fb7462a8101df6c396140c568f1331a0a81c54705373566a01 - linux-arm64 — Download
4895289444afa43c0122c3cb5041fc03a61bc71fd297bd4fac41600f621d3d3d - android — Download
9302a63202073ada2335193115e9caddb4a94c521947c21fa9f49cf01a77a680 - ios — pending