Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 62 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
# xpenser

Open-source personal income and expense tracking for people who want a
self-hosted finance app they can inspect, extend, and connect to their own
workflows.
xpenser is an open-source, self-hostable personal finance tracker for people
who want to replace spreadsheet-based income and expense tracking with
dashboards, categories, vendors, reports, and API/MCP access.

![xpenser landing page preview](./docs/assets/xpenser-landing-preview.png)
![xpenser dashboard month view with income, expenses, net total, and category detail](./docs/assets/xpenser-dashboard-month.png)

It started as a practical replacement for a real Google Spreadsheet accounting
workflow. The project is early and still evolving, but it is useful enough to
run, inspect, extend, self-host, or use as a working Cleverbrush Framework
reference app.

xpenser is also a real-world reference app for
[Cleverbrush Framework](https://docs.cleverbrush.com). It shows how a
[Cleverbrush Framework](https://docs.cleverbrush.com), showing how a
schema-first TypeScript stack can drive API contracts, validation, OpenAPI,
typed clients, React forms, auth-aware endpoints, observability, Telegram
workflows, and MCP access from one cohesive application.

## Why xpenser

- Move spreadsheet-style tracking into a structured app with dashboards,
categories, vendors, reports, and searchable transaction history.
- Track income, expenses, refunds, and returns with categories, notes, dates,
vendors, and currencies.
- Review daily, weekly, monthly, quarterly, and yearly summaries with category
Expand All @@ -27,6 +34,27 @@ workflows, and MCP access from one cohesive application.
- Connect external tools through API keys, a typed Node client, an MCP
server, and a Telegram bot.

## Screenshots

<table>
<tr>
<td>
<img src="./docs/assets/xpenser-dashboard-month.png" alt="xpenser dashboard month view" width="360">
</td>
<td>
<img src="./docs/assets/xpenser-transactions.png" alt="xpenser transactions table" width="360">
</td>
<td>
<img src="./docs/assets/xpenser-preferences-mcp-email.png" alt="xpenser preferences with email reports, API keys, and MCP settings" width="360">
</td>
</tr>
<tr>
<td>Dashboard month view</td>
<td>Transaction history</td>
<td>Email reports, API keys, and MCP</td>
</tr>
</table>

## Built With Cleverbrush

xpenser is intentionally small enough to inspect while still exercising
Expand Down Expand Up @@ -208,6 +236,25 @@ For public deployments, put your reverse proxy in front of the web app and set
`APP_URL` to the public origin. The API service stays private on the Docker
network and the Next app exposes it under `/external-api`.

For a smaller public deployment, use `docker-compose.prod.yml` as the starting
point and provide production secrets for `NEXTAUTH_SECRET`, `AUTH_SECRET`,
`JWT_SECRET`, `WEB_API_SERVICE_SECRET`, and `TELEGRAM_BOT_SERVICE_SECRET`.

## Optional Integrations

The default `.env.example` keeps external integrations off unless you configure
their provider credentials:

- OpenAI email insights: set `OPENAI_API_KEY`, `OPENAI_REPORT_MODEL`,
`RESEND_API_KEY`, `EMAIL_FROM`, `EMAIL_REPORTS_ENABLED=1`, and
`EMAIL_REPORTS_SCHEDULER_ENABLED=1`.
- Telegram bot workflows: set `TELEGRAM_BOT_TOKEN`, `TELEGRAM_BOT_USERNAME`,
and `TELEGRAM_BOT_SERVICE_SECRET`.
- Vendor enrichment: set `BRANDFETCH_API_KEY` or `BRANDFETCH_CLIENT_ID`, then
enable `VENDOR_ENRICHMENT_ENABLED=1`.
- Google sign-in: configure direct Google OAuth as described above, or leave
it disabled and use email/password accounts.

## External API

Create an API key from Settings -> Preferences -> API keys. The API key can be
Expand Down Expand Up @@ -254,7 +301,8 @@ xpenser exposes an MCP Streamable HTTP endpoint for AI agents at
`/external-api/mcp`. Use the same API key from Settings -> Preferences -> API
keys as a bearer token. MCP tools can read and manage the API-key owner's
vendors, categories, and transactions, so treat MCP access as full account data
access:
access. Use a dedicated API key for MCP clients and revoke it when access is no
longer needed:

```json
{
Expand Down Expand Up @@ -309,9 +357,14 @@ product improvements.

## Project Status

xpenser is early, practical, and evolving. The goal is to remain useful as a
personal finance app while staying clear enough for developers to learn how a
Cleverbrush full-stack project fits together.
xpenser is early, practical, and evolving. It has no meaningful user traction
yet; feedback on product fit, README clarity, self-hosting, and MCP workflows is
welcome. The goal is to remain useful as a personal finance app while staying
clear enough for developers to learn how a Cleverbrush full-stack project fits
together.

xpenser does not currently ship bank sync, budget planning, net-worth tracking,
native mobile apps, or mature import pipelines.

## License

Expand Down
6 changes: 3 additions & 3 deletions apps/web/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { ThemeProvider } from '@/components/theme-provider';

const publicUrl = process.env.APP_URL ?? 'https://xpenser.cleverbrush.com';
const description =
'Open-source personal finance tracking for self-hosted workflows, built with Cleverbrush Framework.';
'Open-source, self-hostable personal finance tracking for replacing spreadsheets with dashboards, reports, API keys, and MCP access.';

export const metadata: Metadata = {
metadataBase: new URL(publicUrl),
Expand All @@ -20,7 +20,7 @@ export const metadata: Metadata = {
type: 'website',
url: '/',
siteName: 'xpenser',
title: 'xpenser',
title: 'xpenser - open-source personal finance tracker',
description,
images: [
{
Expand All @@ -33,7 +33,7 @@ export const metadata: Metadata = {
},
twitter: {
card: 'summary_large_image',
title: 'xpenser',
title: 'xpenser - open-source personal finance tracker',
description,
images: ['/og-image.png']
}
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export default function manifest(): MetadataRoute.Manifest {
name: 'xpenser',
short_name: 'xpenser',
description:
'Open-source personal finance tracking for self-hosted workflows.',
'Open-source, self-hostable personal finance tracking for spreadsheet replacement workflows.',
start_url: '/dashboard',
scope: '/',
display: 'standalone',
Expand Down
16 changes: 14 additions & 2 deletions apps/web/components/landing-page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,28 @@ describe('LandingPage', () => {
screen.getByRole('heading', { level: 1, name: 'xpenser' })
).toBeTruthy();
expect(
screen.getByText(/Track income, expenses, refunds/i)
screen.getByText(/Replace spreadsheet-based income and expense/i)
).toBeTruthy();
expect(screen.getByText(/early and evolving/i)).toBeTruthy();
expect(
screen.getByAltText(/xpenser dashboard month view/i)
).toBeTruthy();
expect(
screen.getByText(/Learn Cleverbrush from a working app/i)
).toBeTruthy();
expect(screen.getAllByText(/Telegram bot/i).length).toBeGreaterThan(0);
expect(screen.getAllByText(/MCP server/i).length).toBeGreaterThan(0);
expect(screen.getAllByText(/self-hosted/i).length).toBeGreaterThan(0);
expect(screen.getAllByText(/self-host/i).length).toBeGreaterThan(0);
expect(screen.getAllByText(/open-source/i).length).toBeGreaterThan(0);
expect(screen.getAllByText(/MIT licensed/i).length).toBeGreaterThan(0);
expect(
screen.getAllByText(/Spreadsheet replacement/i).length
).toBeGreaterThan(0);
expect(
screen.getByText(
/read or manage vendors, categories, and transactions/i
)
).toBeTruthy();
expect(
screen.getAllByText(/multiple currencies/i).length
).toBeGreaterThan(0);
Expand Down
Loading
Loading