← all work
productin progressLaravel API · Next.js

CashTrakd

CashTrakd is a budgeting app for people who would rather not think about spreadsheets: accounts, budgets, goals and spending in one calm overview, with streaks and badges to make checking in feel good. It's still being built: the API, auth and accounts are real, and the rest of the dashboard is designed but not wired up yet.

LaravelSanctumPostgreSQLNext.jsReactTailwind CSSDockerGitHub Actions
2 apps
Laravel API + Next.js front end, built as separate apps
cookie auth
Sanctum SPA sessions with CSRF, no tokens in localStorage
5 resources
accounts, categories, budgets, transactions and transfers in the v1 API

the problem

Most budgeting apps either drown you in charts or nag you with red numbers, so people stop opening them. CashTrakd aims for a clear picture of your money and small wins (streaks, badges, goals you can see moving) that make coming back a habit instead of a chore.

how it's built

Two separate apps. A Laravel API, versioned under /api/v1, with Postgres and Redis running in Docker via Sail, models for accounts, categories, budgets, transactions and transfers, account numbers stored hashed, and tests running in GitHub Actions. And a Next.js front end that talks to it with Sanctum's cookie-based SPA auth, so the session lives in an httpOnly cookie and every state-changing request carries a CSRF token.

where it's at

Working today: register, sign in and out, protected routes, and a dashboard whose accounts and net worth come from the API. Budgets, goals and recent activity are on screen as designed but still use sample data while their endpoints are built. Next up: transactions, budgets and goals end to end, then bank sync and a hosted release.

in action

select a screenshot to enlarge it · scroll sideways for more

// apiFetch(): every call from the Next.js app to the Laravel API
// only state-changing requests need the CSRF handshake
if (isMutation) {
  await ensureCsrfCookie();
  requestHeaders.set("X-XSRF-TOKEN", readXsrfTokenFromCookie());
}
 
const response = await fetch(`${API_URL}${path}`, {
  ...init,
  method,
  credentials: "include", // always send the Sanctum session cookie
  headers: requestHeaders,
  body: body === undefined ? undefined : sendsRawBody ? body : JSON.stringify(body),
  signal: combineSignals(init.signal, timeoutSignal),
});