ClearTab Svelte Themes

Cleartab

A focused bill and debt dashboard for people who'd rather spend five calm minutes a week than a frantic afternoon every payday. Track recurring bills, credit cards (including 0% promo periods), monthly budget, payment history, and debt-payoff strategies — all behind a real account with server-side sync.

ClearTab

Quiet money. Calm month.

A focused bill and debt dashboard for people who'd rather spend five calm minutes a week than a frantic afternoon every payday. Track recurring bills, credit cards (including 0% promo periods), monthly budget, payment history, and debt-payoff strategies — all behind a real account with server-side sync.


Stack

Layer What
Frontend pages Svelte 5 (runes) for each dashboard tab, vanilla JS for navbar / modals / auth / theme
Build Vite 8 multi-page, with the @sveltejs/vite-plugin-svelte plugin
Styling Hand-written CSS split into themed files (tokens, components, theme-dark, pages, marketing, budget) + a small Tailwind v4 utility build
Server Node 22 + Express 5, better-sqlite3 for storage
Auth bcrypt password hashing, opaque server-side sessions in SQLite, HttpOnly cookies, CSRF double-submit token, Cloudflare Turnstile bot protection, in-memory login rate limiter
Per-user data sync One JSON blob per user in SQLite, PUT /api/data with debounced client writes, localStorage as offline cache, custom-event reactivity bridge to Svelte

Single deployable unit — Express serves the API and the static client (raw client/ in dev, the Vite-built dist/ in production).


Quick start

Requires Node ≥ 22 (for native fetch, --watch, and the better-sqlite3 / bcrypt prebuilds).

git clone <repo> cleartab
cd cleartab
npm install
npm run dev

Then open http://localhost:5173. Vite serves the client with HMR on :5173 and proxies /api/* to the Express server on :5222.

Sign in with the seeded dev account:

Email [email protected]
Password demopassword11

The seed lives in .env.development and is created automatically on first server start (only when NODE_ENV !== 'production').

You can also hit Express directly at http://localhost:5222 if you don't need HMR — same content, same auth flow, no Vite layer.


Project structure

cleartab/
├── client/
│   ├── *.html                       page entries (home, login, terms,
│   │                                privacy, dashboard, settings, 404, 500)
│   ├── css/
│   │   ├── styles.css               manifest — @imports the five below
│   │   ├── tokens.css               design tokens + body bg
│   │   ├── components.css           nav, buttons, badges, cards, modals…
│   │   ├── theme-dark.css           dark-mode overrides
│   │   ├── pages.css                page-frame, auth, legal, footer
│   │   ├── marketing.css            home/landing styles
│   │   ├── budget.css               Budget tab
│   │   └── tailwind-input.css       (Tailwind source for utility classes)
│   ├── js/
│   │   ├── app.js                   dashboard entry — imports the lot
│   │   ├── settings.js              /settings entry
│   │   ├── public-entry.js          /, /login, /terms, /privacy entry
│   │   ├── auth.js                  /api/auth client + page gating
│   │   ├── storage.js               shared bills/cards/payments/settings
│   │   │                            + debounced sync + cleartab:data-changed
│   │   ├── utils.js                 formatters, helpers, renderer registry
│   │   ├── modals.js                bill/card/pay/confirm modal logic
│   │   ├── navbar.js                renders the appbar into <header data-app-nav>
│   │   ├── theme.js                 light/dark theme handling
│   │   ├── export.js                CSV builders for the dashboard tabs
│   │   └── dashboard.js / bills.js / cards.js / budget.js /
│   │       history.js / payoff.js   thin mount shims for each Svelte tab
│   ├── svelte/                      Svelte 5 components for each tab
│   │   ├── DashboardView.svelte
│   │   ├── BillsList.svelte
│   │   ├── CardsList.svelte
│   │   ├── BudgetView.svelte
│   │   ├── HistoryList.svelte
│   │   └── PayoffView.svelte
│   ├── public/                      copied verbatim to dist root
│   │   ├── robots.txt
│   │   ├── sitemap.xml
│   │   ├── site.webmanifest
│   │   ├── icon.svg
│   │   └── og-image.svg
│   └── svelte.config.js
├── server/
│   ├── index.js                     Express entry — env loading, routes,
│   │                                static, 404/500, dev-user seed
│   ├── db.js                        better-sqlite3 + schema + statements
│   ├── session.js                   loadSession / requireAuth / requireCsrf
│   ├── captcha.js                   Cloudflare Turnstile siteverify
│   ├── rateLimit.js                 in-memory IP+email throttle (5 / 15 min)
│   ├── util.js                      email + password policy, BCRYPT_COST
│   └── routes/
│       ├── auth.js                  signup, login, logout, me
│       ├── data.js                  GET/PUT /api/data
│       └── account.js               change-email, change-password, delete,
│                                    export, export/<type>.csv
├── data/                            SQLite file lives here (gitignored)
├── dist/                            Vite build output (gitignored)
├── .env                             local secrets (gitignored)
├── .env.development                 dev defaults (committed — TEST keys)
├── .env.example                     template
├── vite.config.js                   multi-page + Svelte + clean URLs
└── tailwind.config.js

npm scripts

Script What it does
npm run dev Express (:5222) + Vite (:5173) concurrently. Vite proxies /api → Express. Use this for normal development.
npm run dev:server Express only, with node --watch.
npm run dev:client Vite only.
npm run dev:css Watch-rebuild the Tailwind utility classes into client/css/tailwind-built.css.
npm run build:css One-shot Tailwind utility build (minified).
npm run build vite builddist/. Strips HTML comments and minifies CSS/JS.
npm run preview vite preview of the built dist/.
npm start NODE_ENV=production node server/index.js — serves dist/ + the API.

Environment

Variables are loaded in this order; the first match per variable wins:

.env.<NODE_ENV>.local     # local-only overrides for this mode
.env.local                # local-only overrides, any mode
.env.<NODE_ENV>           # committed defaults for this mode
.env                      # local catch-all

So npm run dev (default NODE_ENV=development) picks up the committed test keys in .env.development, and your private .env is only consulted as a fallback. In production (npm start), .env.production.local, .env.local, and .env all get a shot — but .env.development is skipped.

Variables

Variable Required Default (dev) Notes
NODE_ENV no development Drives env-file loading + cookie Secure flag
PORT no 5222 Express port
TURNSTILE_SECRET yes test key Cloudflare Turnstile server-side secret
TURNSTILE_SITEKEY yes test key Cloudflare Turnstile public sitekey
SESSION_COOKIE no ct_sid Cookie name
SESSION_TTL_HOURS no 12 Session lifetime
DEV_USER_EMAIL no [email protected] Seeded on first dev start (skipped in prod)
DEV_USER_PASSWORD no demopassword11 Same as above

Real Turnstile keys come from https://dash.cloudflare.com/?to=/:account/turnstile — create a widget, copy the sitekey + secret into your .env.


URLs

Clean URLs throughout. Everything *.html legacy URL 301-redirects to its clean form, both on Express and the Vite dev middleware.

URL Page Auth Indexed
/ Marketing landing public
/login Log-in / sign-up public
/terms Terms of Use public
/privacy Privacy Policy public
/dashboard App dashboard (Bills / Cards / Budget / History / Payoff) required ❌ noindex
/settings Email / password / export / import / delete required ❌ noindex
/404 Not-found page public
/500 Server-error page public

API

All under /api. JSON bodies, JSON responses (except CSV / JSON export endpoints).

Auth

Method Path Purpose
POST /api/auth/signup Create account (Turnstile + honeypot + timing + rate-limit checks)
POST /api/auth/login Sign in
POST /api/auth/logout Destroy session (requires X-CSRF-Token)
GET /api/auth/me Session check — returns {user, csrfToken} or {user: null}
GET /api/config Public config (currently just turnstileSitekey)

Per-user data

Method Path Purpose
GET /api/data Whole snapshot — {email, bills, cards, payments, settings}
PUT /api/data Replace the snapshot (auth + CSRF)

Account management

Method Path Purpose
POST /api/account/change-email Change email (re-verifies password)
POST /api/account/change-password Change password (also signs out other devices)
POST /api/account/delete Delete account + all data
GET /api/account/export Full JSON download
GET /api/account/export/bills.csv Bills CSV
GET /api/account/export/cards.csv Cards CSV
GET /api/account/export/history.csv Payment history CSV

All mutating routes (everything except the four GETs) require the session cookie and the X-CSRF-Token header — its value is the csrfToken returned by /api/auth/me (or by signup / login).


How a few things work

Session + CSRF model

  • Login creates a session row in SQLite with an opaque random ID and a separate random CSRF token.
  • The session ID rides in an HttpOnly, SameSite=Lax, Secure (in prod) cookie — unreadable from JS.
  • The CSRF token is returned in JSON bodies; client keeps it in memory and echoes it in X-CSRF-Token on mutating requests.
  • Changing your password also deletes every other session for the same user, leaving only the current device signed in.

Per-user data flow

  1. Dashboard boots → storage.bootstrapData()GET /api/data → populates the live arrays (bills, cards, payments, settings) imported by every module.
  2. Any mutation goes through storage.save(key, value) → writes localStorage and schedules a debounced (800 ms) PUT.
  3. After the PUT (or on applyData), storage dispatches window.dispatchEvent(new CustomEvent('cleartab:data-changed')).
  4. Every Svelte tab listens on onMount and re-pulls its $state snapshot from the live arrays — no prop wiring between vanilla modules and components.
  5. Offline writes get flushed on pagehide / visibilitychange:hidden via fetch(keepalive: true).

Card balances on payments

Marking a card payment as paid (confirmPay) decrements card.balance (and card.promoBalance if present). Edit-payment applies the delta. Delete-payment from the History tab adds the amount back. Balances never go negative.

Dev vs production static serving

  • Dev: Express serves client/ directly + client/public/ as a fallback (so robots.txt etc. work on :5222). Vite serves the same content from :5173 with HMR + proxy.
  • Production (NODE_ENV=production): Express serves dist/ — which Vite has already merged with client/public/ contents — and the Secure cookie flag is enabled.

Production deploy

  1. Get real Turnstile keys from the Cloudflare dashboard, put them in .env (or your hosting provider's env panel).
  2. Set up TLS in front of the app — Render, Fly, Caddy, nginx, Cloudflare — anything that terminates HTTPS. The HttpOnly session cookie is set with Secure in production and will simply be dropped over plain HTTP.
  3. npm install (build deps included for better-sqlite3 / bcrypt native modules).
  4. npm run build:css && npm run build — produces dist/.
  5. npm start — Express serves dist/ + the API on the configured PORT.
  6. Persist data/ between deploys (it holds cleartab.db).
  7. Replace https://cleartab.app placeholders with your real domain anywhere it appears (sitemap.xml, robots.txt, every public page's canonical / og:url / twitter:image / JSON-LD URL). A one-shot replace:
    grep -rl 'cleartab.app' client/ | xargs sed -i '' 's|https://cleartab.app|https://your-domain.com|g'
    

SEO + standards

  • robots.txt allows everything except /dashboard, /settings, /api/* and points to the sitemap.
  • sitemap.xml lists the four public pages.
  • Every public page carries Open Graph + Twitter cards, a canonical URL, and a description. The home page also ships a JSON-LD WebApplication schema. Private pages set noindex,nofollow.
  • A web manifest + maskable SVG icon make the app installable.

License

AGPL-3.0-or-later. If you host a modified version, you need to make your source available to its users.

Top categories

Loading Svelte Themes