BlogDevelopers

How to Build a Personal Finance Dashboard with SheetLink and Grafana

Sync your bank transactions to Postgres with the SheetLink CLI and visualize them in Grafana: spending trends, income, net worth, and more.

The SheetLink CLI syncs your transactions into Postgres or SQLite with Max.

If you're a developer who wants to actually understand your finances (not just see a list of transactions in an app), this is the setup worth building.

SheetLink's CLI syncs your bank transactions to a Postgres database. Grafana connects to that database and turns it into a real-time dashboard: spending by category, monthly cash flow, top merchants, income trends. The kind of visibility you'd normally need an expensive finance tool to get.

This guide walks through the full setup, from first sync to a working dashboard.

What You're Building

By the end of this guide you'll have:

  • All your bank transactions in a Postgres database, updated automatically via cron
  • A Grafana dashboard with panels for monthly spending, income, category breakdown, and top merchants
  • A setup that costs $0 in infrastructure (Grafana Cloud free tier + local or free-tier Postgres)

Prerequisites

  • Node.js 18+
  • A SheetLink account on the Max plan
  • Postgres (local, Supabase, Railway, Neon, or anything with a connection string)
  • Grafana (self-hosted or Grafana Cloud free tier)
  • At least one bank connected in the SheetLink dashboard
npm install -g sheetlink
sheetlink auth

This opens a browser window for Google sign-in. That login expires after a few hours, so for unattended runs like the cron job in Step 3, create a Max API key in the dashboard and save it locally with sheetlink auth --api-key sl_your_key_here.

Step 2: Run Your First Sync to Postgres

sheetlink sync --output postgres://localhost/finances

SheetLink creates two tables automatically:

  • sheetlink_transactions: every transaction from all connected accounts (35 columns)
  • sheetlink_accounts: account metadata and current balances

Upsert key is transaction_id, so running sync multiple times is always safe, with no duplicates.

You should see output like:

  Chase ................. 47 transactions
  Bank of America ....... 83 transactions
  Amex .................. 61 transactions
  191 rows upserted → sheetlink_transactions
✓ sync complete

Verify the data landed:

SELECT COUNT(*), MIN(date), MAX(date) FROM sheetlink_transactions;

Step 3: Automate with Cron

Add a daily sync so Grafana always has fresh data:

# Runs every morning at 7am
0 7 * * * /usr/local/bin/sheetlink sync --output postgres://user:pass@host/finances >> /var/log/sheetlink.log 2>&1

Tip

Store your connection string as an environment variable: sheetlink sync --output $DATABASE_URL. Keeps credentials out of your crontab.

Step 4: Connect Grafana to Postgres

  1. Open Grafana → Connections → Add new data source
  2. Select PostgreSQL
  3. Enter your connection details:
    • Host: your Postgres host + port
    • Database: your database name
    • User/Password: your credentials
    • SSL Mode: require for hosted databases, disable for local
  4. Click Save & Test, and you should see "Database Connection OK"

Step 5: Build Your Dashboard

Create a new dashboard and add panels using the queries below. Each one is a standalone panel. Add as many as you want.

Monthly Spending by Category

SELECT
  DATE_TRUNC('month', date) AS time,
  category_primary AS metric,
  SUM(ABS(amount)) AS value
FROM sheetlink_transactions
WHERE amount > 0
  AND date >= NOW() - INTERVAL '6 months'
GROUP BY 1, 2
ORDER BY 1

Panel type: Bar chart or Time series. Set visualization to stack bars for a clear breakdown.

Monthly Cash Flow (Income vs Expenses)

SELECT
  DATE_TRUNC('month', date) AS time,
  CASE WHEN amount < 0 THEN 'Income' ELSE 'Expenses' END AS metric,
  SUM(ABS(amount)) AS value
FROM sheetlink_transactions
WHERE date >= NOW() - INTERVAL '12 months'
GROUP BY 1, 2
ORDER BY 1

Panel type: Bar chart with grouped bars. Green for income, red for expenses.

Net Cash Flow per Month

SELECT
  DATE_TRUNC('month', date) AS time,
  SUM(amount) * -1 AS net_cash_flow
FROM sheetlink_transactions
WHERE date >= NOW() - INTERVAL '12 months'
GROUP BY 1
ORDER BY 1

Panel type: Time series. Positive = saved money, negative = spent more than earned.

Note

Amounts follow Plaid's sign convention: positive = money out (expense), negative = money in (income/deposit). The * -1 in the net cash flow query flips it so positive = surplus.

Top Merchants (Last 90 Days)

SELECT
  COALESCE(merchant_name, description_raw) AS merchant,
  SUM(ABS(amount)) AS total_spent
FROM sheetlink_transactions
WHERE amount > 0
  AND date >= NOW() - INTERVAL '90 days'
GROUP BY 1
ORDER BY total_spent DESC
LIMIT 15

Panel type: Bar gauge or Table.

Spending This Month vs Last Month

SELECT
  DATE_TRUNC('month', date) AS month,
  SUM(ABS(amount)) AS total_spent
FROM sheetlink_transactions
WHERE amount > 0
  AND date >= DATE_TRUNC('month', NOW()) - INTERVAL '1 month'
GROUP BY 1
ORDER BY 1

Panel type: Stat panel with two values side by side.

Current Account Balances

SELECT
  name AS account,
  current_balance
FROM sheetlink_accounts
ORDER BY current_balance DESC

Panel type: Table or Bar gauge.

Recurring Subscriptions

SELECT
  COALESCE(merchant_name, description_raw) AS merchant,
  COUNT(*) AS months,
  AVG(ABS(amount)) AS avg_amount,
  SUM(ABS(amount)) AS total_paid
FROM sheetlink_transactions
WHERE amount > 0
  AND payment_channel = 'online'
  AND date >= NOW() - INTERVAL '6 months'
GROUP BY 1
HAVING COUNT(*) >= 3
ORDER BY total_paid DESC

Panel type: Table. This surfaces anything that's charged you 3+ times in 6 months. It's a quick way to audit subscriptions.

Step 6: Set Up Alerts (Optional)

Grafana can alert you when spending crosses a threshold. For example, alert when monthly food spending exceeds $800:

  1. Open your spending by category panel
  2. Click Alert → New alert rule
  3. Set condition: value > 800 where category_primary = 'FOOD_AND_DRINK'
  4. Connect a notification channel (email, Slack, PagerDuty, or whatever you use)

The Full Stack

Bank accounts (Plaid)
    ↓
SheetLink CLI (daily cron)
    ↓
Postgres (sheetlink_transactions + sheetlink_accounts)
    ↓
Grafana (dashboards, alerts)

Total infrastructure cost at scale: SheetLink Max at $10.99/month. Grafana Cloud free tier handles personal use. Postgres on Railway or Supabase starts free.

For the user who already has a home server or a small VPS, the marginal cost is zero beyond the SheetLink subscription.

What You End Up With

A dashboard that shows you exactly where your money is going, updated every morning, queryable with SQL, and extensible with anything else Grafana can connect to. Home Assistant, server metrics, uptime: your financial data sitting next to everything else you monitor.

It's the kind of setup that takes an afternoon to build and then runs itself.

SheetLink Max

Everything in Pro, plus CLI automation, Postgres and SQLite output, investments, and Claude.

Compare plans ›

Full refund within 14 days. Cancel anytime.

Questions

Is Grafana free?

Grafana OSS (self-hosted) is completely free and open source. Grafana Cloud has a free tier that covers personal use: up to 3 users, 10,000 series, 14-day retention.

Do I need to know SQL to use this setup?

Basic SQL helps. The queries in this guide are straightforward SELECT + GROUP BY statements. If you can read them, you can modify them.

Can I use SQLite instead of Postgres with Grafana?

Grafana doesn't have a native SQLite data source plugin in most setups. Postgres is the recommended output for Grafana dashboards. Use `sheetlink sync --output postgres://...`.

What SheetLink plan do I need?

CLI automation and Postgres output require the Max plan ($10.99/month or $99/year).

How often does the data refresh?

As often as your cron runs. Once a day at 7am is typical. Grafana will reflect the latest data on every dashboard load.

See it work.

Schedule sheetlink sync and send transactions to Postgres or SQLite. CLI automation is part of Max. About the CLI ›

Keep reading.

Your bank data, in your control.

Free for one bank and your last 7 days. No card needed.