opensheet
<div align="center">
# OpenSheet
### A database UI without the database.
Your tables are plain `.jsonl` files on disk. Edit one cell — `git diff` shows one line.
Close OpenSheet, and `jq`, DuckDB, Excel, pandas and your AI agents can still read everything.
[](LICENSE)
[](#why-it-stays-small)
[](#verification)
[](#no-network-by-construction)
[中文文档](README.zh-CN.md)
<img src="docs/img/hero-light.png#gh-light-mode-only" alt="OpenSheet grid view" width="880">
<img src="docs/img/hero-dark.png#gh-dark-mode-only" alt="OpenSheet grid view" width="880">
</div>
---
## What is this
Spreadsheet-database tools have to put your rows somewhere. The usual answers are a database you
also have to run, a storage engine that ships with the app, or a single binary file in the app's own
format. All three work. All three mean the tool sits between you and your data.
OpenSheet's source of truth is **a text file you already know how to read**. One folder is a library,
each `.jsonl` inside it is a table, one line is a record. There is no database, no account, no server,
no sync. Delete OpenSheet and your data doesn't move an inch.
```bash
$ cat my-library/campaigns.jsonl
{"id":"KS-001","project":"Modular Mechanical Keyboard","raised_usd":412870,"rating":5}
{"id":"KS-002","project":"Solar Camp Lantern","raised_usd":88250,"rating":4}
$ jq -r 'select(.rating == 5) | .project' my-library/campaigns.jsonl
Modular Mechanical Keyboard
```
That's the whole storage format. The spreadsheet UI is a view onto it, not a wrapper around it.
## Quick start
Requires Python 3.9+. No pip install, no Docker, no build step.
```bash
git clone https://github.com/newgepard/opensheet
cd opensheet
PYTHONPATH=src python3 -m opensheet ~/my-library/
```
It opens `http://127.0.0.1:<free port>` in your browser. Point it at a folder of `.jsonl` files, at a
single `.jsonl` file, or at an empty folder to start from scratch.
**macOS desktop app** — an 860 KB native shell (Swift + WKWebView, uses the system WebKit):
```bash
bash shell/build.sh
open shell/build/OpenSheet.app
```
## The fidelity promise
This is what everything else is built around, and it is tested rather than asserted.
**1. Untouched rows are written back byte-for-byte.** Edit one cell in a 10,000-row table and
`git diff` shows exactly one changed line. Rows you didn't touch are never re-serialized — not
reordered, not reformatted, not re-escaped.
**2. Non-ASCII text is never escaped.** `{"名前":"甲"}` stays exactly that. It never turns into
`{"\u540d\u524d":"\u7532"}`, which is what `json.dumps` does by default and what makes a file
unreadable in every tool that isn't a JSON parser.
**3. Sparse files stay sparse.** If a row doesn't have a key, OpenSheet will not invent one for it.
Adding a field to a table with 50,000 existing rows rewrites zero of them.
Undo is built as **inverse commands, not snapshot rollback**, specifically so promise 1 survives it:
edit a cell, press `⌘Z`, save — `git diff` is empty. A snapshot-restore implementation would look
correct on screen while quietly re-serializing every row it touched.

Backing this up: a fingerprint check refuses to overwrite changes made behind OpenSheet's back,
writes go through a temp file plus `os.replace()`, and every save keeps a `.bak` alongside an
open-time snapshot.
## Features
- **Your data is a file you own** — plain `.jsonl`, readable by `jq`, DuckDB, pandas, Excel and git
- **Undo everything** — `⌘Z` / `⇧⌘Z`; a batch delete collapses into a single undo step
- **13 field types** — text, number, select, multi-select, date, checkbox, URL, attachment, formula,
cross-table lookup, created/updated time, auto-number. Numbers render as progress bars, currency,
ratings or percentages **without changing the stored value**
- **4 view types** — grid, kanban, gallery, plus per-view filters, sorts, grouping, statistics,
row height, frozen columns and column order
- **10,000 rows stay smooth** — virtual scrolling; measured at 57 ms to load and 59 ms to save a
7.5 MB table
- **Import and export** — in from CSV, XLSX, JSON, SQLite and `.grist`; out to CSV, XLSX, JSON,
JSONL, Markdown and Parquet
- **Attachments stay local** — files land in `<library>/assets/`, the cell holds a relative path,
nothing is uploaded anywhere
## Built for agents, not decorated with AI
Adding an AI button to a spreadsheet is a feature. Choosing a storage format an agent can read and
write without an adapter is an architecture. OpenSheet did the second one, and everything else
follows from it: the file is line-oriented so appends are `>>`, the format is text so `grep` works,
and the writer is single so byte fidelity survives concurrent access.
An MCP server runs in the same process as the HTTP server, sharing one `Store` instance:
```json
{
"mcpServers": {
"opensheet": {
"command": "python3",
"args": ["-m", "opensheet", "/path/to/libraries", "--mcp", "--mcp-root", "/path/to/libraries"],
"env": { "PYTHONPATH": "/path/to/opensheet/src" }
}
}
}
```
Tools: `list_libraries`, `list_tables`, `get_schema`, `query`, `upsert_rows`.
`upsert_rows` returns a receipt — `inserted` / `updated` / `unchanged` counts plus the primary keys
for each — and supports `dry_run`. Rows identical to what's already on disk count as `unchanged`,
and the file is not touched.
**Same process is not an implementation detail.** Byte fidelity depends on in-process state: which
bytes each row arrived as. Two processes each holding their own copy would each conclude the other's
writes were "changes" and re-serialize the whole file on the next save. A second process detects the
lock file and forwards to the first, so there is always exactly one writer.
Agents can also skip OpenSheet and append to the file directly. The rules that keep fidelity intact
when they do are documented in [the direct-write guide](docs/agent-direct-write.md).
## Architecture
```mermaid
graph TD
A["my-library/"] --> B["campaigns.jsonl<br/>source of truth"]
A --> C["suppliers.jsonl"]
A --> D[".opensheet/<br/>schema · views · links"]
B --> E["store.py<br/>row passthrough · atomic write"]
C --> E
E --> F["single process"]
F --> G["HTTP + browser UI"]
F --> H["MCP over stdio"]
G --> I["native macOS shell"]
```
`.opensheet/` holds field types, column widths, view definitions and cross-table links. Delete it and
you lose formatting, not data — field types fall back to inference.
## Why it stays small
The backend is Python standard library only. The frontend is plain JavaScript — no framework, no
build step. The macOS shell is 184 KB of Swift on the system WebKit, not a bundled browser.
DuckDB is optional and **read-only**, serving the SQL panel and Parquet export. It never participates
in filtering, sorting or grouping, and it never writes. Every write goes through `store.py`.
## No network, by construction
No telemetry, no update checks, no CDN assets, no online demo. The browser only requests same-origin
`/api/*` paths, the server binds to the IPv4 loopback only, and the desktop shell holds zero TCP
connections of its own. `tests/test_offline.py` enforces all of this.
## Verification
```bash
bash tests/opensheet-全量验收-20260815.sh
```
329 tests: 188 JavaScript unit, 83 Python, 42 browser end-to-end (Playwright), 8 fidelity round-trip,
8 library round-trip.
The fidelity tests compare files byte-for-byte rather than field-by-field — a test that only checked
values would pass while promise 1 was silently broken.
Browser tests need `npm install` inside `web/` for Playwright's Chromium.
## What OpenSheet will not do
Comments, permissions, real-time collaboration, automations, a hosted version, an online demo.
Each of them needs an account system or a public address, which contradicts the first design
constraint: *data never leaves your machine*.
This is a deliberate boundary, not a roadmap gap.
## Status
Working and used daily, with two rough edges worth knowing before you try it:
- **The UI is currently Chinese-only.** The code, comments and interface strings are all in Chinese.
English localisation hasn't been done yet.
- **macOS only for the desktop shell.** The web version runs anywhere Python does.
## Contributing
Issues and PRs welcome. Two things to know before touching the storage layer:
1. The row-passthrough logic in `store.py` *is* the product. Changing it means keeping the fidelity
tests green, and those compare bytes, not fields.
2. Code comments are currently in Chinese. Translation help is very welcome.
## License
MIT — see [LICENSE](LICENSE).
TDQS
Scored across 5 tools
Each tool maps to a distinct action and resource level: libraries, tables, schema, query, and upsert. There is no meaningful overlap, and the descriptions reinforce the boundaries clearly.
Most tool names follow a clear verb_noun snake_case pattern: list_libraries, list_tables, get_schema, upsert_rows. The single-word 'query' is a minor deviation, but the overall naming style remains uniform and predictable.
Five tools is well-scoped for an OpenSheet data access server. Each tool covers a necessary step in the workflow without redundancy or bloat.
The set covers the core workflow well: discovering libraries, listing tables, inspecting schema, querying data, and upserting rows. The main gap is the lack of any delete or schema-mutation capability, but that may be intentionally out of scope for this server.