$ DEEPAK SILAYCH / WRITING

GitHub as a Database

Abstract

Using a private repository and the GitHub Contents API as file storage for a small personal application.

Contents
  1. Application and storage model
  2. Read and write boundary
  3. What the repository makes easier
  4. Limits and sensitive records
  5. Conclusion

A personal application often needs less infrastructure than its first design suggests. When I built a private data vault, I needed a place to keep a small collection of records, inspect changes, and recover earlier versions. I chose a private GitHub repository containing JSON files, accessed through the Contents API. This gave the application persistence without another database service to operate, although it also tied its write path to the limitations of a version-control platform. The useful question was whether those limitations were acceptable for a tool used by one person.

Application and storage model

The vault is a Next.js application behind my own authentication layer. Its records are separated into files such as contacts.json, notes.json, and passwords.json, with server-side API routes handling access to the repository. A read retrieves the relevant file, decodes its contents, and parses the JSON before returning the records to the interface. A write replaces the file with an updated version. This is file storage with version history; there are no indexes, joins, or transactions spanning the collection.

Keeping the representation as plain JSON made the data easy to inspect outside the application. I could open a file in an editor, compare revisions, or transform the records with a short script. That also left a straightforward migration path: the application could eventually move to SQLite or Postgres without first extracting data from an unfamiliar format. The repository provided a remote copy and a history of changes, although I would distinguish that convenience from an independent backup and recovery strategy.

Read and write boundary

The application routes storage operations through a small connector instead of calling GitHub from individual interface components. The connector owns repository paths, response decoding, authentication, and update requests. Screens ask it to read or save records without depending on GitHub’s response format. This separation matters even in a small project because the unusual storage choice should be replaceable without changing every form and page.

Updating an existing file requires its current SHA. The application reads the file metadata and submits the replacement content with that SHA, identifying the version it intends to replace. A competing update can make the version stale, requiring the application to fetch the current state and resolve the conflict before trying again. This guards against some accidental overwrites, but it does not provide the transaction semantics of a database. A single-user application can still produce overlapping writes from multiple tabs or background operations, so the update path must account for conflicts rather than assuming they cannot happen.

What the repository makes easier

The main benefit was the visibility of changes. If a note disappeared or a write produced an unexpected result, I could examine the diff and compare it with the preceding revision. The same tools I used to understand code changes were available for the application’s data. For a small collection of manually edited records, this was more useful than a sophisticated query interface, and it reduced the amount of infrastructure I needed to maintain.

There was also a practical benefit to having fewer moving parts. The application still needed authentication, error handling, and a carefully scoped server-side credential, but it did not need a separate database process and its associated configuration. That tradeoff suited a personal tool with infrequent writes. It would become less attractive if most interactions required fetching and rewriting a large file or if the application grew dependent on low-latency updates.

Limits and sensitive records

A private repository is not sufficient protection for secrets. Password records require particular care because Git retains earlier versions: changing or deleting a value in the latest JSON file does not remove it from history. Repository access, token exposure, and encryption therefore matter independently of the application’s login screen. The presence of a password file describes the vault’s data model; it should not be read as a recommendation to store plaintext credentials in Git.

The approach also has clear functional limits. Frequent concurrent writes create conflicts, full-file updates become inefficient as records grow, and queries require application-side processing rather than indexed access. Relational data, strict transactions, and a large shared application would push me toward a database. Small configuration collections, portfolio content, or personal notes are closer to the shape of the storage model, provided the application can tolerate the API’s latency and availability constraints.

Conclusion

For this vault, a repository was a manageable storage choice because the data was small, the workload was personal, and revision history was useful. The experiment did not eliminate the responsibilities of a data layer; it exchanged database operations for file updates, API dependencies, and conflict handling. Keeping those responsibilities behind a connector made the choice easier to understand and left room to replace it when the application’s requirements changed.

Related essays

Self-hosting Apps and AI Agents on a LaptopChallenges of Building Conversational AI Writing index