On this page7 min left
At a glance
Local-first means the editable source of truth remains useful without a network; remote storage adds synchronization and durability.
Autosave and version history solve different problems: recovery snapshots protect drafts, while explicit commits create meaningful durable versions.
Safe synchronization must expect divergence and preserve both versions instead of silently choosing a winner.
01The product contract
A sharp tool for one interrupted workflow
There is a particular kind of friction that appears when you are deep in a terminal and need to keep a JSON payload, command sequence, or debugging note. Opening a browser or heavyweight notes application breaks the flow. Sending proprietary text to an arbitrary paste service creates a different problem: you have lost control of where the data lives.
The useful constraints are more important than the implementation language: the tool must open instantly, work offline, recover unsaved edits, keep human-readable files, synchronize across machines, and avoid owning a new server.
| Decision | Local-firstLocal files + Git | Remote-firstGitHub Gist or API | Product platformCustom sync service |
|---|---|---|---|
| Best for | Personal snippets that must remain editable offline and portable. | Small remote snippets when offline editing is not the primary requirement. | Teams, sharing policies, search, collaboration, or non-Git clients. |
| You own | File layout, autosave, Git workflow, and conflict experience. | Local cache, API retries, revision mapping, and rate-limit behavior. | API, database, identity, migrations, uptime, backups, and sync protocol. |
| Trade-off | Excellent transparency; synchronization semantics must be designed carefully. | Less Git plumbing, but the network service becomes part of every important path. | Maximum control with the largest operational surface. |
| Examples | Private repository, normal commits, local recovery directory. | Gist API, provider-hosted revision history. | Dedicated service with records, accounts, and device synchronization. |
Local-first
Local files + Git
- Best for
- Personal snippets that must remain editable offline and portable.
- You own
- File layout, autosave, Git workflow, and conflict experience.
- Trade-off
- Excellent transparency; synchronization semantics must be designed carefully.
- Examples
- Private repository, normal commits, local recovery directory.
Remote-first
GitHub Gist or API
- Best for
- Small remote snippets when offline editing is not the primary requirement.
- You own
- Local cache, API retries, revision mapping, and rate-limit behavior.
- Trade-off
- Less Git plumbing, but the network service becomes part of every important path.
- Examples
- Gist API, provider-hosted revision history.
Product platform
Custom sync service
- Best for
- Teams, sharing policies, search, collaboration, or non-Git clients.
- You own
- API, database, identity, migrations, uptime, backups, and sync protocol.
- Trade-off
- Maximum control with the largest operational surface.
- Examples
- Dedicated service with records, accounts, and device synchronization.
02Ownership
Local-first is a behavior, not a cache
A cache assumes the authoritative copy lives elsewhere. A local-first tool treats the local file as immediately useful and editable, then synchronizes when a remote is available. The local-first software paper describes broader ideals around offline work, ownership, longevity, and collaboration. A personal CLI needs only a focused subset, but the distinction still matters.
- Offline is normal. Creating, opening, editing, listing, and searching local snippets should not require GitHub.
- Files stay legible. A user can inspect, copy, back up, or version the data without the application.
- Sync is explicit. The tool clearly reports whether work is local-only, synchronized, diverged, or conflicted.
- Remote failure is contained. Authentication or network problems do not destroy the current draft.
Loading diagram…
- Open or create a snippet in the terminal editor.
- Debounced edits produce an atomic local recovery snapshot.
- The journal records dirty state and the recovery generation.
- An explicit save updates the working file and creates a meaningful Git commit when content changed.
- An explicit sync fetches and compares local and remote history.
- Unambiguous histories fast-forward or push.
- Diverged histories preserve a conflict copy before reconciliation.
- A resolved history is pushed to the private GitHub repository.
03Durability
Use different layers for different promises
One file cannot simultaneously represent the editor buffer, crash recovery, a user-approved version, and a synchronized remote state. Giving those promises separate layers makes failure behavior much easier to explain.
Loading diagram…
- The editor buffer contains the current in-memory text.
- A local recovery snapshot protects recent edits from a process crash.
- The working file contains content the user explicitly accepted.
- A Git commit creates a named durable version and history.
- The private remote repository provides an authenticated off-device copy.
- The journal records how these layers relate and which work remains pending.
- Working file: the user-visible content that opens immediately and can remain local.
- Recovery snapshot: a short-lived, crash-safe copy written frequently while editing.
- Journal: compact metadata describing dirty files, recovery generations, last known remote revision, and pending operations.
- Git commit: an intentional durable version with a stable identity and history.
- Remote repository: an authenticated synchronization target and off-device copy.
Use the operating system’s application-data or configuration directory rather than inventing a path under the current working directory. Go exposes this boundary through os.UserConfigDir, with platform-specific behavior.
04Crash safety
Autosave without manufacturing history
Autosave should protect the draft without creating a remote commit for every keystroke. A practical loop debounces edits, writes a temporary file in the same filesystem, flushes it when the platform supports that guarantee, and atomically renames it over the recovery snapshot.
func SaveRecovery(path string, body []byte) error {
tmp := path + ".tmp"
file, err := os.OpenFile(tmp, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o600)
if err != nil {
return err
}
if _, err = file.Write(body); err != nil {
file.Close()
return err
}
if err = file.Sync(); err != nil {
file.Close()
return err
}
if err = file.Close(); err != nil {
return err
}
return os.Rename(tmp, path)
}05Remote durability
Make save and sync separate decisions
A local save confirms the contents the user wants to keep. A sync reconciles that version with the remote repository. Keeping those commands separate avoids coupling everyday editing to connectivity and makes it possible to describe failures precisely.
- Create a stable local version. Write the working file, clear its accepted recovery snapshot, and create a commit only when content changed.
- Fetch before integrating. Learn whether the remote advanced since the last synchronized revision.
- Fast-forward when possible. If only one side changed, apply the unambiguous update.
- Merge only from a clean baseline. Do not start reconciliation with unrelated uncommitted changes.
- Push only a resolved history. Never hide a conflict behind a successful-looking sync message.
Git’s merge documentation explains the states created by a non-trivial merge. A CLI can wrap those states, but should not pretend they do not exist.
06Divergence
Preserve both versions before asking who wins
Two machines can edit the same snippet before either synchronizes. Automatic line merging is useful when changes do not overlap, but a personal text tool should prefer recoverability over cleverness when intent is ambiguous.
Loading diagram…
- Require a clean managed worktree before synchronization.
- Fetch remote history without modifying the working file.
- If neither side changed, report synchronized.
- If only local changed, push the local commit.
- If only remote changed, fast-forward the local branch.
- If both changed, create a conflict copy before attempting reconciliation.
- If the merge is clean, commit and push the resolved history.
- If intent remains ambiguous, stop and report the conflict paths and next action.
Suggested conflict-copy identity
Use a stable path derived from the snippet name plus device and timestamp, for example notes/today.conflict-laptop-20260729T103000Z.txt. Record the relationship in the journal so listing and cleanup commands can explain why the file exists.
07Trust boundary
Reuse authentication, but define the privacy limit
Delegating authentication to GitHub CLI avoids inventing token storage. The GitHub CLI authentication flow normally stores credentials in the system credential store and documents its fallback behavior. The Pastebin CLI should call gh auth status and fail with a specific setup instruction rather than copying tokens into its own configuration.
- Use restrictive local permissions. Create data, journal, and recovery files for the current user only.
- Avoid secrets in process arguments. Arguments can appear in shell history or process listings.
- Redact diagnostic output. Logs should identify paths and operations without printing snippet contents or credentials.
- Make repository visibility explicit. Verify the created remote is private and stop if its visibility changes unexpectedly.
08Distribution
Install without taking over the machine
A single Go binary is a strong fit for a personal CLI, but distribution still needs integrity, ownership, and rollback. Install into a user-owned binary directory, verify release checksums, and keep upgrades explicit or policy-driven.
set -euo pipefail
install_dir="${XDG_BIN_HOME:-$HOME/.local/bin}"
mkdir -p "$install_dir"
# Download the release and its published checksum to a temporary directory.
# Verify before replacing the currently installed binary.
sha256sum --check pb_SHA256SUMS
install -m 0755 pb "$install_dir/pb"- Do not require administrator access. The tool owns only its user-level binary and application directories.
- Do not replace a working binary before verification. Stage and verify the new release, then rename it into place.
- Rate-limit update checks. Cache the last check and let users choose automatic, prompted, or manual upgrades.
09Implementation sketch
A sync contract that refuses data loss
The core sync function should return structured states rather than only printing Git output. That keeps the command interface honest about whether it synchronized, stayed local, or produced a conflict.
func Sync(ctx context.Context, entry JournalEntry) (SyncResult, error) {
if err := repo.RequireCleanWorktree(ctx); err != nil {
return SyncResult{}, err
}
remote, err := repo.Fetch(ctx)
if err != nil {
return SyncResult{State: LocalOnly}, err
}
relation := repo.Compare(entry.LastSyncedCommit, remote.Head)
switch relation {
case Unchanged:
return SyncResult{State: Synchronized}, nil
case LocalAhead:
return repo.Push(ctx)
case RemoteAhead:
return repo.FastForward(ctx, remote.Head)
case Diverged:
conflictPath, err := repo.PreserveConflictCopy(entry.Path)
if err != nil {
return SyncResult{}, err
}
return repo.MergeWithConflictPath(ctx, remote.Head, conflictPath)
default:
return SyncResult{}, ErrUnknownHistory
}
}10Before release
A local-first CLI checklist
- Test offline first. Every local command works when DNS, GitHub, or authentication is unavailable.
- Interrupt every write. Kill the editor and process during snapshot, journal, commit, fetch, merge, and upgrade operations.
- Simulate divergence. Edit the same file on two machines and verify that neither version disappears.
- Protect path boundaries. Reject traversal, symlink escape, reserved names, and writes outside the managed root.
- Verify permissions and redaction. Inspect created files, subprocess arguments, and diagnostic logs.
- Make every state visible. Users can distinguish dirty, recovered, local-only, synchronized, and conflicted snippets.
11Working vocabulary
Glossary
- Local-first
- Software whose primary working copy remains useful and editable on the local device without depending on a network request.
- Recovery snapshot
- A short-lived crash-safe copy used to restore edits that were not yet accepted as a durable version.
- Journal
- Structured local metadata recording dirty state, recovery generations, pending work, and synchronization identity.
- Atomic rename
- Replacing a file through a filesystem rename so readers observe either the previous complete version or the new complete version.
- Fast-forward
- Advancing a Git branch to a descendant commit without creating a merge commit.
- Divergence
- A history state where local and remote sides both contain commits not present on the other side.
- Conflict copy
- A preserved alternate version created before reconciliation so no ambiguous edit is silently discarded.
12Sources and further reading
References
- Building a GitHub Backed Pastebin CLI in Go
Abhishek Choudhury
Original project article, published April 2026. - Local-First Software: You Own Your Data, in Spite of the Cloud
Martin Kleppmann, Adam Wiggins, Peter van Hardenberg, and Mark McGranaghan
The principles and trade-offs behind local-first software. - Package os: UserConfigDir
The Go Authors
Platform-aware application configuration directories. - git-merge documentation
The Git Project
Merge states, conflicts, abort behavior, and safe preconditions. - GitHub CLI: gh auth login
GitHub
Authentication modes and credential-storage behavior.


