<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dean</title>
    <description>The latest articles on DEV Community by Dean (@dean0224).</description>
    <link>https://dev.to/dean0224</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3987554%2Fe634ac7b-f092-4db1-a70b-cea792518c9e.png</url>
      <title>DEV Community: Dean</title>
      <link>https://dev.to/dean0224</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dean0224"/>
    <language>en</language>
    <item>
      <title>How I built Google Drive sync without a backend (and the 3 bugs that almost broke me)</title>
      <dc:creator>Dean</dc:creator>
      <pubDate>Tue, 16 Jun 2026 14:35:12 +0000</pubDate>
      <link>https://dev.to/dean0224/how-i-built-google-drive-sync-without-a-backend-and-the-3-bugs-that-almost-broke-me-1c2n</link>
      <guid>https://dev.to/dean0224/how-i-built-google-drive-sync-without-a-backend-and-the-3-bugs-that-almost-broke-me-1c2n</guid>
      <description>&lt;p&gt;When I started building PenPage — a privacy-first note app that&lt;br&gt;
  stores everything in IndexedDB — I made one assumption that cost me&lt;br&gt;
  three weeks of debugging:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Google Drive sync will be the easy part."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It wasn't.&lt;/p&gt;

&lt;p&gt;Here's what I learned building a sync engine entirely in the&lt;br&gt;
  browser, with no backend server.&lt;/p&gt;



&lt;p&gt;## The core idea: one file to rule them all&lt;/p&gt;

&lt;p&gt;Instead of syncing every note file individually, I built around&lt;br&gt;
  a single &lt;code&gt;sync.json&lt;/code&gt; that stores all metadata:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  ppage-app/
  ├── sync.json        ← the source of truth
  ├── pages/
  │   └── page-*.md   ← actual note content
  └── images/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sync.json&lt;/code&gt; holds folder structure, page metadata, image metadata,&lt;br&gt;
  and device info — but NOT page content. On every sync:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Download &lt;code&gt;sync.json&lt;/code&gt; (or skip if &lt;code&gt;modifiedTime&lt;/code&gt; hasn't changed)&lt;/li&gt;
&lt;li&gt;Compare local IndexedDB state vs Drive state&lt;/li&gt;
&lt;li&gt;Upload/download only what changed&lt;/li&gt;
&lt;li&gt;Upload the new &lt;code&gt;sync.json&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This keeps API calls to 2-3 per sync cycle instead of N×2 per file.&lt;/p&gt;



&lt;p&gt;## Bug #1: The 404 that wasn't really a 404&lt;/p&gt;

&lt;p&gt;Google Drive returns 404 when you try to access a folder that's&lt;br&gt;
  been deleted and recreated — even if a folder with the same name&lt;br&gt;
  exists now.&lt;/p&gt;

&lt;p&gt;This hit me when implementing "Force Upload" (which recreates the&lt;br&gt;
  app folder from scratch). Device A would force upload, delete and&lt;br&gt;
  recreate the folder. Device B still had the old folder ID cached&lt;br&gt;
  — and every API call returned 404.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; wrap every Drive operation in a recovery handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;withFolderRecovery&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;404&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reinitialize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// re-fetch all folder IDs&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;// retry once&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any 404 triggers a full folder ID refresh, then retries. Simple,&lt;br&gt;
  but it took me a while to realize the root cause.&lt;/p&gt;



&lt;p&gt;## Bug #2: The silent data corruption hiding in a sentinel value&lt;/p&gt;

&lt;p&gt;Every sync, I run a cleanup step that repairs folder &lt;code&gt;parentId&lt;/code&gt;&lt;br&gt;
  values. The check looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="c1"&gt;// Intended: fix folders with wrong parentId&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isRootParentId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;folder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parentId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;repairFolder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;folder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;isRootParentId()&lt;/code&gt; returned &lt;code&gt;true&lt;/code&gt; for both &lt;code&gt;'workspace'&lt;/code&gt; (the&lt;br&gt;
  actual sentinel for "orphaned folder") AND &lt;code&gt;'root'&lt;/code&gt; (the correct&lt;br&gt;
  value for top-level user folders).&lt;/p&gt;

&lt;p&gt;Result: every sync, ALL top-level folders got their &lt;code&gt;updatedAt&lt;/code&gt;&lt;br&gt;
  timestamp refreshed to &lt;code&gt;Date.now()&lt;/code&gt;. The comparison logic saw&lt;br&gt;
  local as newer than Drive → uploaded everything → silently&lt;br&gt;
  overwrote changes from other devices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="c1"&gt;// Only match the actual bad sentinel value&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;folder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parentId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;workspace&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;repairFolder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;folder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One character difference. Weeks of mysterious "my changes&lt;br&gt;
  disappeared" reports.&lt;/p&gt;




&lt;p&gt;## Bug #3: IndexedDB index queries don't match &lt;code&gt;undefined&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Force Download is supposed to: clear local data → import from Drive.&lt;/p&gt;

&lt;p&gt;But after Force Download, pages appeared blank. The root cause was&lt;br&gt;
  a chain of four silent failures:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;clearAllData()&lt;/code&gt; queries IndexedDB by &lt;code&gt;workspaceId: 'global'&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Old records had &lt;code&gt;workspaceId: undefined&lt;/code&gt; (pre-migration data)&lt;/li&gt;
&lt;li&gt;IndexedDB index queries are &lt;strong&gt;exact match&lt;/strong&gt; — &lt;code&gt;undefined&lt;/code&gt; ≠ &lt;code&gt;'global'&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Old records survived the clear&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;importAll()&lt;/code&gt; tried to create records with same IDs → &lt;code&gt;store.add()&lt;/code&gt;
 silently fails on duplicate keys&lt;/li&gt;
&lt;li&gt;New records never written → UI shows nothing
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="c1"&gt;// ❌ Misses orphan records where workspaceId is undefined&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAllPages&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;workspaceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;global&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// ✅ Explicit orphan cleanup&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allPages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAllPages&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;orphans&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;allPages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workspaceId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;deleteAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orphans&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; &lt;code&gt;store.add()&lt;/code&gt; failure on duplicate keys is silent.&lt;br&gt;
  &lt;code&gt;store.put()&lt;/code&gt; overwrites. Know which one you're using.&lt;/p&gt;




&lt;p&gt;## What actually works well&lt;/p&gt;

&lt;p&gt;Despite the bugs, the architecture held up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;modifiedTime&lt;/code&gt; as a proxy for "changed"&lt;/strong&gt; — no polling,
no webhooks, no server&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parallel uploads&lt;/strong&gt; (5 concurrent) reduced sync time 80%
for large note sets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tombstones in sync.json&lt;/strong&gt; for deleted pages — other devices
learn about deletions without needing the deleted file&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;## Would I do it again?&lt;/p&gt;

&lt;p&gt;Yes, but I'd budget 3× more time for edge cases. The Google Drive&lt;br&gt;
  API docs describe the happy path. The bugs live in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stale cached folder IDs&lt;/li&gt;
&lt;li&gt;Silent IndexedDB failures&lt;/li&gt;
&lt;li&gt;Sentinel value collisions in your own data model&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building anything with Google Drive API or browser-side&lt;br&gt;
  sync, happy to answer questions in the comments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PenPage&lt;/strong&gt;: &lt;a href="https://penpage.com" rel="noopener noreferrer"&gt;https://penpage.com&lt;/a&gt;&lt;/p&gt;




</description>
      <category>api</category>
      <category>javascript</category>
      <category>showdev</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I spent 5 months building a note app nobody asked me to build</title>
      <dc:creator>Dean</dc:creator>
      <pubDate>Tue, 16 Jun 2026 14:28:05 +0000</pubDate>
      <link>https://dev.to/dean0224/i-spent-5-months-building-a-note-app-nobody-asked-me-to-build-3b6</link>
      <guid>https://dev.to/dean0224/i-spent-5-months-building-a-note-app-nobody-asked-me-to-build-3b6</guid>
      <description>&lt;p&gt;Last November, I got fed up.&lt;/p&gt;

&lt;p&gt;Every note-taking app I tried either locked my data in their cloud,&lt;br&gt;
  charged a subscription, or both. So I did what developers do —&lt;br&gt;
  I built my own.&lt;/p&gt;

&lt;p&gt;Five months later, PenPage (penpage.com) is live: a privacy-first&lt;br&gt;
  WYSIWYG note app that stores everything in your browser's IndexedDB&lt;br&gt;
  and optionally syncs to your own Google Drive.&lt;/p&gt;

&lt;p&gt;## The hardest part: Google Drive sync&lt;/p&gt;

&lt;p&gt;I thought sync would take a weekend. It took three weeks.&lt;/p&gt;

&lt;p&gt;The core problem: Google Drive has no real-time conflict detection.&lt;br&gt;
  When you edit on two devices, you get two versions with no merge&lt;br&gt;
  strategy. I ended up building a sync engine around &lt;code&gt;modifiedTime&lt;/code&gt;&lt;br&gt;
  comparison + a single &lt;code&gt;sync.json&lt;/code&gt; as the source of truth.&lt;/p&gt;

&lt;p&gt;Edge cases I didn't expect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Race conditions when auto-sync fires during manual sync&lt;/li&gt;
&lt;li&gt;Google Drive returning 404 for files that technically still exist&lt;/li&gt;
&lt;li&gt;Folder &lt;code&gt;parentId&lt;/code&gt; returning &lt;code&gt;'root'&lt;/code&gt; vs the actual root folder ID
(they mean different things)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;## Lessons learned&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Local-first is harder than it sounds — not the storage, but
 the sync&lt;/li&gt;
&lt;li&gt;IndexedDB is powerful but needs a proper abstraction layer&lt;/li&gt;
&lt;li&gt;PWA Service Workers will surprise you in production every time&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you're building anything with Google Drive API or IndexedDB,&lt;br&gt;
  happy to share more details in the comments.&lt;/p&gt;




&lt;p&gt;PenPage: &lt;a href="https://penpage.com" rel="noopener noreferrer"&gt;https://penpage.com&lt;/a&gt;&lt;/p&gt;




</description>
      <category>productivity</category>
      <category>showdev</category>
      <category>sideprojects</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
