Spread Sync
unlistedby Yuto Takagi
Embed Google Sheets cells/ranges into Markdown with on-open sync, in-place editing, and explicit write-back.
๐ Spread Sync
Your Google Sheet, inside your Obsidian note.
Embed sheet cells inline. Render ranges as tables. Edit them in place. Push back. All with conflict-aware writes and PKCE OAuth.
๐ Docs ยท ๐ Install ยท โ๏ธ Setup ยท ๐ก Usage
โจ What it does
| ๐ข Inline cell | `spread_sync("id", "Sheet1", "B3")` โ renders as the live value |
| ๐ Range table | ```spread-sync ``` block โ Markdown table, headers/transpose/named ranges supported |
| โ๏ธ Edit in place | Add editable: true โ cells become editable, one button batchUpdates all changes |
| โ๏ธ Explicit write | ```spread-write ``` block + Push this block command |
| ๐ก๏ธ Conflict-aware | Drive modifiedTime check before every push, Overwrite/Cancel prompt on mismatch |
| ๐ PKCE OAuth | Bring your own Google Cloud Desktop client. Refresh token encrypted via safeStorage |
| โก Smart caching | LRU + debounced persist, stale-while-revalidate, manual refresh commands |
| ๐ Resilient | 429 backoff, offline-aware, dedicated error badges (no access, Excel format, etc.) |
๐ Install
cd /path/to/your/vault/.obsidian/plugins
git clone https://github.com/yut0takagi/obsidian-spread-sync.git spread-sync
cd spread-sync && npm install && npm run build
Then in Obsidian: Settings โ Community plugins โ enable Spread Sync.
โ๏ธ Setup (5 min)
Spread Sync uses your own Google Cloud OAuth credentials. You stay in control of scopes and quota.
Step 1 โ Create a Google Cloud project
Open Google Cloud Console โ create or select a project.
Step 2 โ Enable APIs
APIs & Services โ Library โ enable:
- Google Sheets API
- Google Drive API
Step 3 โ Configure OAuth consent screen
- User type: External
- Add scopes:
.../auth/spreadsheets+.../auth/drive.metadata.readonly - Publishing status: In production (avoids the 7-day refresh-token expiry of "Testing" mode)
Step 4 โ Create OAuth client
Credentials โ Create credentials โ OAuth client ID โ Application type: Desktop app. Copy both Client ID and Client secret.
Note: Google's docs explicitly say Desktop OAuth client secrets are not actually confidential (source). Storing it in your local Obsidian settings is consistent with how the official Google libraries handle it.
Step 5 โ Paste into Obsidian
Settings โ Spread Sync โ OAuth credentials section. Paste both values. Click Sign in with Google. Done.
๐ก Usage
Inline cell
The current store count is `spread_sync("<spreadsheet-id>", "Sheet1", "B3")`.
Range as a table
```spread-sync
id: <spreadsheet-id>
sheet: Sheet1
range: A1:D10
header: true # optional, default true
transpose: false # optional, default false
```
Editable table โจ
```spread-sync
id: <spreadsheet-id>
sheet: Sheet1
range: A1:D10
editable: true
```
| Click any cell to edit. | Dirty cells highlight yellow. | Push button at the bottom batches all dirty cells into one values:batchUpdate. |
Whole sheet
```spread-sync
id: <spreadsheet-id>
sheet: Sheet1
```
Named range
```spread-sync
id: <spreadsheet-id>
sheet: Sheet1
named: MonthlyKPI
```
Aliases
Set kpi โ <long-spreadsheet-id> in settings, then reference as "@kpi":
```spread-sync
id: "@kpi"
sheet: Sheet1
range: A1:C5
```
Explicit write (one-shot)
```spread-write
id: <spreadsheet-id>
sheet: Sheet1
target: B3
value: 42
mode: replace # or "append"
```
Place cursor inside the block โ Command Palette โ Spread Sync: Push this block.
For range writes:
target: A1:B2
value:
- [1, 2]
- [3, 4]
๐๏ธ Commands
| Command | Description |
|---|---|
Spread Sync: Refresh current file | Invalidate cache for the active file, force re-fetch |
Spread Sync: Refresh all open files | Same, for every open markdown leaf |
Spread Sync: Refresh by spreadsheet | Prompt for an id / URL / @alias and invalidate all its cache |
Spread Sync: Push this block | Send the current spread-write block to Sheets |
๐จ Error badges
| Badge | Meaning |
|---|---|
โ re-auth | Token expired / not signed in โ click to re-sign-in |
โ no access | 403 / 404 from Sheets โ check sharing or API enablement |
โ Excel format | Sheet is a .xlsx uploaded to Drive โ convert to native Google Sheets |
โ syntax error | Block YAML / inline call is malformed (tooltip shows the parser error) |
โ range error | Inline syntax got a multi-cell range, or editable got an invalid range |
โ fetch failed | Generic API error โ hover for full message |
โ stale Nm | Last fetch failed; showing cached value N minutes old |
โ offline | Network is down |
๐๏ธ Architecture
Four isolated layers:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Renderer inline / block postprocessors โ
โ editable table widget โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Auth OAuth (PKCE) ยท safeStorage โ
โ localhost loopback callback โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Sheets+Drive pure HTTP (https.request) โ
โ read ยท batchRead ยท write โ
โ batchWrite ยท modifiedTime โ
โ 429 backoff โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Storage LRU cache ยท debounced persist โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
๐ Requirements
- Obsidian 1.4+
- Desktop only (Electron
safeStoragenot available on mobile) - Node 18+ for build
๐ง Limitations
editable: trueonly works with literalrange:likeA1:B2(notnamed:or whole sheet, nottranspose: true).- No batchGet wiring yet โ each inline expression is a separate API request (Phase 2).
Push this blockuseseditor.setValuewhich resets cursor/undo history (Phase 2: replace withreplaceRange).
๐งช Development
npm install
npm run dev # esbuild watch mode
npm test # vitest run โ 59 tests across 8 files
Tests cover: parser, id resolver, cache, Sheets client (with nock), Drive client, OAuth client (PKCE + token exchange), TokenStore, table formatter. Renderer / commands / settings UI are verified manually in a real Obsidian vault.
๐ License
MIT โ built by @yut0takagi.
For plugin developers
Search results and similarity scores are powered by semantic analysis of your plugin's README. If your plugin isn't appearing for searches you'd expect, try updating your README to clearly describe your plugin's purpose, features, and use cases.