Blog · Build It Yourself

The Era of Personalized Apps Is Here

For months I shared screenshots from my phone to my Mac by sending myself a Microsoft Teams DM and accepting image(15).png filenames as the cost of doing business. One coffee and one prompt later, I had my own app that did exactly what I wanted, and nothing I did not.

By Michael Pierce · MDPSync · April 25, 2026

9 min read

The Friction

I move a screenshot, a document, or a link from my Samsung phone to my Mac several times a day. I have tried more than a dozen apps to do it. They were either broken, cumbersome, ad-laden, or wanted a monthly subscription for a feature I could describe in two sentences. The promising ones quietly disappeared the next time I needed them. The professional ones wanted me to sign in, accept a privacy policy, and trust their cloud with files I was about to immediately delete.

My fallback for months was Microsoft Teams. I would send myself a personal message from the phone, walk back to the Mac, find the thread, click the file, and accept that every screenshot was now named image(15).png. The chat history kept growing. The filenames kept getting worse. The app I actually wanted was embarrassingly simple to describe.

A share-sheet entry on the phone. Files land in ~/Downloads. Text and links land in a Teams channel I can search later. No clutter. No subscription. No third party in the middle.

One Coffee, One Prompt

One Saturday morning I sat down with coffee, opened a Claude session, and wrote out what I actually wanted. The prompt did not arrive on the first try. There were a few rounds of back-and-forth, the kind of refinement that turns a vague idea into something a model can build against. After the first pass and a couple of clarifications, the prompt looked like this.

The Initial Prompt
Build a cross-device sharing system that lets me send files, links, and text from my Android phone to my Mac desktops through a self-hosted relay on my Raspberry Pi. Three native components, no cloud storage dependencies.

Relay server (PHP + Node.js on Pi):
- PHP API endpoints behind Apache with Bearer token auth on every request
- POST /api/share.php — receives multipart uploads from Android, stores files in a pending/ directory with JSON metadata sidecars
- GET /api/download.php — serves files to Mac clients with per-device tracking
- GET /api/status.php — health check and pending file list with lazy TTL cleanup (7-day expiry)
- POST /api/register.php — device registration
- Node.js WebSocket server on a dedicated port (PM2-managed, Apache-proxied) that pushes file_ready notifications to connected Macs when PHP receives a new share
- No database — all state is filesystem (pending files, JSON metadata, devices.json registry)
- Multi-device fan-out: files stay on disk until every registered Mac downloads them, tracked via pending_devices and downloaded_by arrays in metadata with flock for concurrent writes

Mac menubar agent (Swift, SPM executable):
- Runs as accessory app (menubar icon only, no Dock presence)
- Persistent WebSocket connection to relay with auto-reconnect (exponential backoff, capped at 30s)
- On file_ready message: downloads file via HTTP GET with device_id, saves to ~/Downloads, shows macOS notification
- Drains backlog on reconnect (fetches pending files from status endpoint for files missed while offline)
- Preferences window: relay URL, API key, device name, download directory, launch at login
- Auto-update checker against GitHub Releases every 48h
- Zero external dependencies — Foundation, AppKit, SwiftUI only

Android share target (Kotlin):
- Appears in system share sheet for text, links, images, and files
- WorkManager-based upload queue with network constraint and exponential backoff retry (3 attempts)
- OkHttp3 multipart POST to relay
- API key stored in EncryptedSharedPreferences
- Files copied to cache before upload to avoid losing content provider access

Domain served via Cloudflare Tunnel. Single shared API key authenticates all components. Timing-safe token comparison everywhere.

About an hour later I had a working product. Not a prototype. The Android share sheet was wired in, the Pi was relaying, and the Mac was getting notifications when a file landed. I was sharing a screenshot to my desktop while I was still in the kitchen.

SyncToDesktop app icon

What I Built

Three native components, no cloud, around 1,500 lines of code total. The architecture is small enough to fit in a single diagram, and small enough to keep in my head when something needs fixing.

Figure 1: SyncToDesktop System Architecture SYNCTODESKTOP — SYSTEM ARCHITECTURE CLIENT Android Share Target Kotlin · WorkManager · OkHttp3 • ACTION_SEND intent handler • Multipart POST with retry • EncryptedSharedPreferences • Sideloaded APK, single user PHONE RELAY (PI) Pi Relay Apache · PHP 8.2 · Node + PM2 • /api/share, download, status • Bearer token, hash_equals() • WebSocket server, dedicated port (PM2) • JSON sidecars + flock, no DB RASPBERRY PI AGENT Mac Menubar Agent Swift · SPM · AppKit + SwiftUI • Persistent WebSocket client • Drops files in ~/Downloads • macOS native notifications • Auto-update via GitHub M5 STUDIO + 13" AIR HTTPS POST /api/share.php WebSocket file_ready CLOUDFLARE TUNNEL Public HTTPS + WSS · no port-forwarding · no static IP Phone reaches the Pi from any network on the planet Data flow: share intent → multipart upload → PHP writes pending/ + JSON sidecar → /notify to Node WS → broadcast file_ready → Mac downloads → ~/Downloads + notification Multi-device fan-out: pending_devices[] vs downloaded_by[] · 7-day TTL cleanup · flock() on metadata

Android share target (Kotlin)

Appears in the system share sheet for text, links, images, and files. WorkManager queues each upload with retry and a network constraint, so if I share something on the train it ships when the signal comes back. The API key lives in EncryptedSharedPreferences. I sideloaded the APK onto my phone because Play Store certification was not worth the overhead for an app that has exactly one user.

Pi relay (PHP + Node.js)

Apache serves four PHP endpoints behind a Bearer token, with timing-safe comparison on every request. A small Node.js WebSocket server on a dedicated port (PM2-managed, Apache-proxied) pushes a file_ready message to every connected Mac the second a new file lands. There is no database. State lives on disk as JSON sidecars next to the files themselves, with file locks for the only concurrency case that actually exists. The Pi is exposed to the public internet through a Cloudflare Tunnel, so the phone reaches it from anywhere with no port-forwarding and no static IP.

Mac menubar agent (Swift, SPM)

Runs as an accessory app, no Dock presence, just a small icon in the menubar. It holds a persistent WebSocket connection to the relay with exponential-backoff reconnect. On a file_ready message it pulls the file with a device id, drops it in ~/Downloads with the original filename preserved, and posts a macOS notification. The agent checks GitHub Releases every 48 hours and updates itself, so distribution is a git tag instead of a build pipeline.

The Second Mac

The first version had no concept of multiple Macs. It worked perfectly for the one case I tested at the kitchen table, which was the kitchen table. Two days later I packed for a trip with my 13" MacBook Air and noticed that whichever machine grabbed the file first was the only one that got it. I added a device registry, a per-file pending_devices array, and a downloaded_by array that gets appended under a file lock. Files now stay on the relay until every registered Mac has pulled a copy. The agent drains a backlog on reconnect, so anything shared while a laptop was asleep on a plane lands as soon as it wakes up.

A later pass closed a small download race that could have leaked a file to an unauthorized device by moving the auth gate ahead of the bytes-on-the-wire call. Nothing about that fix felt heavy. I noticed the issue on a Thursday afternoon, traced it in a Claude session while watching coffee brew, and shipped the fix to both Macs and the phone before the cup was empty. There is no support team. There is no on-call rotation. There is the customer, the architect, the QA, and the model, all in one window, all on the same side of the problem.

The Plan, in Full

The short prompt above is what I started with. The longer document below is what would let Claude rebuild the whole system from scratch on a fresh Pi: directory layout, auth model, share pipeline, multi-device tracking, the Swift file map, the Kotlin file map, the setup sequence, and every design decision worth defending. It is the artifact you would hand to a model if you wanted your own version of this running by dinner.

The Bigger Point

This is the part of the post I actually care about. With a homelab, a Claude subscription, and an afternoon, you are now your own software shop for the long tail of personal annoyances no commercial app will ever solve cleanly enough. You skip the trials, the subscriptions, the ads, the trackers, and the support tickets that go unanswered for weeks. You become your own customer support, paired with a model that reads carefully and writes carefully.

  • One-user apps with zero distribution overhead
  • Iteration on the timescale of "I noticed something annoying yesterday"
  • Privacy by default, because nothing leaves your hardware
  • Skills that compound, because every app you build teaches the next one

The leverage was not in the code. The leverage was in deciding to write the code at all.

Got a personal annoyance you would build your way out of?

SyncToDesktop is a private project, but the pattern travels. If you have a homelab and a workflow that no off-the-shelf app handles cleanly, start a conversation.