Steven
Steven8 min read

Taking the Backend Out of the Upload Path

GeekBye records your screen and saves the video to your Google Drive. The first version shipped every recording through GeekBye's own servers on the way there; a release later, the file went straight from your machine to Drive, and the backend was demoted to holding a single pointer. The interesting part is how little code the 'direct, resumable' version actually contains — because the resumability came from deleting a proxy, not from writing one.

Engineering
Architecture
Desktop
GeekBye Releases
Taking the Backend Out of the Upload Path

GeekBye can record your screen — video, system audio, and mic — and drop the finished recording into your Google Drive automatically. Between v1.8.11 and v1.8.13, the feature's user-facing description barely changed, but the path the video takes to get to Drive was rerouted completely. In the first version, your recording traveled through GeekBye's servers. In the second, it went straight from your machine to your Drive, and GeekBye's backend never saw a byte of it. That reroute is the whole story, and the satisfying part is that the "better" version contains conspicuously less code than the one it replaced.

The pipeline, end to end

Recording happens in the renderer. ScreenRecordingService.ts creates a MediaRecorder over the capture stream with { mimeType: 'video/webm;codecs=vp9,opus', videoBitsPerSecond: 1_000_000 } and starts it with a one-second timeslice (CHUNK_INTERVAL_MS = 1000). Every ondataavailable blob is shipped over IPC to the main process, where the recording:save-chunk handler appends it to a file on disk with fs.promises.appendFile. Worth flagging early, because it's a tempting thing to misread: that one-second cadence is the disk-write rhythm, not an upload chunk size. By the time the upload starts, there's a single finished WebM file sitting on disk.

What differs between the two releases is only what happens to that finished file.

The before: everything through the backend (v1.8.11)

The v1.8.11 upload path, in CloudUploader.processRecording(), is the obvious design and a completely reasonable first version:

  1. POST /api/geekbye/recordings to create a backend record with the recording's metadata.
  2. Read the entire video into memory and POST it to the backend as multipart — const fileBuffer = fs.readFileSync(filePath) — hitting POST /api/geekbye/recordings/${id}/upload. The backend then uploads that file to Drive and returns a driveUrl.
  3. POST …/process to send the transcript for AI analysis.

The load-bearing detail is step 2: the client loads the whole recording into memory and every byte of it transits GeekBye's own servers on the way to Drive. For a short clip that's fine. For a long screen recording it's three problems at once — memory pressure on the client from readFileSync, bandwidth and relay cost on the backend for a file it doesn't even keep, and a single monolithic POST that has to restart from zero if the network hiccups halfway through.

It's also worth being honest that this "before" wasn't some pristine original design. The v1.8.11 feature squashed together a within-week pivot: an early cut uploaded to Cloudflare R2 via pre-signed URLs and converted WebM to MP4 with a bundled ffmpeg-static; that was replaced by the backend-mediated Drive flow; and then the ffmpeg conversion was ripped out entirely in favor of uploading the WebM as-is (the commit notes it lowered the bitrate and produced roughly four-times-smaller files — an inline estimate, not a benchmark). So even the "before" had already learned to delete a native dependency once. The next release would delete the proxy too.

The after: straight to Drive (v1.8.13)

v1.8.13 rewrites CloudUploader.processRecording() around one sentence from the commit that shipped it: upload directly to Drive, "keeping backend for metadata record + AI transcript analysis only." The new path:

  1. POST /api/geekbye/recordingsmetadata only (title, duration, file size, format: 'webm'). The comment in the code is blunt: // Create backend record (metadata only — no file upload).
  2. Upload the video directly to Google Drive, skipping the backend middleman entirely.
  3. PATCH /api/geekbye/recordings/${backendRecordingId} with the resulting { driveUrl, driveFolderId } — the backend record is told where the file ended up.
  4. Transcript still goes to …/process for analysis.

The backend went from being in the file's path to being told about the file after the fact. It holds a row and a pointer; the bytes live only on the user's disk and in the user's Drive.

Becoming a real Drive client

For the client to talk to Drive directly, it has to be an actual Google Drive API client, and v1.8.13 adds the machinery for that: a new DriveService (the Drive client), a new DriveAuthRepository backed by a new drive_auth SQLite table, and the googleapis SDK as a dependency.

The authorization is a clean capability hand-off. The backend's app-config endpoint relays Google OAuth client credentials — a clientId and clientSecret — down to the app, which CloudUploader reads and passes to driveService.initialize(...). From there the desktop app runs the whole OAuth flow itself: it stands up a temporary http server on a random 127.0.0.1 port, opens the consent screen in the system browser with shell.openExternal, catches the redirect on a /callback, and exchanges the code for tokens with google-auth-library. Those tokens are saved locally — saveDriveAuth(access_token, refresh_token, expiry_date) into drive_auth — with the drive.file scope, which lets the app manage only the files it creates. Refresh is local too: an oauth2Client.on('tokens', …) listener writes refreshed tokens straight back to the table. The backend hands over the capability once and then stays out of the loop.

The "resumable" part, honestly

Here's the detail I most want to be straight about, because it's exactly the kind of thing a changelog rounds off. The release note says "resumable uploads," and that's true from where the user stands. But GeekBye did not implement the resumable protocol. There is no chunk-size constant, no byte-offset tracker, and no Content-Range / 308 Resume Incomplete handling anywhere in GeekBye's source. Grep for them and you get nothing — they live inside the googleapis SDK.

What GeekBye's own code does is this:

const fileStats = statSync(videoFilePath)
const videoStream = createReadStream(videoFilePath)

const videoRes = await this.drive.files.create(
  {
    requestBody: { name: `${title}.webm`, parents: [folderId] },
    media: { mimeType: 'video/webm', body: videoStream },
    fields: 'id',
  },
  {
    onUploadProgress: (evt) => {
      const percent = Math.round((evt.bytesRead / fileStats.size) * 100)
      onProgress(percent)
    },
  },
)

Two choices do all the work. First, media.body is a stream (createReadStream), not a buffer — and passing a stream is precisely what makes googleapis negotiate a resumable session on your behalf instead of doing one shot. Second, progress is read off the SDK's onUploadProgress callback against fileStats.size. That's the entire "resumable upload" implementation at the application level: stream in, progress out. The hard protocol — open a session, upload in chunks, resume from an offset after a drop — is the SDK's job, and the correct engineering decision was to let it do that job rather than reinvent it.

Which reframes the whole release. The reliability improvement didn't come from writing an upload engine. It came from deleting the proxy that sat in front of one — and, earlier the same week, deleting the ffmpeg step in front of that. Less code, more resilience, because the resilient thing was a well-worn library the moment you stopped hand-feeding it a buffer.

The bits that are genuinely GeekBye's

Handing the transfer to the SDK doesn't mean there was nothing left to build. Two pieces of real application logic surround it.

An orphaned-folder rollback. Each recording gets its own Drive subfolder (created with mimeType: 'application/vnd.google-apps.folder'), and the transcript lands there as a second file next to the video. If the upload throws, the catch deletes that folder — this.drive.files.delete({ fileId: folderId }), logged as Rolled back orphaned Drive folder — so a failed upload doesn't leave a trail of empty folders in your Drive. Creating a container before you know the operation will succeed is a small liability; making the failure path clean it up is the fix.

Lifecycle around a singleton. DriveService is a singleton holding an in-memory OAuth client, and that in-memory client doesn't survive an app restart even though the tokens do (they're in the database). So CloudUploader re-initializes DriveService from the stored tokens before every upload, which means a recording made right after relaunch still uploads without asking you to reconnect. And the review pass turned the drive-connect handler into fire-and-forget, because a blocking connect call was stalling the renderer's status polling — the UI couldn't show "connecting…" if the connect call held the thread. Those two — re-init-from-storage and non-blocking connect — are the direct path's real edge cases, and notably neither of them is about upload chunking. When you delegate the hard part, the bugs you're left with are lifecycle bugs.

Three things this release taught us

  1. Ask where the bytes flow, not just whether it works. The v1.8.11 proxy worked. But routing every recording through your own servers costs you bandwidth, memory, and restartability for a file you don't keep. Redrawing the data path so the client talks to the destination directly is often the whole optimization.
  2. The best resumable upload is the one you didn't write. Passing a stream to a mature SDK bought a full resumable session for two lines of code. The instinct to hand-roll chunking and offset math would have produced more code and less reliability.
  3. When you delegate the core, you inherit lifecycle, not algorithms. The direct client's real bugs were "re-init the singleton after restart" and "don't block the connect call," not byte offsets. That's a good trade — lifecycle bugs are visible and local; protocol bugs are neither.

For the previous chapter in the v1 story, born and fixed in twelve minutes (v1.8.10); for the next, telling a call from an open app (v1.8.15–v1.8.19); and for the whole arc, the anatomy of shipping software to perfection.

Related Articles

The Three Verbs That Keep Web Audio Alive
Steven
Steven9 min read

The Three Verbs That Keep Web Audio Alive

Two GeekBye point releases, two months apart and in two different files, taught our audio code the same lesson from opposite ends: stop treating the browser's AudioContext as disposable. One release learned to resume() a context macOS had silently suspended mid-recording; the other learned to suspend() instead of close() so back-to-back sessions stop slamming into Chromium's roughly-six-context ceiling. Resume, suspend, close — that's the whole plot.

Engineering
Audio
Desktop
Telling a Call From an Open App
Steven
Steven9 min read

Telling a Call From an Open App

GeekBye can notice you've joined a video meeting and offer to record it. The detection turns out to be the easy half — a Swift binary reading window titles every ten seconds. The hard half is precision: not firing when Zoom is merely open, not prompting for a meeting you're already recording, and not muting the microphone in the call you're actually in. Three releases, and each one is a guard that had to learn to not defeat itself.

Engineering
macOS
Desktop
Born and Fixed in Twelve Minutes
Steven
Steven7 min read

Born and Fixed in Twelve Minutes

GeekBye v1.8.10's changelog says it fixed a crash while editing keyboard shortcuts. It did — but the crash was introduced and fixed in the same pull request, twelve minutes apart, and never reached a single user. The real story is the reliability cascade that produced it: a small, correct change to when shortcuts are suspended, and the React teardown bug that fell out of one line meant to make the editor safer.

Engineering
React
Desktop