<?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>Forem: Max Friedmann</title>
    <description>The latest articles on Forem by Max Friedmann (@maxfriedmann).</description>
    <link>https://forem.com/maxfriedmann</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%2F2532425%2F55e69066-bb60-485b-b25d-93c600b33500.jpeg</url>
      <title>Forem: Max Friedmann</title>
      <link>https://forem.com/maxfriedmann</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/maxfriedmann"/>
    <language>en</language>
    <item>
      <title>Works Offline, Syncs Automatically: How smallstack Implements Local-First Architecture</title>
      <dc:creator>Max Friedmann</dc:creator>
      <pubDate>Mon, 13 Apr 2026 12:07:17 +0000</pubDate>
      <link>https://forem.com/smallstack/works-offline-syncs-automatically-how-smallstack-implements-local-first-architecture-1poo</link>
      <guid>https://forem.com/smallstack/works-offline-syncs-automatically-how-smallstack-implements-local-first-architecture-1poo</guid>
      <description>&lt;p&gt;Most web apps are optimized for the happy path: fast internet, reliable servers, users sitting at their desks. But the real world looks different. Field technicians in basements. Property managers in underground parking garages. Service workers on remote sites. For these users, "your connection was lost — please try again" is not an acceptable error message.&lt;/p&gt;

&lt;p&gt;This is why we built smallstack on a local-first architecture. Here's what that actually means in practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local-First vs. "Offline Mode"
&lt;/h2&gt;

&lt;p&gt;"Offline mode" is what most apps call it when they let you view cached content without an internet connection. You can browse — but the moment you try to save something, you hit a wall.&lt;/p&gt;

&lt;p&gt;Local-first is different in a fundamental way: &lt;strong&gt;the local device is the primary storage location.&lt;/strong&gt; The app reads from and writes to local storage first. The server is a sync partner, not the single source of truth.&lt;/p&gt;

&lt;p&gt;The practical difference:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Offline mode&lt;/strong&gt;: read-only without connection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local-first&lt;/strong&gt;: full read/write without connection, automatic sync when reconnected&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How Incremental Sync Works
&lt;/h2&gt;

&lt;p&gt;The challenge with local-first isn't the offline part — that's straightforward. The hard part is sync: how do you reconcile changes made by multiple users who were offline at the same time?&lt;/p&gt;

&lt;p&gt;smallstack uses &lt;strong&gt;SignalDB&lt;/strong&gt; for incremental sync. The key design decision: instead of syncing entire collections, we sync only the deltas — changes since the last known sync point. Each change carries a timestamp. On reconnect, the sync protocol exchanges only what changed since the last successful sync.&lt;/p&gt;

&lt;p&gt;This approach has several practical advantages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Minimal bandwidth&lt;/strong&gt; — particularly important for users on mobile connections&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fast reconnect&lt;/strong&gt; — syncing 50 changed records is much faster than re-downloading thousands&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time propagation&lt;/strong&gt; — changes from one user appear at other clients within seconds of reconnecting&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conflict Resolution
&lt;/h2&gt;

&lt;p&gt;When two users edit the same field while offline, you have a conflict. Our approach for most cases is &lt;strong&gt;last-write-wins&lt;/strong&gt; with timestamp ordering. The most recent change (by device clock) wins.&lt;/p&gt;

&lt;p&gt;This covers the majority of real-world cases. For more complex scenarios — like numeric counters that both users incremented — the data model can define merge strategies. The goal is predictable behavior that teams can reason about, not invisible magic.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Real Scenario: Field Service Inspection
&lt;/h2&gt;

&lt;p&gt;Here's a concrete example of how this plays out:&lt;/p&gt;

&lt;p&gt;A service technician arrives at an industrial site with no mobile coverage. They open the smallstack PWA (installed from browser — no app store needed). The app loads with all their assigned tasks already cached from the last sync.&lt;/p&gt;

&lt;p&gt;They complete three inspections, attach photos, add notes. All changes are written to local IndexedDB immediately — no spinners, no "saving..." indicators. The UI is instant.&lt;/p&gt;

&lt;p&gt;An hour later, back in the parking lot with signal, the app reconnects. SignalDB runs the incremental sync: the three new inspection records and their attached file references are pushed to the server. Within seconds, the office team can see the completed inspections on their dashboard.&lt;/p&gt;

&lt;p&gt;No manual "sync now" button. No lost data. No double-entry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Architecture Matters for Business Apps
&lt;/h2&gt;

&lt;p&gt;For consumer apps, offline support is a nice-to-have. For business-critical workflows, it's different: if your team can't record data, they either lose it or work around your tools. Both outcomes are bad.&lt;/p&gt;

&lt;p&gt;Local-first removes this failure mode entirely. The app works regardless of connection state. This makes it genuinely suitable for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Field service teams&lt;/strong&gt; working in buildings with poor coverage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trades and construction&lt;/strong&gt; where sites often have no reliable WiFi&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Property management&lt;/strong&gt; — inspections, maintenance logs, tenant records&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Any team&lt;/strong&gt; that needs reliability over SaaS uptime dependence&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Performance Bonus
&lt;/h2&gt;

&lt;p&gt;Offline capability is the headline, but local-first has a performance side effect that benefits everyone — including users who are always online.&lt;/p&gt;

&lt;p&gt;When data lives locally, read operations are instant. Filtering a list of 5,000 records? No network round-trip. The UI is as fast as the local machine, not as fast as the network.&lt;/p&gt;

&lt;p&gt;This changes how you can design UIs. You can afford live filtering, real-time search, and reactive dashboards without debouncing every keystroke or showing skeleton loaders everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;smallstack ships this local-first architecture as part of its no-code platform — you get offline capability and real-time sync without writing a line of sync code yourself. The widget-based app builder sits on top of this foundation.&lt;/p&gt;

&lt;p&gt;If you want to dig deeper into the technical side, check out the &lt;a href="https://docs.smallstack.com" rel="noopener noreferrer"&gt;smallstack documentation&lt;/a&gt; or try building a simple app on the free tier to see the sync behavior in action on &lt;a href="https://smallstack.com" rel="noopener noreferrer"&gt;smallstack.com&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Building something local-first? Have questions about the sync approach? Drop a comment — happy to get into the details.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>CRDTs and Local-First Architecture: How smallstack Handles Offline Conflict Resolution</title>
      <dc:creator>Max Friedmann</dc:creator>
      <pubDate>Wed, 08 Apr 2026 20:30:16 +0000</pubDate>
      <link>https://forem.com/smallstack/crdts-and-local-first-architecture-how-smallstack-handles-offline-conflict-resolution-338c</link>
      <guid>https://forem.com/smallstack/crdts-and-local-first-architecture-how-smallstack-handles-offline-conflict-resolution-338c</guid>
      <description>&lt;h1&gt;
  
  
  CRDTs and Local-First Architecture: How smallstack Handles Offline Conflict Resolution
&lt;/h1&gt;

&lt;p&gt;Most web apps treat the network as a prerequisite. No connection? You get a spinner, an error banner, or worse — silent data loss. At smallstack, we made a different choice from the start: &lt;strong&gt;local-first&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This post breaks down what that means technically, how we use CRDTs and SignalDB to handle conflict resolution, and why this architecture matters for building serious business software.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with "Offline Mode"
&lt;/h2&gt;

&lt;p&gt;"Offline mode" is a spectrum. At the bad end: you cache the last-loaded page and show a banner saying "you're offline, changes won't be saved." At the slightly better end: you queue writes locally and flush them when connectivity returns — but without any conflict handling, so the last writer wins (silently destroying the other's work).&lt;/p&gt;

&lt;p&gt;Neither is good enough for field teams who genuinely work disconnected for hours at a time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local-first&lt;/strong&gt; inverts the model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The local device is the &lt;strong&gt;primary data store&lt;/strong&gt;, not a cache.&lt;/li&gt;
&lt;li&gt;Reads are instant — no network roundtrip.&lt;/li&gt;
&lt;li&gt;Writes are committed locally first, then synced.&lt;/li&gt;
&lt;li&gt;The server's job is &lt;strong&gt;replication between peers&lt;/strong&gt;, not the source of truth.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This changes the UX fundamentally: no loading spinners, no "connection lost" interruptions, no perceived latency. But it requires solving a hard problem — what happens when two users modify the same record while offline?&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter CRDTs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;CRDT&lt;/strong&gt; stands for Conflict-free Replicated Data Type. A CRDT is a data structure where concurrent modifications from multiple sources can always be merged into a consistent result without coordination — no locks, no conflict resolution UI, no data loss.&lt;/p&gt;

&lt;p&gt;The key insight: instead of storing "the current value," CRDTs store &lt;strong&gt;operations&lt;/strong&gt; or &lt;strong&gt;state vectors&lt;/strong&gt; that can be merged in any order and always produce the same result (a property called &lt;em&gt;commutativity&lt;/em&gt; and &lt;em&gt;associativity&lt;/em&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  A practical example
&lt;/h3&gt;

&lt;p&gt;Two field technicians, Anna and Ben, are offline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Anna updates the status of Work Order #42 to &lt;code&gt;in_progress&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Ben adds a note to Work Order #42: "Need replacement part."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When both sync:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two independent operations on the same record.&lt;/li&gt;
&lt;li&gt;Both changes are present in the merged result.&lt;/li&gt;
&lt;li&gt;No conflict.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What if both update the &lt;em&gt;same field&lt;/em&gt; — say, the status field?&lt;/p&gt;

&lt;p&gt;smallstack uses &lt;strong&gt;Last-Write-Wins (LWW)&lt;/strong&gt; semantics for scalar fields, with a logical timestamp (not wall clock). The operation with the higher timestamp wins. For collaborative text editing or list structures, LWW doesn't cut it — and that's where proper CRDT types (like Logoot or RGA for sequences) come in.&lt;/p&gt;

&lt;h2&gt;
  
  
  SignalDB: Incremental Sync in Practice
&lt;/h2&gt;

&lt;p&gt;smallstack uses &lt;a href="https://github.com/maxnowack/signaldb" rel="noopener noreferrer"&gt;SignalDB&lt;/a&gt; — a reactive, local-first database for the browser built with TypeScript. It's the layer between our SvelteKit frontend and the MongoDB backend.&lt;/p&gt;

&lt;p&gt;The sync protocol works like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client → Server: "Give me everything updated after timestamp T"
Server → Client: [delta of changed documents]
Client:          Merge deltas into local collection
                 Notify reactive subscribers ($state runes update automatically)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every document in SignalDB has an &lt;code&gt;updatedAt&lt;/code&gt; field. On reconnect, the client sends its latest known timestamp, and the server returns only what's changed since then — not the full dataset. This makes initial sync fast and incremental updates tiny.&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;// Simplified example of how sync works under the hood&lt;/span&gt;
&lt;span class="nx"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;pull&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lastPulledAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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;changes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/orders?updatedAfter=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;lastPulledAt&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;changes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Only the delta&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;changes&lt;/span&gt;&lt;span class="p"&gt;)&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;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/orders&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;changes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;});&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;Svelte 5 runes make this reactive binding seamless:&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;// In a .svelte.ts service file&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;orders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$state&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;

  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// SignalDB collection drives $state reactively&lt;/span&gt;
    &lt;span class="nx"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&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;Every time the sync delivers new data, &lt;code&gt;orders&lt;/code&gt; updates automatically, and all components using it re-render. No manual refresh, no &lt;code&gt;writable()&lt;/code&gt; stores — just reactive local state that happens to be backed by a synced collection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-Time on Top of Local-First
&lt;/h2&gt;

&lt;p&gt;Local-first doesn't mean "eventually consistent and slow." smallstack also supports real-time push from the server using server-sent events. When a user makes a change:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It's committed locally (instant, no network wait).&lt;/li&gt;
&lt;li&gt;It's pushed to the server asynchronously.&lt;/li&gt;
&lt;li&gt;The server broadcasts the change to other connected clients.&lt;/li&gt;
&lt;li&gt;Those clients merge the incoming delta into their local store.&lt;/li&gt;
&lt;li&gt;Their reactive state updates.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;From the perspective of a connected user, it feels like Google Docs — changes appear immediately. From the perspective of a disconnected user, nothing breaks — changes queue locally and sync when reconnected.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Means for the Architecture
&lt;/h2&gt;

&lt;p&gt;Building local-first requires discipline:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No server-side rendering for user data.&lt;/strong&gt; If data lives in the client's SignalDB, SSR can't hydrate it. We SSR only public, non-personalized content. Authenticated app views are entirely client-rendered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Schema must travel with data.&lt;/strong&gt; When a user defines a custom data type in smallstack (our no-code type system), that schema has to be available locally for validation and rendering — even offline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Migrations are tricky.&lt;/strong&gt; If the server schema changes and a client is offline, we need to handle schema version mismatches gracefully on sync. We version migrations and apply them lazily on the client.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conflict policy must be explicit.&lt;/strong&gt; For every field type, we define the merge strategy upfront: LWW for most scalar fields, custom merge for structured types like address objects, append-only for logs and history.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Most No-Code Platforms Can't Do This
&lt;/h2&gt;

&lt;p&gt;Building local-first isn't a feature you can add later. It requires rethinking the data flow from day one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Central-server tools (Airtable, Monday, Notion) make the server the source of truth. Offline is an afterthought — literally bolted on after the fact.&lt;/li&gt;
&lt;li&gt;Adding CRDT-based sync to an existing server-centric codebase means rebuilding the entire persistence and sync layer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We made the local-first decision before writing the first line of product code. That's why it actually works — on construction sites, in basements, on planes, in tunnels.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;If you're building business software that needs to work in environments with unreliable connectivity, the local-first approach is worth the investment. SignalDB is open source and well-documented — a good starting point.&lt;/p&gt;

&lt;p&gt;smallstack is the no-code platform built on this foundation. If you want to see what it looks like to give non-technical users a reliable, offline-capable business app — &lt;a href="https://docs.smallstack.com" rel="noopener noreferrer"&gt;check out the docs&lt;/a&gt; or try the free tier.&lt;/p&gt;

&lt;p&gt;Questions about the architecture? Drop them in the comments — happy to go deeper on any part of this.&lt;/p&gt;

</description>
      <category>signaldb</category>
      <category>crdt</category>
      <category>conflictresolution</category>
      <category>localfirst</category>
    </item>
    <item>
      <title>I built a CLI to see my real GitHub language stats – does something like this already exist?</title>
      <dc:creator>Max Friedmann</dc:creator>
      <pubDate>Sun, 22 Feb 2026 19:03:19 +0000</pubDate>
      <link>https://forem.com/maxfriedmann/i-built-a-cli-to-see-my-real-github-language-stats-does-something-like-this-already-exist-1n18</link>
      <guid>https://forem.com/maxfriedmann/i-built-a-cli-to-see-my-real-github-language-stats-does-something-like-this-already-exist-1n18</guid>
      <description>&lt;p&gt;I recently created my new online CV and wanted to show all of my contributions — also the ones from private repositories. My GitHub profile looked nearly empty, even though I'd been coding professionally for years. Most of that work just happened to live in private repos.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqtth7iou69b1eyyww1ld.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqtth7iou69b1eyyww1ld.png" alt=" " width="800" height="956"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Example of how the output data can be visualized — this chart is from my CV app, built on top of the JSON data this tool produces&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;That's when I realized: GitHub's profile is basically useless for showing your real activity as a professional developer. So I started looking for a tool that could compute honest, per-language stats from &lt;em&gt;all&lt;/em&gt; my commits — public and private. I couldn't find one. So I built it: &lt;strong&gt;&lt;code&gt;github-lang-stats&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;It scans every commit you've personally authored on GitHub — including private repos — and calculates how many lines you've changed per programming language. The result is an honest, verifiable snapshot of your actual work.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx github-lang-stats &lt;span class="nt"&gt;--token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;your-github-pat&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No install needed. Just a GitHub Personal Access Token.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just use the GitHub profile?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No language breakdown&lt;/strong&gt; — GitHub shows total contributions, not which languages you used&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Private repos are invisible&lt;/strong&gt; — Most (of my) professional work happens in private repos, which don't show up at all&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it out!
&lt;/h2&gt;

&lt;p&gt;👉 &lt;a href="https://www.npmjs.com/package/github-lang-stats" rel="noopener noreferrer"&gt;github-lang-stats on npm&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'd love to hear your thoughts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Did you know of a similar tool? (I couldn't find one!)&lt;/li&gt;
&lt;li&gt;Would you use this for your portfolio or resume?&lt;/li&gt;
&lt;li&gt;What features would you want to see?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Drop a comment below — all feedback, issues, and PRs are welcome on &lt;a href="https://github.com/smallstack/github-lang-stats" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;! ⭐&lt;/p&gt;

</description>
      <category>github</category>
      <category>language</category>
      <category>stats</category>
      <category>privaterepositories</category>
    </item>
  </channel>
</rss>
