A Rust tool that snapshots a Google Sheet into a git repository and optionally serves it as a web page. Each sync commit records the CSV content, a SHA-256 hash, and a nanosecond timestamp. Snapshots can also be announced on Nostr (plain text note + NIP-34 repository announcement).
A live deployment syncs every minute via GitHub Actions and is published to GitHub Pages.
- Quick start
- CLI interface
- HTTP / web interface
- Git snapshot format
- Nostr integration
- GitHub Actions workflows
- Building
# Serve the default sheet on a random local port
cargo run
# Sync (snapshot) the default sheet once, then exit
cargo run -- sync
# Serve a custom sheet on a fixed address
cargo run -- --sheet-url "https://..." --bind-addr 127.0.0.1:8080 serve
# Sync and publish a Nostr note
cargo run -- --privkey nsec1... syncstartsmall [OPTIONS] [MODE]
| Mode | Description |
|---|---|
serve (default) |
Start the HTTP server and keep it running. On every GET / request the sheet is fetched live and rendered as HTML. POST /sync takes a snapshot and commits it to git. |
sync |
Fetch the sheet once, commit a snapshot to git, optionally publish to Nostr, then exit. Suitable for cron jobs and CI. |
| Flag | Value | Default | Description |
|---|---|---|---|
--sheet-url <URL> |
Google Sheets CSV export URL | SHEET_CSV_URL env var, or the bundled StartSmall sheet |
The URL that returns the sheet as CSV. Use the export as CSV link from Google Sheets (…/export?format=csv&gid=0). |
--bind-addr <ADDR> |
host:port |
BIND_ADDR env var, or 127.0.0.1:0 (random port) |
Address the HTTP server listens on. Only used in serve mode. |
--privkey <NSEC_OR_HEX> |
Nostr private key | (none – Nostr disabled) | Private key used to sign and publish Nostr events on sync. Accepts nsec1… bech32 or 64-character hex. |
--version |
Print version and exit. | ||
--help |
Print help and exit. |
All flags can alternatively be configured through environment variables. CLI flags take precedence.
| Variable | Equivalent flag | Description |
|---|---|---|
SHEET_CSV_URL |
--sheet-url |
Google Sheets CSV export URL. |
BIND_ADDR |
--bind-addr |
Listen address for the HTTP server. |
SYNC_TOKEN |
(no flag equivalent) | Secret token required to authorize POST /sync from non-loopback addresses. See Authorization. |
NOSTR_RELAYS |
(no flag equivalent) | Comma-separated list of Nostr relay WebSocket URLs. Defaults to wss://relay.damus.io, wss://relay.nostr.band, wss://nostr.wine. |
Start the server with cargo run (or cargo run -- serve) and open the printed URL in a browser.
Serving spreadsheet viewer at http://127.0.0.1:PORT
Fetches the Google Sheet CSV live, parses it, and returns an HTML page.
Response 200 OK – HTML table with:
- A metadata section for any leading rows that appear before the column-header row.
- A header row detected automatically (a row whose cells include keywords such as
date,amount,grantee,twitter,link, etc.). - Data rows rendered below the header. Each row is numbered.
- Cells that look like URLs, domains, or Twitter/X handles are rendered as clickable links.
- A Sync snapshot to git button that posts to
/sync.
Response 200 OK (error page) – If the sheet cannot be fetched, a plain error message is rendered instead of a 5xx, so the browser always receives a readable page.
Fetches the sheet, writes sheet-snapshots/google-sheet.csv and sheet-snapshots/google-sheet.meta, stages both files, and creates a git commit in the current repository. If a Nostr private key is configured, a text note and a NIP-34 repository announcement are published.
Response 200 OK – HTML confirmation page showing the commit hash, SHA-256, row count, and optional Nostr event IDs.
Response 403 Forbidden – The request was not authorized. See Authorization.
Response 503 Service Unavailable – The server is already handling the maximum number of concurrent requests (16).
POST /sync is automatically authorized when the request originates from a loopback address (127.0.0.1 / ::1). This covers the browser button when the server is running locally.
For remote clients (e.g., a GitHub Actions webhook), set the SYNC_TOKEN environment variable to a secret string. The client must then supply the token in one of two ways:
X-Sync-Token: <token>Authorization: Bearer <token>Token comparison is performed with a constant-time algorithm (via the subtle crate) to avoid timing-based attacks.
If SYNC_TOKEN is not set, remote POST /sync requests are always rejected with 403 Forbidden.
Each sync writes two files and creates a commit:
The raw CSV body exactly as returned by the Google Sheets export URL.
A newline-delimited key=value file:
source_url=<csv-export-url>
sha256=<hex-encoded SHA-256 of the CSV body>
rows=<number of CSV rows>
synced_at_unix_ns=<Unix timestamp in nanoseconds>
sync: snapshot Google Sheet document
sha256: <hex>
rows: <count>
source: <url>
file: sheet-snapshots/google-sheet.csv
meta: sheet-snapshots/google-sheet.meta
synced_at_unix_ns: <nanoseconds>
The commit author/committer is read from the local git config. If no user is configured, the fallback identity Start Small Bot <start-small-bot@example.com> is used.
Passing --privkey (or equivalent) enables two Nostr publications on every successful sync.
A short human-readable note is broadcast:
StartSmall snapshot synced
commit: <git-commit-sha>
sha256: <csv-sha256>
rows: <count>
source: <csv-export-url>
A NIP-34 git_repository_announcement event is published with:
- id:
startsmall - name:
StartSmall - description:
Google Sheet snapshot viewer and git sync - web: read from a
CNAMEfile in the repository root (if present) - clone: derived from the
originremote URL (SSHgit@github.com:and HTTPS forms are both normalised) - relays: the configured relay list
Default relays:
wss://relay.damus.iowss://relay.nostr.bandwss://nostr.wine
Override with NOSTR_RELAYS=wss://my-relay.example.com,wss://other.example.com.
Runs on every push and pull request to main, and on a cron schedule every minute.
| Step | Command |
|---|---|
| Build | cargo build --verbose |
| Test | cargo test --verbose |
Triggered on pushes to main, every minute via cron, and manually via workflow_dispatch.
sync job (runs on schedule / workflow_dispatch only)
- Checks out the full history.
- Configures git identity (
StartSmall Bot). - Runs
cargo run -- syncwithSHEET_CSV_URLset. - Pushes the resulting snapshot commit back to the branch.
build job (runs on push to main only)
- Generates a static
_site/directory containing:index.html– plain HTML table built from the latestgoogle-sheet.csv.google-sheet.csv– the raw CSV.CNAME– the custom domain file (if present)..nojekyll– disables Jekyll processing.
- Uploads the artifact for Pages deployment.
deploy job
Deploys the uploaded artifact to GitHub Pages.
Prerequisites: Rust toolchain (stable). Install via rustup.
# Debug build
cargo build
# Release build
cargo build --release
# Run tests
cargo test
# Run with release optimisations
cargo run --release -- serveThe compiled binary is target/release/startsmall (or target/debug/startsmall for debug builds).