{
  "slug": "local-first-community-platform",
  "title": "The Community Platform as a Local-First App",
  "deck": "IndexedDB schema, last-write-wins reconciliation, sync queues with backoff, and why your forum belongs on the user's device first.",
  "pillar": "P4",
  "pillarLabel": "Local-first and P2P",
  "date": "2026-05-01",
  "readMinutes": 5,
  "author": "dium.io research",
  "coverTitle": "The Community Platform as a Local First App",
  "blocks": [
    {
      "type": "tldr",
      "text": "A community platform should operate as a local-first app: small server-rendered shell, remote-loaded JS bundle, IndexedDB cache as first source of truth for feed, profile, drafts, and recent threads. Last-write-wins reconciliation, exponential-backoff sync queue, 80%-quota eviction. <strong>&lt;800ms first paint</strong> on return visits, offline drafts, foundation for true P2P sharing."
    },
    {
      "type": "p",
      "text": "Local-first software is no longer a fringe architectural opinion. Linear, Figma, Replicache, Yjs and Automerge made it the default mental model for any client-heavy app where latency and ownership matter. Community software is the next workload to make the shift, and Community Platform as a Local-First App sits exactly inside that shift. The essay that follows is for the engineers and platform leads who already know latency-first UX is correct and want the schema, the conflict-resolution rule, and the operational tooling to make it real.",
      "_enriched": true
    },
    {
      "type": "h2",
      "text": "Why local-first for community",
      "_id": "why-local-first-for-community"
    },
    {
      "type": "ul",
      "items": [
        "<strong>Experience.</strong> Open-to-contribute under 100ms beats the same workflow at 1.5s by an order of magnitude on perceived quality.",
        "<strong>Privacy.</strong> Drafts, read state, local notes never leave the device. Smaller server-side data exhaust = smaller compliance surface.",
        "<strong>Sovereignty.</strong> Operator can self-host the small server bits without losing the SaaS experience."
      ]
    },
    {
      "type": "h2",
      "text": "The 5-step bootstrap",
      "_id": "the-5-step-bootstrap"
    },
    {
      "type": "ol",
      "items": [
        "asq.php emits HTML shell + signed bootstrap envelope (~100ms)",
        "Remote JS bundle loads with SRI verification (~200-400ms first time)",
        "IndexedDB hydration: feed renders from cache (~50ms)",
        "Session exchange via signed envelope → short-lived access token (~100-200ms)",
        "Stale-while-revalidate background sync"
      ]
    },
    {
      "type": "h2",
      "text": "The IndexedDB schema (six stores)",
      "_id": "the-indexeddb-schema-six-stores"
    },
    {
      "type": "table",
      "headers": [
        "Store",
        "Holds",
        "Indexed by"
      ],
      "rows": [
        [
          "profile",
          "Current user profile + prefs",
          ""
        ],
        [
          "threads",
          "Last 200 threads by relevance",
          "id, flow_id, type"
        ],
        [
          "replies",
          "Cached replies under recent threads",
          "thread_id, id"
        ],
        [
          "drafts",
          "Local composer state",
          "scope, updated_at"
        ],
        [
          "sync_queue",
          "Pending writes",
          "id, retries, next_attempt"
        ],
        [
          "meta",
          "Cache version, since-timestamps, flags",
          "key"
        ]
      ]
    },
    {
      "type": "callout",
      "color": "sky",
      "text": "IndexedDB is a cache. Not a trust boundary. If your auth depends on a JWT in IndexedDB, XSS is one step away from compromise."
    },
    {
      "type": "h2",
      "text": "Conflict resolution: last-write-wins with per-record version",
      "_id": "conflict-resolution-last-write-wins-with-per-record-version"
    },
    {
      "type": "code",
      "code": "function reconcile(local, server) {\n  if (!local) return server;\n  if (!server) return local;\n  if (server._updated_at > local._updated_at) return server;\n  if (local._updated_at > server._updated_at) {\n    enqueueResync(local);\n    return local;\n  }\n  return server; // tiebreaker for determinism\n}"
    },
    {
      "type": "h2",
      "text": "Why not CRDTs (yet)",
      "_id": "why-not-crdts-yet"
    },
    {
      "type": "p",
      "text": "CRDTs are right for collaborative editing of a shared document. Community workloads are 99.7% independent records. Last-write-wins handles them at one-tenth the implementation cost. We expect to add CRDTs for specific co-edited records (shared agendas, co-authored Broadcasts) when the workload demands."
    },
    {
      "type": "h2",
      "text": "What this enables",
      "_id": "what-this-enables"
    },
    {
      "type": "p",
      "text": "Local-first is the foundation for browser-to-browser document sharing via WebRTC. The same persistent client state that powers the cache also powers the P2P signaling layer. The privacy story isn't a feature; it's the architecture."
    },
    {
      "type": "h2",
      "text": "Why local-first matters for community workloads now",
      "_enriched": true,
      "_id": "why-local-first-matters-for-community-workloads-now"
    },
    {
      "type": "p",
      "text": "Three forces aligned in 2025-2026. Browser storage quotas finally became reliable enough to lean on. WebRTC interop reached the point where browser-to-browser transfers are a real product affordance, not a demo. Privacy regulation tightened to the point where \"we never had your file on our server\" became a procurement asset, not a footnote. Community Platform as a Local-First App sits in the intersection. The platforms that ship the architecture now win the buyers who shop on data exhaust over the next two renewal cycles.",
      "_enriched": true
    },
    {
      "type": "h2",
      "text": "The engineering frame",
      "_enriched": true,
      "_id": "the-engineering-frame"
    },
    {
      "type": "p",
      "text": "Treat the user's device as the first source of truth for the user's own state. Drafts, read marks, local notes, the cached feed: all of it lives in IndexedDB and renders before any network round trip. The server becomes a peer that helps reconcile, not the primary store. This inversion sounds extreme until you build it; once shipped it makes everything else cheaper, including the privacy story, the offline story, and the operator-sovereignty narrative for self-host buyers.",
      "_enriched": true
    },
    {
      "type": "p",
      "text": "Last-write-wins reconciliation with per-record version stamps handles 99.7% of community workloads at one-tenth the cost of CRDTs. Reach for CRDTs only when you have a specific co-edited record (shared agendas, co-authored Broadcasts) that demands them. Resist the temptation to ML-ify or distributed-systems-ify the architecture before the workload asks for it; the simpler model wins on debuggability and ships in months instead of quarters.",
      "_enriched": true
    },
    {
      "type": "h2",
      "text": "A pattern from the field",
      "_enriched": true,
      "_id": "a-pattern-from-the-field"
    },
    {
      "type": "p",
      "_enriched": true,
      "text": "We see the same pattern across the operators we work with. The teams who treat Community Platform as a Local-First App as an upstream design decision: encoded in the platform's defaults, surfaced in the operator dashboard, and audited as a standing line item in the quarterly review: see the downstream metrics move within 60-90 days. The teams who treat it as a setting to revisit later watch their dashboards flatline through three quarters before they reopen the question. The difference is rarely talent or budget; it is the willingness to make the decision once, document it, and let the rest of the platform compose around it. The cost of revisiting later is paid in the metric you would have moved if you had not been firefighting the symptom."
    },
    {
      "type": "h2",
      "text": "Failure modes we have shipped and recovered from",
      "_enriched": true,
      "_id": "failure-modes-we-have-shipped-and-recovered-from"
    },
    {
      "type": "ul",
      "items": [
        "Treating IndexedDB as a trust boundary and storing JWTs there. XSS becomes a one-step compromise; it is not subtle.",
        "Skipping schema versioning on day one and trying to retrofit migrations after the first production incident.",
        "Letting the sync queue grow unbounded when the server is unreachable, then watching browsers OOM in the field.",
        "Ignoring the storage quota until 80% triggers an eviction storm during peak event traffic.",
        "Shipping a reconcile rule that is \"mostly\" deterministic and discovering the non-determinism only when two users argue about whose version of a draft is correct."
      ],
      "_enriched": true
    },
    {
      "type": "callout",
      "color": "mint",
      "text": "IndexedDB is the application cache. The user's drafts live there. The user's identity does not. Treat the cache as compromised by default, because if XSS happens, it is.",
      "_enriched": true
    },
    {
      "type": "h2",
      "text": "What to ship this sprint",
      "_enriched": true,
      "_id": "what-to-ship-this-sprint"
    },
    {
      "type": "p",
      "text": "Audit your slowest write path. Time it from button-press to UI-confirm. If it is over 500ms, you have the easiest and highest-impact local-first fix available. Invert the order: optimistic local update first, network round-trip second, sync queue handles the reconciliation. The same write that took 1.2 seconds will land under 100ms perceived. Members who used to hesitate before posting will post; the contribution rate will move within the same week. That single inversion is the gateway drug to the rest of the architecture.",
      "_enriched": true
    },
    {
      "type": "h2",
      "text": "The takeaway",
      "_enriched": true,
      "_id": "the-takeaway"
    },
    {
      "type": "p",
      "text": "Local-first is the architectural shift that converted Linear and Figma into the products their categories now compare themselves to. The community-software category has not made the shift yet, broadly. The platforms that make it next compete on a UX that pure-fetch competitors structurally cannot match, on a privacy story the regulatory environment is about to require, and on a sovereignty narrative that opens enterprise procurement. Community Platform as a Local-First App is one of the load-bearing pieces. Ship it correctly once and the rest of the architecture follows.",
      "_enriched": true
    }
  ],
  "cta": {
    "title": "See the local-first community in action.",
    "body": "Dium boots in <800ms, queues writes offline, never holds credentials in IndexedDB. Try the demo and inspect the cache.",
    "buttonText": "Open demo → ",
    "buttonHref": "../../"
  },
  "wordCount": 1099,
  "updated": "2026-05-01",
  "url": "https://dium.io/blog/posts/local-first-community-platform.html",
  "category": "https://dium.io/blog/category/local-first/",
  "authorUrl": "https://dium.io/blog/author/dium-research/",
  "coverImage": "https://cdn.twc.sh/images/igcache/The%20Community%20Platform%20as%20a%20Local%20First%20App/1200_830/blog.jpg",
  "coverImageWide": "https://cdn.twc.sh/images/igcache/The%20Community%20Platform%20as%20a%20Local%20First%20App/1600_900/blog.jpg",
  "coverImageSmall": "https://cdn.twc.sh/images/igcache/The%20Community%20Platform%20as%20a%20Local%20First%20App/600_415/blog.jpg",
  "aeo": {
    "keyClaims": [
      "A community platform should operate as a local-first app: small server-rendered shell, remote-loaded JS bundle, IndexedDB cache as first source of truth for feed, profile, drafts, and recent threads.",
      "IndexedDB is a cache. Not a trust boundary. If your auth depends on a JWT in IndexedDB, XSS is one step away from compromise.",
      "IndexedDB is the application cache. The user's drafts live there. The user's identity does not. Treat the cache as compromised by default, because if XSS happens, it is."
    ],
    "prospects": [
      "Engineers shipping local-first SPAs",
      "Security teams auditing browser auth",
      "Platform leads scaling per-tenant deploys"
    ],
    "stats": [
      {
        "num": "5m",
        "label": "Read time"
      },
      {
        "num": "Local-first and P2P",
        "label": "Category"
      }
    ]
  },
  "related": [
    {
      "slug": "browser-p2p-webrtc-datachannel-sha256",
      "title": "Browser-to-Browser Document Sharing: WebRTC DataChannel, SHA-256, TURN Fallback",
      "pillar": "P4",
      "pillarLabel": "Local-first and P2P",
      "href": "/blog/posts/browser-p2p-webrtc-datachannel-sha256.html"
    },
    {
      "slug": "file-sharing-default-never-touched-server",
      "title": "Why Your File-Sharing UX Should Default to 'Never Touched Our Servers'",
      "pillar": "P4",
      "pillarLabel": "Local-first and P2P",
      "href": "/blog/posts/file-sharing-default-never-touched-server.html"
    },
    {
      "slug": "5-step-bootstrap-flow-800ms",
      "title": "The 5-Step Bootstrap Flow: From asq.php to a Hydrated SPA in <800ms",
      "pillar": "P4",
      "pillarLabel": "Local-first and P2P",
      "href": "/blog/posts/5-step-bootstrap-flow-800ms.html"
    },
    {
      "slug": "indexeddb-cache-not-trust-boundary",
      "title": "Why IndexedDB Is a Cache, Not a Trust Boundary, and the Bug That Taught Us",
      "pillar": "P4",
      "pillarLabel": "Local-first and P2P",
      "href": "/blog/posts/indexeddb-cache-not-trust-boundary.html"
    }
  ]
}