Technical writing

Local-first architecture for developer tools

A local-first developer tool creates its primary artifact on the user's machine in a documented, portable format and keeps core inspection usable without a hosted account. Separate immutable artifacts, rebuildable indexes, and disposable caches, then make CI or cloud synchronization an adapter rather than the authority. This boundary limits what a network outage, service shutdown, or corrupted index can take away. Sources: Local-first software research, AgentReceipt repository.

Local-first systems

A systems design for CLIs that keep source, prompts, credentials, and evidence on the developer's machine while still supporting reproducible automation and optional remote coordination.

Validation scope
Storage boundaries and command behavior were checked against the linked project versions; the Go atomic-write example is illustrative and excludes platform-specific durability guarantees.

This guide answers

  • What does local-first mean for a developer tool?
  • How should a CLI store durable local state safely?
  • How can a local-first tool integrate with CI or hosted services without becoming cloud-dependent?

When to use this / when not to use this

Use this when

  • The tool handles source, prompts, credentials, build evidence, or other data that should remain locally owned.
  • Core capture, inspection, verification, or export must survive offline use and hosted-service failure.

Do not use this when

  • A central service is the legitimate authority for shared state, access control, or real-time collaboration.
  • The product cannot define conflict resolution, migration, backup, and deletion semantics for local data.

Decide what the local machine is authoritative for

A tool is not local-first merely because it has a CLI. If every useful command uploads source, requires a hosted account, or stores the only readable history remotely, the terminal is a thin client. A local-first developer tool creates its primary artifact on the user's machine in a documented format and keeps core inspection available offline. Sources: Ink & Switch local-first software research.

Write down the authority split. The local machine can own session evidence, configuration, indexes, and private keys; a remote service can own organization policy, team discovery, or publication. When the two disagree, the product should have an explicit reconciliation rule instead of whichever response arrived last winning silently.

A practical local-first promise

  • Core capture, inspection, verification, and export work without network access.
  • Artifacts live in a documented path and portable, versioned format.
  • Uninstalling the binary does not make existing artifacts unreadable.
  • Remote publication is optional and visibly names what will leave the machine.
  • The user can delete local data without an online control plane.

Separate immutable artifacts, mutable indexes, and caches

Durable evidence and rebuildable convenience data should not share the same failure mode. Store completed receipts, reports, or scan results as immutable artifacts. Keep lookup indexes and last-used pointers in a small transactional database or atomic metadata files. Put downloaded toolchains and derived renderings in a cache that can be deleted at any time. Sources: SQLite transactional guarantees, XDG Base Directory Specification.

Content-addressed artifact names reduce accidental duplication and make integrity checks cheap. They do not replace lifecycle metadata: keep schema version, media type, size, creation context, and retention class beside each digest. For mutable state, write to a temporary file, fsync when durability matters, and rename atomically. Sources: OCI Content Descriptor specification.

State layout

  • Artifacts: immutable receipts, patches, reports, exports, and signatures.
  • Index: session lookup, labels, current schema, and artifact references.
  • Config: explicit user preferences with source and precedence.
  • Secrets: operating-system keychain or restricted files, never general config JSON.
  • Cache: downloads and derived data that a repair command can recreate.
System diagramLocal authority with optional remote coordination

Core commands read and write local portable artifacts; adapters publish copies or policy results without owning the original record.

Source: AgentReceipt repository
  1. 01WorkspaceSource + instructions + tool state
  2. 02Local coreCapture, scan, verify, migrate
  3. 03ArtifactsImmutable portable bundles
  4. 04IndexMutable, rebuildable lookup
  5. 05AdaptersCI, PR, dashboard, policy sync

Design for two terminals and a killed process

Developer tools are invoked by humans, editors, hooks, and agents at the same time. Use a per-repository or per-session lock with owner metadata and a bounded stale-lock recovery rule. Avoid a single global lock that turns unrelated repositories into one failure domain.

Assume the process can stop between every pair of writes. Build multi-step operations around a staging directory or journal, then publish one final pointer atomically. On startup, distinguish incomplete staging data from complete artifacts; offer a repair or discard action instead of treating partial files as valid history. Sources: SQLite atomic commit and crash recovery.

Crash tests

  • Kill the process after artifact write but before index update.
  • Run two sessions in the same repository and in two different repositories.
  • Fill the disk during finalization and confirm the previous index still opens.
  • Interrupt a schema migration and rerun it.
  • Leave a lock behind, then test stale-owner detection without deleting a live lock.

Add remote adapters after the local contract is stable

CI, pull requests, team dashboards, and hosted policy are valuable, but each should consume the same export that a local verifier can read. The adapter uploads an explicit bundle, records the remote object identifier locally, and never mutates the signed artifact to add synchronization metadata.

Queue outbound work when offline and use idempotency keys derived from the artifact identity. Authentication failure should block publication, not local capture or inspection. This keeps service outages from turning a development tool into an availability dependency while still supporting organizational workflows. Sources: Stripe idempotent request documentation.

Remote boundary checks

  • Preview the files, fields, and redactions that will be uploaded.
  • Use separate scopes for read, publish, and policy administration.
  • Persist idempotency keys and remote identifiers outside signed artifacts.
  • Retry transport failures without recollecting mutable local inputs.
  • Keep local verification available when the remote API is down.

A local-first artifact architecture

This diagram separates authoritative bytes from rebuildable views and optional coordination. The boundary is drawn from the storage patterns shared by AgentReceipt, Skills Doctor, and RitualAI.

01

Authoritative

Survives restart and remains inspectable without a service

  • Atomic artifact files
  • Canonical manifests
  • Signatures + digests
02

Derived

Can be deleted and rebuilt from authoritative artifacts

  • SQLite indexes
  • Search projections
  • Rendered reports
03

Coordination

Improves sharing without becoming the local write authority

  • Sync cursor
  • Remote object store
  • Publication API
  1. Write temp file → fsync → atomic rename → publish manifest reference
  2. Read artifact → validate digest → update rebuildable index
  3. Queue remote sync after local commit; retain the local result when sync fails

Choosing the authority boundary

This comparison records the tradeoffs I encountered building file-backed developer tools. The choice is not file versus database in the abstract; it is which representation a user can recover and inspect when every helper process is gone.

Engineering comparisonDerived from RitualAI repository
Choosing the authority boundary
CriterionArtifact filesEmbedded databaseRemote service
Offline write pathCompleteCompleteUnavailable without a queue
Human inspectionDirect with ordinary toolsRequires query toolingRequires API access
Query flexibilityLow until indexedHigh locallyHigh but network-bound
Recovery modelCopy and verify bytesRestore or repair databaseProvider-specific export
Role in this architectureAuthorityRebuildable projectionOptional coordination

Implementation examples

Concrete commands and data shapes you can adapt.

Publish a file atomically in Gogo
func atomicWrite(path string, data []byte, mode fs.FileMode) error {
	dir := filepath.Dir(path)
	tmp, err := os.CreateTemp(dir, ".pending-*")
	if err != nil { return err }
	defer os.Remove(tmp.Name())
	if err := tmp.Chmod(mode); err != nil { return err }
	if _, err := tmp.Write(data); err != nil { return err }
	if err := tmp.Sync(); err != nil { return err }
	if err := tmp.Close(); err != nil { return err }
	return os.Rename(tmp.Name(), path)
}
Keep durable data and disposable cache visibly separatetext
.tool/
├── artifacts/sha256/     # immutable, portable
├── index.sqlite          # mutable, rebuildable
├── config.json           # user-owned preferences
├── staging/              # incomplete operations
└── cache/                # safe to delete

Decision log

The choices that shape the design—and what each choice costs.

  1. Make artifacts the integration boundary

    Local commands, CI, and hosted adapters can share a stable format without sharing process internals.

    Tradeoff: The artifact schema becomes a public API that needs versioning and compatibility tests.
  2. Treat indexes as rebuildable

    A corrupted lookup database should not destroy the evidence or source material it points to.

    Tradeoff: Rebuild commands need enough metadata in each artifact and may take longer on large histories.
  3. Keep synchronization metadata outside signed bundles

    Publishing to a second destination does not change the identity or integrity of the original artifact.

    Tradeoff: Users must back up both artifacts and optional local labels or remote mappings if they need the full convenience state.

Failure cases

What breaks, how it presents, and the recovery boundary.

A crash exposes a half-written artifact
Signal

The index points to a file whose digest or footer is incomplete.

Response

Write in staging, verify the digest, atomically rename, and update the index only after publication succeeds.

Two processes finalize the same session
Signal

Competing writers produce different manifests or overwrite the current pointer.

Response

Use a session-scoped lock and immutable result names; reject the second finalization unless it is byte-identical.

Remote auth failure blocks local work
Signal

Capture or inspection exits because a token is expired or the service is unavailable.

Response

Queue publication separately and keep local commands independent of remote authentication.

Repositories and primary references

Read the implementation, specifications, and tool documentation behind the article.

Inspect the systems that ground this guide in implementation work.

Last updated: