If you're a developer who manages your own finances and you've ever thought "I wish I could just query my transactions with SQL" — the SheetLink CLI is built for you.
This guide walks through installing the CLI, authenticating, and syncing your Plaid-connected bank accounts directly into a Postgres database.
Prerequisites
- Node.js 18+
- A Postgres database (local, Railway, Supabase, Neon, RDS — anything with a connection string)
- A SheetLink account with MAX plan (CLI + database output require MAX)
- At least one bank connected via the SheetLink dashboard
Install the CLI
Verify the install:
Authenticate
This opens a browser window for Google sign-in. After authenticating, your credentials are stored locally and the CLI can run unattended.
Run Your First Sync to Postgres
SheetLink will:
- Connect to your SheetLink account and fetch connected banks
- Pull transactions via Plaid
- Create a
sheetlink_transactionstable if it doesn't exist - Upsert all transactions — no duplicates, no matter how many times you run it
You should see output like:
Chase ................. 23 transactions
Bank of America ....... 31 transactions
54 rows upserted → sheetlink_transactions
✓ sync complete
The Schema
Two tables are created automatically on first sync. The full schema matches what the SheetLink Google Sheets extension writes — all 34 Plaid transaction fields.
CREATE TABLE sheetlink_transactions (
transaction_id TEXT PRIMARY KEY,
account_id TEXT,
persistent_account_id TEXT,
account_name TEXT,
account_mask TEXT,
date DATE NOT NULL,
authorized_date DATE,
datetime TEXT,
authorized_datetime TEXT,
description_raw TEXT,
merchant_name TEXT,
merchant_entity_id TEXT,
amount DECIMAL(10,2),
iso_currency_code TEXT,
unofficial_currency_code TEXT,
pending BOOLEAN,
pending_transaction_id TEXT,
check_number TEXT,
category_primary TEXT,
category_detailed TEXT,
payment_channel TEXT,
transaction_type TEXT,
transaction_code TEXT,
location_address TEXT,
location_city TEXT,
location_region TEXT,
location_postal_code TEXT,
location_country TEXT,
location_lat DECIMAL(10,7),
location_lon DECIMAL(10,7),
website TEXT,
logo_url TEXT,
source_institution TEXT,
category TEXT,
synced_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE sheetlink_accounts (
account_id TEXT PRIMARY KEY,
persistent_account_id TEXT,
name TEXT,
official_name TEXT,
mask TEXT,
type TEXT,
subtype TEXT,
current_balance DECIMAL(10,2),
available_balance DECIMAL(10,2),
iso_currency_code TEXT,
institution TEXT,
last_synced_at TIMESTAMP DEFAULT NOW()
);
Upsert key: transaction_id — Plaid's unique identifier. Running sync repeatedly is always safe.
category_primary and category_detailed are Plaid's personal finance category fields (e.g. FOOD_AND_DRINK / FOOD_AND_DRINK_RESTAURANTS). category is the legacy Plaid category array joined as a string, kept for backwards compatibility. description_raw is the raw transaction name as Plaid returns it.
If you're migrating from an older version or just need the core fields, --slim writes the original 14-column schema:
sheetlink sync --output postgres://localhost/mydb --slim
Querying Your Data
Once synced, your transactions are queryable like any other table:
-- Monthly spending by category
SELECT
DATE_TRUNC('month', date) AS month,
category_primary,
SUM(amount) AS total
FROM sheetlink_transactions
WHERE amount < 0
GROUP BY 1, 2
ORDER BY 1 DESC, 3 ASC;
-- Top merchants last 90 days
SELECT
COALESCE(merchant_name, description_raw) AS merchant,
SUM(ABS(amount)) AS total_spent
FROM sheetlink_transactions
WHERE date >= NOW() - INTERVAL '90 days'
AND amount < 0
GROUP BY 1
ORDER BY total_spent DESC
LIMIT 20;
-- Monthly income
SELECT
DATE_TRUNC('month', date) AS month,
SUM(amount) AS income
FROM sheetlink_transactions
WHERE amount > 0
GROUP BY 1
ORDER BY 1 DESC;
💡 Tip
Amounts follow Plaid's sign convention: negative = money out (expense), positive = money in (income/deposit).
Other Output Formats
The --output flag supports more than Postgres:
The last command outputs JSON to stdout — pipeable to jq or any other tool.
Automating with Cron
After the initial sheetlink auth, the CLI runs without user interaction. Add a daily cron job:
# Sync every morning at 7am
0 7 * * * /usr/local/bin/sheetlink sync --output postgres://user:pass@host/dbname >> /var/log/sheetlink.log 2>&1
Or use a process manager like pm2 if you prefer:
pm2 crontab -- "0 7 * * * sheetlink sync --output postgres://..."
ℹ️ Info
If you're running this on a server, store your Postgres credentials in an environment variable rather than the connection string directly: sheetlink sync --output $DATABASE_URL
Connecting Remote Databases
The --output flag accepts any standard Postgres connection string:
# Supabase
sheetlink sync --output postgres://postgres:[password]@db.[project].supabase.co:5432/postgres
# Railway
sheetlink sync --output postgresql://postgres:[password]@[host].railway.app:5432/railway
# Neon
sheetlink sync --output postgres://[user]:[password]@[host].neon.tech/neondb
What MAX Includes
Postgres, SQLite, CSV, and JSON output are all MAX features. MAX also includes:
- REST API access with API key auth
sheetlink items— list connected bankssheetlink config— manage CLI settings- No rate limits on sync frequency
SheetLink MAX
Add CLI, Postgres, and API access
Upgrade to MAX — $10.99/mo14-day money-back guarantee · Cancel anytime