<?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 Shen</title>
    <description>The latest articles on DEV Community by Phinn Shen (@phinnshen).</description>
    <link>https://dev.to/phinnshen</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%2F4143051%2F7bb3517c-9272-4c3f-9500-1ac063993f04.png</url>
      <title>DEV Community: Phinn Shen</title>
      <link>https://dev.to/phinnshen</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/phinnshen"/>
    <language>en</language>
    <item>
      <title>37 days of zero App Store sales: I pulled the data on all 41 of my apps</title>
      <dc:creator>Phinn Shen</dc:creator>
      <pubDate>Wed, 07 Oct 2026 14:58:18 +0000</pubDate>
      <link>https://dev.to/phinnshen/37-days-of-zero-app-store-sales-i-pulled-the-data-on-all-41-of-my-apps-2kl2</link>
      <guid>https://dev.to/phinnshen/37-days-of-zero-app-store-sales-i-pulled-the-data-on-all-41-of-my-apps-2kl2</guid>
      <description>&lt;p&gt;&lt;em&gt;A real post-mortem from an indie dev. All numbers come from the App Store Connect API and GitHub Traffic API — reproducible.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;I have 41 apps on the App Store. In early October I pulled the Sales Report: &lt;strong&gt;from Sep 1 to Oct 7, 37 straight days, daily units = 0.&lt;/strong&gt; Not "sales slowed down" — not a single sale for over a month.&lt;/p&gt;

&lt;p&gt;The contrast stings more: in the same period my GitHub repo was cloned by ~1,000 real users over 70 days (deduplicated, CI excluded), peaking at 80 clones/day. Same developer, same products. A thousand people watching on GitHub, zero transactions on the App Store.&lt;/p&gt;

&lt;p&gt;This post is the diagnosis and the fixes. If you ship indie apps, you can skip this hole entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. First, rule out a broken billing system
&lt;/h2&gt;

&lt;p&gt;Obvious first suspect: the data. Pulled ASC Sales Report day by day:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-09-01 units: 0
2026-09-15 units: 0
2026-09-30 units: 0
2026-10-07 units: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Side quest discovered here: &lt;strong&gt;ASC only keeps ~30 days of Sales Reports.&lt;/strong&gt; Older requests return 400. If you don't snapshot your sales data locally, long-window retrospectives become impossible. That's also why I now snapshot GitHub traffic into the repo daily.&lt;/p&gt;

&lt;p&gt;The data is fine. It's just zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Then the store structure: 15 live apps, 0 free
&lt;/h2&gt;

&lt;p&gt;Of the 41 apps, 26 are invisible (in review, rejected, delisted). 15 are actually downloadable.&lt;/p&gt;

&lt;p&gt;Pulled pricing for all 15 via iTunes Lookup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$0.99 ×2 · $2.99 ×4 · $3.99 ×1 · $4.99 ×4 · $5.99 ×2 · $9.99 ×2 · $12.99 ×2 (flagship AI workbench)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Number priced at zero: &lt;strong&gt;0.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;All paid upfront. No free entry point anywhere.&lt;/p&gt;

&lt;p&gt;What that means: no matter where traffic comes from — GitHub, X, dev.to — the user lands on a product page whose first screen is a buy button. No brand, no ratings (0 ratings store-wide, a separate death loop), no trial. The paywall is a wall.&lt;/p&gt;

&lt;p&gt;My mistake was classic: when content marketing brought ~40 GitHub clones/day in August, I thought "traffic → trust" was established. Actually the funnel was broken &lt;em&gt;earlier&lt;/em&gt;: not one app could be opened and used for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The fix: a free window
&lt;/h2&gt;

&lt;p&gt;Four apps are now free to download until Oct 31, including my main product KinetBrief (an AI note app). This isn't a clearance sale — it's fixing "can this be used at all" before asking "will you pay".&lt;/p&gt;

&lt;p&gt;An ASC gotcha worth recording: &lt;strong&gt;iTunes Lookup API caches prices for hours.&lt;/strong&gt; Don't verify a price change via the API right after making it — you'll see the stale price and panic. The ASC backend price schedule is the source of truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Traffic-side autopsy: three channels died at once
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Reddit (dead, not salvageable)
&lt;/h3&gt;

&lt;p&gt;My account u/PhinnShen now shows "suspended". Post-mortem: in early September I posted product threads across multiple subreddits at a rate of 3 per 24h. By mid-September every post was nuked by the site-level spam filter (115 comments, comment_karma = 0 — not a single one counted).&lt;/p&gt;

&lt;p&gt;Two lessons: (1) &lt;strong&gt;shadow removal is always visible to the author&lt;/strong&gt; — you think it's live; nobody else can see it. Verify via third-party archives like Arctic Shift. (2) &lt;strong&gt;comment_karma stuck at 0 is the fastest health check&lt;/strong&gt; that your account is flagged. Flagged accounts have no appeal path; rebuilding takes weeks, so factor that latency into any promotion plan.&lt;/p&gt;

&lt;h3&gt;
  
  
  X (alive, but dormant)
&lt;/h3&gt;

&lt;p&gt;Account survives, 57 followers, top tweet 89 likes (the build story of a local-first task app). Last post: Sep 30. No ban — I just stopped posting. (Now also carrying a "temporary label" that may reduce reach, so links go in replies, not main posts.)&lt;/p&gt;

&lt;h3&gt;
  
  
  juejin (my only proven engine, also stopped)
&lt;/h3&gt;

&lt;p&gt;In early August I published a hands-on comparison of 4 AI agent engines running the same real-world task. For those two weeks: 40 GitHub clones/day, the all-time peak. Then I stopped publishing in late September. The one channel the data actually validated — and I went quiet at the best moment.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Why 1,000 GitHub clones didn't become downloads
&lt;/h2&gt;

&lt;p&gt;The most interesting finding. 70 days, 1,592 person-days, ~1,000 unique cloners. The traffic was real. But:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;They clone the source, not the product.&lt;/strong&gt; Someone cloning a macOS AI workbench repo can compile it themselves; the App Store build adds nothing for them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The repo had no conversion hook.&lt;/strong&gt; The README CTA pointed to GitHub Releases (133 downloads total), not the App Store page. The last meter was never wired up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The two funnels overlap less than you'd think.&lt;/strong&gt; GitHub = developers who want to read the implementation. App Store = users who want a working thing. Additive, not interchangeable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fix: repo front page now has a 30-second TL;DR and an App Store link; release notes go out on both channels.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Summary: 0 downloads = structure × traffic, both zero
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;September state&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Store structure&lt;/td&gt;
&lt;td&gt;15 live apps, all paid, zero free entry&lt;/td&gt;
&lt;td&gt;4 apps free until Oct 31&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reddit&lt;/td&gt;
&lt;td&gt;account site-flagged&lt;/td&gt;
&lt;td&gt;rebuild slowly (weeks), short-term write-off&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;alive but silent 37 days&lt;/td&gt;
&lt;td&gt;resume weekly posting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;juejin&lt;/td&gt;
&lt;td&gt;stopped (the only proven engine)&lt;/td&gt;
&lt;td&gt;this post is the restart&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub → MAS&lt;/td&gt;
&lt;td&gt;1k+ clones, zero conversion hook&lt;/td&gt;
&lt;td&gt;README redesign&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The mirror image: fix structure without traffic, and nobody knows it's free. Fix traffic without structure, and you pour water into a paywall. &lt;strong&gt;Indie distribution is multiplication: any factor at zero means the result is zero.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Finally
&lt;/h2&gt;

&lt;p&gt;This is post one of a series. Next: 90 days of building a SwiftUI task manager (FTS5 Chinese search, on-device AI tiering, menu-bar form-factor trade-offs), and a follow-up agent-engine benchmark.&lt;/p&gt;

&lt;p&gt;The four free apps (incl. KinetBrief) are searchable as "Kinet" on the App Store, or via the README on my GitHub profile. Bring data-backed criticism, I can take it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Data sources: ASC Sales Report API + iTunes Lookup API + GitHub Traffic API, pulled 2026-10-07.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ios</category>
      <category>indiedev</category>
      <category>marketing</category>
      <category>appstore</category>
    </item>
    <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 Shen</dc:creator>
      <pubDate>Fri, 25 Sep 2026 14:06:45 +0000</pubDate>
      <link>https://dev.to/phinnshen/i-built-a-task-app-that-stores-everything-in-one-plain-sqlite-file-and-i-dont-regret-skipping-154m</link>
      <guid>https://dev.to/phinnshen/i-built-a-task-app-that-stores-everything-in-one-plain-sqlite-file-and-i-dont-regret-skipping-154m</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>
