<?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: Phinn</title>
    <description>The latest articles on DEV Community by Phinn (@phinn).</description>
    <link>https://dev.to/phinn</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4137618%2F0bd907c7-4cbe-425f-8c17-71f4daf4a117.png</url>
      <title>DEV Community: Phinn</title>
      <link>https://dev.to/phinn</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/phinn"/>
    <language>en</language>
    <item>
      <title>I built a task app that stores everything in one plain SQLite file — and I don't regret skipping Core Data</title>
      <dc:creator>Phinn</dc:creator>
      <pubDate>Tue, 22 Sep 2026 12:30:34 +0000</pubDate>
      <link>https://dev.to/phinn/i-built-a-task-app-that-stores-everything-in-one-plain-sqlite-file-and-i-dont-regret-skipping-ode</link>
      <guid>https://dev.to/phinn/i-built-a-task-app-that-stores-everything-in-one-plain-sqlite-file-and-i-dont-regret-skipping-ode</guid>
      <description>&lt;p&gt;&lt;em&gt;This is the engineering story behind &lt;a href="https://apps.apple.com/app/id6780944289" rel="noopener noreferrer"&gt;KinetTask&lt;/a&gt;, a Mac/iOS task app I shipped last week. It's a Show HN-adjacent story but dev.to gets the parts HN would skewer me for. Numbers and regrets included.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The decision everyone told me was wrong
&lt;/h2&gt;

&lt;p&gt;When I started building a local-first task app, every senior Swift dev I asked said the same thing: "Use GRDB. Or SwiftData. Writing SQLite by hand is masochism."&lt;/p&gt;

&lt;p&gt;I did it anyway: &lt;strong&gt;the SQLite3 C API, directly, in Swift 6.1&lt;/strong&gt;. Not because I'm tough — because I wanted exactly three things and none of them needed an ORM:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The database file &lt;strong&gt;is&lt;/strong&gt; the export format. Copy &lt;code&gt;tasks.sqlite3&lt;/code&gt; to another machine, it just opens. No "export" feature needed.&lt;/li&gt;
&lt;li&gt;Every agent action lands as an &lt;strong&gt;append-only journal row&lt;/strong&gt; — so when the AI planner retries a task, I can diff what actually changed instead of replaying blindly.&lt;/li&gt;
&lt;li&gt;Checkpoints store a &lt;strong&gt;hash of the normalized artifact&lt;/strong&gt;. Hash the normalized artifact, not the raw output — timestamps will gaslight you into thinking two identical results differ.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The actual surface area (it's small)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The whole "ORM":&lt;/span&gt;
&lt;span class="c1"&gt;// - open with WAL + busy_timeout&lt;/span&gt;
&lt;span class="c1"&gt;// - one prepared statement per query&lt;/span&gt;
&lt;span class="c1"&gt;// - one user_version pragma per schema migration step&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. A few tables, WAL mode, prepared statements, &lt;code&gt;PRAGMA user_version&lt;/code&gt; for migrations. The entire persistence layer is ~600 lines including the journal and checkpoint logic. GRDB would have been 150 lines of &lt;em&gt;my&lt;/em&gt; code plus a dependency I'd have to reason about during every weird concurrency bug.&lt;/p&gt;

&lt;p&gt;The honest cost: I wrote two use-after-free bugs against &lt;code&gt;sqlite3_stmt&lt;/code&gt; in the first week. C API gives you exactly enough rope.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug that justified the whole approach (v1.2.1)
&lt;/h2&gt;

&lt;p&gt;My iOS widget was silently losing every checkbox tap. Root cause, the fun kind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Main app's DB lived in its sandbox &lt;code&gt;Application Support&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The widget wrote to the &lt;strong&gt;App Group container&lt;/strong&gt; — a &lt;em&gt;different physical file&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Both files happily existed forever. No crash. No error. Just… widget taps vanishing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fix: unify the data root (App Group on iOS), migrate legacy DBs, turn on &lt;strong&gt;WAL + busy_timeout(2s)&lt;/strong&gt; because now two processes share one file, and add a snapshot-reconciliation pass when the main app becomes active — because the widget writing to SQLite doesn't magically invalidate the main app's in-memory cache, and its next &lt;code&gt;UPDATE&lt;/code&gt; would happily &lt;strong&gt;overwrite the tap with stale state&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;None of this was exotic. All of it was invisible until a real user compared widget state with list state. With Core Data this specific bug would've been &lt;em&gt;harder&lt;/em&gt; to create (shared store coordinator handles cross-process hints) — that's the fair counterargument, and I'd still make the same trade, because I can &lt;code&gt;sqlite3 tasks.sqlite3&lt;/code&gt; on a user's machine during support and &lt;em&gt;see everything&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What on-device LLM actually feels like
&lt;/h2&gt;

&lt;p&gt;The AI features (natural-language capture, "what should I focus on") run on-device: Apple's foundation model where available, a GGUF model via llama.cpp in-process otherwise.&lt;/p&gt;

&lt;p&gt;The real numbers nobody puts in marketing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cold inference on a 3B model: noticeably dumber than any cloud model. For "parse this sentence into a task with due date" it's fine. For "reorganize my projects" it's… enthusiastic.&lt;/li&gt;
&lt;li&gt;The failure mode isn't hallucination, it's &lt;strong&gt;over-triggering&lt;/strong&gt;: the model wants to help when it shouldn't. Fix wasn't a better model — it was an anti-filler gate: if the parse adds no fields beyond what you typed, don't create filler structure.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Ship the widget &lt;strong&gt;after&lt;/strong&gt; verifying both processes write the same file. Obvious in hindsight; it always is.&lt;/li&gt;
&lt;li&gt;The journal + hash checkpoint design came from my agent-engine side project, and it's the only reason I could debug the retry-drift cases at all. If your app has &lt;em&gt;any&lt;/em&gt; automated writes, an append-only log is cheap insurance.&lt;/li&gt;
&lt;li&gt;I still don't have sync. The file is yours — iCloud Drive it if you want. v1.2 added a &lt;code&gt;.kinettask&lt;/code&gt; backup format for machine moves, which covers 90% of what people actually ask for.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Free for 50 tasks, one-time $4.99, no subscription, no account, no telemetry: &lt;a href="https://apps.apple.com/app/id6780944289" rel="noopener noreferrer"&gt;App Store&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ask me anything about the SQLite internals in the comments — especially if you think skipping GRDB was stupid, I genuinely want that thread.&lt;/p&gt;

</description>
      <category>swift</category>
      <category>sqlite</category>
      <category>ios</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
