Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

UpSized icon

UpSized

Live Demo

A private, client-side dashboard for Up Bank accounts and transactions — the desktop/web companion to Up's phone-only app.

Not affiliated with, endorsed by, or officially connected to Up Banking Pty Ltd or Bendigo and Adelaide Bank in any way. This is an unofficial, community-built client that talks to Up's public API using your own Personal Access Token. Use it at your own risk — see Security & encryption and License below.


Contents


What it does

UpSized reads your Up Bank accounts and transactions using a Personal Access Token you generate yourself, and shows them in a fast, desktop-friendly dashboard:

  • Accounts overview — every account as a card, with a Saver/Spending/Home Loan badge, plus an automatic Individual / 2Up split if you have a joint account, and a Home Loan section that only appears if you actually have one.
  • Transaction list — infinite-scroll history, monthly separators, live "Pending" badges that clear automatically once Up settles them.
  • Transaction detail — click any transaction for a full breakdown: raw merchant text, card method & last 4 digits, who made the purchase, category, round-up, cashback, foreign amount, hold amount, and both timestamps.
  • Search — fuzzy filter across description, raw text, message, note, merchant, account, category, and tags.
  • Live sync — polls for new transactions on an interval you choose (or manual-only), shows Up's live rate-limit budget, and re-checks anything still "pending" so it never gets stuck.
  • Installable — the hosted build is a real PWA (installable, works offline for the app shell) via a service worker.

Everything happens in your browser, talking directly to Up's API. There is no UpSized server. See Security & encryption for the full story.

Currently this can only read your data not write as the Up api does not allow this.
See the Up API for details.


Screenshots

Onboarding welcome screen
Onboarding
Connecting a Personal Access Token
Connect
Setting a password unlock
Set password
All accounts dashboard view
All accounts
A single saver account view
Saver account
Transaction detail modal
Transaction detail
Settings panel
Settings
Mobile
Mobile

Click any thumbnail for the full-size image.


Hosted vs. Standalone — which one do I want?

The app ships as source (index.html + css/ + js/) but can also be packed into a single self-contained HTML file via pack.py. Both are the exact same app — the difference is how you run it, and that difference has one real consequence: whether passkeys are available.

Hosted (served over HTTPS, or http://localhost) Standalone (single file, opened via file://)
What it is index.html + css/js files, served by a real web server One file: standalone/UpSized.single.html, generated by pack.py
Passkeys (WebAuthn) ✅ Full support ❌ Unavailable — file:// is not a "secure context," so the browser refuses WebAuthn outright. The app detects this automatically and drops straight to the password option — nothing breaks, it just silently offers the fallback.
Password unlock Available as an opt-in alternative The only option
Installable as an app (PWA) ✅ Yes, via the service worker (sw.js) ❌ No — service workers also require a real origin, can't run from file://
Offline behaviour Shell is cached after first load and kept fresh automatically N/A — there's nothing to fetch, it's already one file
Best for Daily use, installing on your phone/desktop, strongest security mode Trying it instantly, air-gapped use, handing to someone with zero setup

The distinction that actually matters is the origin (file:// vs. https:///localhost), not which build you picked. If you take the standalone file and upload it to any HTTPS host instead of double-clicking it locally, passkeys work fine there too — pack.py just produces a portable artifact, it doesn't lock you out of anything.


Getting started

1. Get a Personal Access Token

You'll need an Up Bank account. Generate a token at api.up.com.au — the app asks for this once, on first run, and encrypts it before it's stored (see below).

2. Run it — pick one

Hosted (recommended — unlocks passkeys and PWA install):

# any static file server works; this is just the quickest for local testing
python -m http.server 8080
# then open http://localhost:8080

For real use, put the repo's contents on any HTTPS static host (GitHub Pages, Netlify, your own server with the included .htaccess, etc.) — no build step, no backend, no database required.

Standalone (fastest — zero setup, but no passkeys):

python pack.py
# writes standalone/UpSized.single.html — just open it in a browser

Re-run pack.py any time you change the source files; it always regenerates the whole bundle from scratch.

Requirements

  • Any modern browser (WebAuthn + IndexedDB support — that's essentially all of them, including mobile)
  • Python 3 (standard library only, no pip install needed) — only if you want to use pack.py or sync_manifest.py. The app itself needs nothing but a browser.

Security & encryption

This section is meant to be read, not skimmed — being upfront about exactly what protects your data (and what doesn't) matters more here than almost anywhere else in the app.

The short version

  • No server, ever. UpSized is static files. Your browser talks directly to api.up.com.au over HTTPS. There is nothing in between — no analytics, no proxy, no logging, nothing operated by anyone but you and Up.
  • Everything sensitive is encrypted at rest in your browser's local storage (IndexedDB) with AES-256-GCM before it's ever written to disk: your Personal Access Token, every account, every transaction.
  • The encryption key never touches disk in raw form. It's held in memory only while the app is unlocked, and is gone the moment you lock it, close the tab, or reload.
flowchart LR
    You["🧑 You, in your browser"] -->|"HTTPS, your token"| API["Up's official API\napi.up.com.au"]
    You -->|"AES-256-GCM encrypted"| Local["This device only\n(IndexedDB)"]
    API -.-> Nothing["🚫 no UpSized server\n🚫 no analytics\n🚫 no third party, anywhere"]
Loading

How the encryption key is obtained

You unlock the app with either a passkey or a password — your choice, made once during setup. Whichever you pick determines how the AES key is derived:

flowchart TD
    Start["Set up a lock"] --> Secure{"Secure context available?\n(https:// or localhost)"}
    Secure -- "No — e.g. file://" --> Password["Password mode\nPBKDF2-HMAC-SHA256, 600,000 rounds"]
    Secure -- "Yes" --> PRFCheck{"Does the authenticator\nsupport the WebAuthn\nPRF extension?"}
    PRFCheck -- "Yes" --> PRF["prf mode\nAES key derived directly\nfrom the passkey itself"]
    PRFCheck -- "No" --> Fallback["fallback mode\nReal random AES key,\ngated behind the passkey"]
Loading
Mode How the key is produced What's stored locally
prf (passkey, best case) The WebAuthn PRF extension derives a stable secret straight from the passkey itself on every unlock. The key is never generated or stored anywhere — it's recomputed fresh each time from the passkey + a salt. Just the credential ID and the salt (a salt isn't a secret by design — it only needs to stay consistent, not hidden).
fallback (passkey, older authenticators) Some authenticators/browsers support passkeys but not the PRF extension. In that case a real random AES-256 key is generated once and stored — access to it is gated behind a successful passkey check, but the key itself isn't mathematically derived from the passkey. The credential ID and the (encrypted-at-rest-by-the-OS, not by this app) random key material, plus the mode flag.
password A password you choose runs through PBKDF2-HMAC-SHA256 at 600,000 iterations (OWASP's current baseline) to derive the AES key. A random salt and a small encrypted "check" blob used only to confirm you typed the right password — the password itself is never stored, anywhere, in any form.

No recovery path for password mode, on purpose. If you forget it, there's no reset — the only way back in is wiping local data and reconnecting from scratch. The app tells you this explicitly before you set a password, and it's a deliberate trade-off: there's no server to hold a "forgot password" flow for.

Using the same passkey on multiple devices

Passkeys synced by your password manager (iCloud Keychain, 1Password, Google Password Manager, etc.) can be reused on a new device via "I already have a passkey" on the welcome screen. This authenticates against any passkey registered for the site rather than one specific stored credential ID, so a synced passkey is recognised even on a device that's never seen it before. Each device still derives (or generates) its own local key independently — nothing about your data syncs between devices, only the passkey's ability to unlock a device does. Up's data is re-pulled fresh from the API on first load anyway, so there's no cross-device data-consistency problem to solve here.

What's stored where

Storage Contents Encrypted?
IndexedDB — secure store Your Up Personal Access Token ✅ AES-256-GCM
IndexedDB — accounts / transactions stores Every account and transaction record ✅ AES-256-GCM (per record). createdAt, accountId, and status are kept as plain, unencrypted index fields — this is what makes infinite scroll, "last 100 on startup," and auto-purge possible without decrypting the whole database on every scroll. Nothing sensitive lives in those three fields; everything else (merchant, amounts, message, category, card details, etc.) is inside the encrypted blob.
localStorage Passkey credential ID, PRF/fallback salts, the chosen unlock mode, a small non-secret password "check" blob ❌ Not encrypted, but nothing here is a secret by itself — a credential ID and a salt are meaningless without the actual passkey/password to go with them.
Cache Storage (service worker) The app's own HTML/CSS/JS shell, for offline load N/A — it's just the app's own code, not your data. Up's API is explicitly never cached (see sw.js) — every request always hits the network live.

Rate limiting

Up doesn't publish an exact request budget, only that X-RateLimit-Remaining is present on every response — so the app treats it as something to discover live: it reads that header on every call, shows it in the topbar, and automatically backs off with retry/backoff on 429/5xx responses.


What this app can't protect you from

Being transparent cuts both ways — here's what's genuinely out of scope for client-side encryption, so you can judge the actual threat model:

  • A compromised or unlocked device. If someone has access to your unlocked browser/device, they have access to whatever you have access to — same as any app.
  • A malicious browser extension with broad page permissions could, in principle, read data while the vault is unlocked and the key is in memory.
  • Whoever hosts the file controls what code runs. If you don't control the hosting yourself (or don't trust it), you're trusting that host with everything — this is true of any client-side web app, not specific to UpSized. Prefer hosting it yourself, or the standalone build, if that matters to you.
  • Physical/forensic access to localStorage + a live passkey/password is, definitionally, enough to unlock it — that's the whole point of it being your unlock method.
  • There is no cloud backup, no account recovery, no "forgot password." Losing your passkey and forgetting your password (if you set one instead) means wiping local data and reconnecting from scratch. This is by design, not an oversight — there's no server to hold a recovery flow.

Settings & data lifecycle

Accessible via the ⚙ icon once unlocked:

  • Auto-refresh interval — off (manual only), or every 30s / 1min / 5min / 15min. Each check is a small delta request for "anything newer than what I already have," not a full re-pull.
  • Local history retention — how many days of transactions to keep on this device (default 90). Older ones are purged automatically if auto-purge is on.
  • Forget this device — wipes all local data (accounts, transactions, token, and the passkey/password enrollment itself) and returns to first-run. Irreversible, and the app makes you confirm it.

Project structure

UpSized/
├── index.html                Markup for all screens (onboarding, lock, main app, modals)
├── manifest.json             PWA manifest (readable source — see sync_manifest.py)
├── sw.js                     Service worker: app-shell caching, network-first strategy
├── css/
│   └── styles.css            All styling, one file, CSS custom properties for theming
├── js/
│   ├── crypto.js             Vault key derivation & AES-GCM encrypt/decrypt (see Security above)
│   ├── db.js                 IndexedDB wrapper — accounts, transactions, settings, token
│   ├── api.js                Up API client — requests, rate-limit tracking, retry/backoff
│   ├── ui.js                 Small shared DOM/formatting helpers
│   └── app.js                Orchestration: screens, sync engine, rendering, event wiring
├── icons/
│   └── icon.svg              App icon (also used as favicon and PWA icon)
├── pack.py                   Bundles everything into standalone/UpSized.single.html
├── sync_manifest.py          Regenerates the inlined manifest <link> in index.html from manifest.json
└── standalone/
    └── UpSized.single.html   Generated — see "Standalone" above

Tech stack & design notes

  • Zero dependencies, zero build step. Vanilla HTML/CSS/JS. Nothing to npm install.
  • Classic scripts, not ES modules — on purpose. import/export triggers a CORS-checked fetch that Chrome refuses outright under file://, even for same-folder files. Classic <script> tags don't have that restriction, so every file attaches its API to a shared window.UV namespace instead. Load order in index.html matters: crypto.js, db.js, api.js, ui.js, then app.js.
  • CORS-friendly by design. Up's API sends CORS headers that permit direct browser calls, which is the whole reason a server-side proxy isn't needed here.
  • Full inline documentation lives at the top of each js/*.js file if you want the deeper "why," not just the "what."
  • Code is mostly organic (written by me) with a dash of Clause however nearly all documentation was written by Claude because I am lazy and terrible at documenting anyway. (better than no documentation?)

License

MIT — permissive, free to use/fork/modify (including commercially), with an explicit "as is, no warranty" disclaimer, which matters given this handles real financial data.

See LICENSE.

About

A private, client-side dashboard for Up Bank accounts and transactions.

Resources

Stars

7 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages