What a Local First Native Mac App Actually Looks Like on Disk
Every other page about this phrase is either philosophy or an Electron wrapper in a trench coat. This one opens the exact file Fazm writes to your disk, walks the schema table by table, and points at the one column that decides which rows ever leave your Mac.
Why Every Top Result for This Phrase Misses the Point
Search "local first native mac app" and the results split three ways. The Ink & Switch essay, which is brilliant but talks about CRDTs, not apps. A directory of web apps tagged "local-first" because they write to IndexedDB. And a long tail of Electron apps marketing themselves as native. None of them show you the inside of a real, shipping, native Mac app that treats the disk as the source of truth.
If you came here from a Reddit thread looking for a concrete answer to "what does local first even mean when I open the app bundle," this is the page. The short version: it means a file, a schema, and a flag. The long version is the rest of this page.
The Anchor Fact: One File, One Path
Fazm keeps everything you produce in one SQLite database per signed-in user. The code that builds the path lives in Desktop/Sources/AppDatabase.swift. Lines 238 to 257. You can diff it against any release tag.
That is the whole story about where your data lives. Not "somewhere in your home directory." Not "in the app container." A single file at a path you can paste into Terminal right now.
Prove It in 30 Seconds
Install Fazm, sign in once, then open Terminal. The file is there.
The three files are the real signature of a local-first SQLite app: the db, the shared-memory file, and the write-ahead log. If you see those three, writes survive a crash. If you see only one, they do not.
Seven Tables, Two Jobs
The schema splits cleanly in two. Tables that describe you to you (indexes, graphs, activity) stay local forever. Tables that feed the chat experience can selectively round-trip through the backend.
indexed_files
Every file Fazm has indexed from your drive. Path, filename, extension, size, folder, depth, created/modified timestamps. No backendSynced column. Never leaves the disk.
local_kg_nodes
Nodes of the on-device knowledge graph. Entities extracted from your files: people, projects, topics. Also local-only.
local_kg_edges
Typed relationships between nodes ("works_on", "mentions", "authored_by"). Pair with local_kg_nodes to traverse.
chat_messages
Assistant and user turns, per session_id. Carries backendSynced = false by default, so a message lives on your Mac until sync pushes it. Has a paired FTS5 virtual table.
ai_user_profiles
The profile summary Fazm writes about you from your activity. Also carries backendSynced so you can sync or not.
observer_activity
Insights and cards the background observer produces. Typed rows with a status lifecycle (pending → shown → acted or dismissed). Local-only.
chat_messages_fts
Virtual FTS5 table over chat_messages.messageText with content_rowid linkage. Added in migration fazmV5.
The One Column That Decides Everything
A lot of apps claim "local first." Few of them let you audit that claim in a single file. In Fazm, the claim is implemented as a boolean column named backendSynced, and the presence or absence of that column on a table tells you whether rows from that table will ever see the network.
Three tables can sync: ai_user_profiles, chat_messages, and task_chat_messages (renamed from chat_messages in migration fazmV3). Four tables cannot: indexed_files, local_kg_nodes, local_kg_edges, observer_activity. That is the whole local-first contract. No "trust us." No per-feature toggles. The schema is the contract.
What Stays Local vs What Can Sync
All five inputs write into one file. Only two of the resulting tables carry the flag that unlocks the outbound path.
The Numbers That Matter
Values pulled directly from Desktop/Sources/AppDatabase.swift. 1000 pages at 4 KB each = ~4 MB WAL before auto-checkpoint.
How Fazm Survives a Crash on Your Mac
WAL mode (write-ahead log) is how SQLite keeps concurrent reads and writes safe without locking the entire database. Fazm sets it on every open, and the checkpoint cadence is tuned for a desktop workload, not a server.
If WAL setup fails (read-only volume, disk I/O error on an external drive), Fazm falls back to the default rollback journal and keeps running. It does not refuse to launch. For a native Mac app that is someone's only copy of their chat history, refusing to launch is the wrong default.
The Schema, Version by Version
Five migrations brought the database from empty to what ships today. Nothing is hidden; the whole story is in one file.
fazmV1: the four founding tables
ai_user_profiles, indexed_files, local_kg_nodes, local_kg_edges. This is where the local-first split starts: the profile gets backendSynced, the three others do not.
fazmV2: task_chat_messages
Adds persistence for onboarding chat. Carries backendSynced from day one, because chat is the feature that benefits from optional sync.
fazmV3: rename to chat_messages
The rename generalized the table beyond onboarding. Data migration is a single ALTER TABLE, no schema rewrite, no data loss.
fazmV4: observer_activity
A single table for everything the background observer produces: insights, cards, skill drafts. Status lifecycle pending → shown → acted/dismissed. Local-only.
fazmV5: session_id + FTS5
Adds session_id to chat_messages and creates chat_messages_fts, a virtual FTS5 table using content='chat_messages' so there is still only one source of truth for message bodies.
Full-Text Search Over Your Own Chat History, Without a Server
In most "AI chat" apps, search over history means a server round- trip. In a local-first native Mac app, search over history is a SQLite FTS5 query against a virtual table on your disk.
The content='chat_messages' configuration is deliberate: it makes the FTS table a derived index of the real chat_messages rows, not a duplicate copy. Rebuild is a single INSERT. Searches are ms-fast. No network.
“A local-first app is one you can tar up and move. Fazm is one SQLite file per user plus a screenshot directory. That is the whole footprint.”
Fazm AppDatabase.swift
Local First Native vs Electron With a Local Store
Both claim the label. Only one of them actually behaves like a native Mac app when you pull the wifi cable.
| Feature | Typical Electron app | Fazm (native) |
|---|---|---|
| Runtime | Chromium + Node, ~150 MB at idle | Swift binary, no embedded browser |
| Data format | IndexedDB / LevelDB in profile dir | SQLite (WAL) at a path you can ls |
| Reads macOS accessibility tree | No (not available to web content) | Yes, AXUIElementCopyAttributeValue |
| Global hotkeys | Via Electron bridge, limited | Carbon RegisterEventHotKey directly |
| Launch at login | Via a helper bundle or electron-auto-launch | SMAppService.mainApp.register() |
| Offline reads | Depends on per-feature cache | Always, every table |
| Sync contract | Opaque, in JS | One boolean column, visible in schema |
| Audit the claim yourself | Read the minified JS bundle | Read one Swift file |
See the file yourself
Install Fazm, sign in once, and the database is on your disk within seconds. No account wall to see the schema.
Download Fazm →Why This Matters If You Came From a Reddit Thread
The honest reason people on r/macapps, r/selfhosted, and r/localllama keep pushing "local first native" is that they have been burned once. Either an app they trusted synced more than they expected, or they woke up to a "sunset" email and lost their notes, or they noticed an AI feature silently shipping every keystroke to a server.
The fix for all three is the same: the app's storage lives on your disk, the schema is auditable, and sync is a per-row decision, not an app-wide toggle. That is what the SQLite path above, the presence or absence of a single column, and the open source schema file are collectively saying.
If in six months Fazm disappears, you still have fazm.db. You can open it with any SQLite client, read every table, and export your data. That is the load-bearing property of a real local-first native Mac app.
FAQ: Local First Native Mac Apps
What counts as a local first native Mac app?
Three things have to be true at once. One, the binary is compiled for macOS (AppKit or SwiftUI, signed, notarized, distributed as a .app or .pkg), not an Electron window. Two, the app's primary read path is a file on your disk, not a request to a server. Three, writes succeed fully offline and sync is opt-in per record. Fazm satisfies all three: a Swift app signed by Mediar, a per-user SQLite database at ~/Library/Application Support/Fazm/users/{firebaseUid}/fazm.db, and a per-row backendSynced column that defaults to false.
Where does Fazm store its data on my Mac?
~/Library/Application Support/Fazm/users/{firebaseUid}/fazm.db, with the SQLite WAL and SHM sidecar files next to it. The screenshot cache lives at ~/Library/Application Support/Fazm/Screenshots/. If the database ever fails quick_check, a backup is written to ~/Library/Application Support/Fazm/backups/omi_corrupted_{timestamp}.db and the recovery path tries a SQLite .recover dump. All of that logic lives in Desktop/Sources/AppDatabase.swift at lines 238 through 745.
Does Fazm work offline?
Every read path does. File indexing, knowledge graph lookups, chat history search via the chat_messages_fts FTS5 virtual table, user profile, and settings all resolve from the local SQLite file. Chat inference calls a model provider over the network by default, but you can point Fazm at a local endpoint (Ollama, LM Studio) if you have one running. The data layer never needs the network to answer a query.
What does 'local first' actually mean in the Fazm schema?
It means some tables have a backendSynced boolean column, others do not. ai_user_profiles, chat_messages, and task_chat_messages carry the flag, so individual rows can be pushed to the backend later. indexed_files, local_kg_nodes, local_kg_edges, and observer_activity do not carry the flag and never leave the disk. You can verify this in Desktop/Sources/AppDatabase.swift lines 880 through 990: look at which CREATE TABLE blocks include the t.column("backendSynced", .boolean) line and which do not.
Why WAL mode and what does wal_autocheckpoint = 1000 mean?
WAL (write-ahead log) lets readers and writers proceed concurrently and survives most crashes without corrupting the main db file. Fazm sets journal_mode = WAL, synchronous = NORMAL, and wal_autocheckpoint = 1000 pages (roughly four megabytes) on open. See Desktop/Sources/AppDatabase.swift lines 388 through 392. If WAL fails (disk I/O error, read-only volume), the app falls back to the default journal mode and keeps going rather than refusing to launch.
How is this different from an Electron app that 'stores data locally'?
An Electron app is a Chromium instance wrapping a web page. The data file is usually an IndexedDB directory or a JSON blob the web app serializes. It cannot read the macOS accessibility tree, cannot register a Carbon global hotkey, cannot register with SMAppService for launch-at-login, and does not get any macOS power-management notifications. Fazm is a native Swift binary. It uses AXUIElementCreateApplication to read the frontmost window, SMAppService.mainApp.register() to start at login, and NSWorkspace observers for sleep, wake, lock, and unlock. None of those are available to a web app in a window.
Is the database schema open source?
Yes. The full migrator lives in Desktop/Sources/AppDatabase.swift, and the repo is public at github.com/mediar-ai/fazm. If you want to audit the schema, open that one file and search for migrator.registerMigration. Five migrations so far: fazmV1 creates ai_user_profiles, indexed_files, local_kg_nodes, local_kg_edges. fazmV2 adds task_chat_messages. fazmV3 renames it to chat_messages. fazmV4 adds observer_activity. fazmV5 adds a session_id column and a chat_messages_fts FTS5 virtual table.
What happens if the database gets corrupted?
On every open, Fazm runs PRAGMA quick_check. If the result is anything other than ok, the file is moved to ~/Library/Application Support/Fazm/backups/omi_corrupted_{timestamp}.db, and the app tries to recover rows using the SQLite .recover command-line tool. If recovery succeeds, those rows are re-imported into a fresh db. If it fails, the app starts with a clean schema. No spinner, no modal, no call home. Source: Desktop/Sources/AppDatabase.swift lines 680 through 745.
Does Fazm need screen recording permission?
No. Screen recording is only needed if you enable the optional screenshot cache for a specific workflow. The default context-extraction path uses the macOS Accessibility tree via AXUIElementCopyAttributeValue, which reads window titles, focused fields, and button roles without capturing pixels. Concretely: AppState.swift calls AXUIElementCreateApplication(pid) on the frontmost process, then copies kAXFocusedWindowAttribute. No Vision framework import, no OCR.
Can I just delete the local database to reset Fazm?
Yes. Quit the app, delete ~/Library/Application Support/Fazm/users/{firebaseUid}/, relaunch. Fazm will recreate the directory and run all five migrations fresh. Your chat history, file index, and knowledge graph are gone. Anything that had backendSynced = true at the time of last sync will re-download on next login; anything that was local-only is gone for good. That is the correct privacy trade: local-only means local-only.
The schema is the contract
0 tables. 0 migrations. 0 file per user. Download Fazm and audit it from the inside.
Try Fazm free
Comments (••)
Leave a comment to see what others are saying.Public and anonymous. No signup.