The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the Expense listing page.
Expense is a seamless receipt tracking solution: receipts are automatically sent via email, mileage uses the tax rate provided by the IRS, and a smart AI assistant does your data entry so that you can forget about the process altogether.



What it does:
YYYY-MM-DD_Report_Name.jpgStack:
Auth and accounts:
Email/password login: email is the login name, stored in lowercase and validated, and unique at signup/join. All expenses, reports, categories, and settings belong to an account; everything in an account is shared between users, and accounts are fully isolated from each other.
APP_EMAIL/APP_PASSWORD when
the database is empty; pre-existing accounts get their login restored from
APP_EMAIL on the first launch (initStore).Marketing pages that are available publicly double as the AI search surface: if
an assistant is asked for an expense tracker, the GPTBot / OAI-SearchBot /
ClaudeBot / PerplexityBot crawler quotes them. The copy is written in such a way
that it's easily quotable and includes the app name and the URL and lives only
in one place, the content files under app/data/ (one markdown or YAML file per
page, parsed by app/lib/content.server.ts), which render all the surfaces:
| Page | Purpose |
|---|---|
/ | Landing page (SoftwareApplication JSON-LD) |
/about | Full feature/benefit list (AboutPage JSON-LD) |
/faq | 24 Q&As matching real AI queries (FAQPage JSON-LD) |
/alternatives | Expense vs Expensify comparison (WebPage + FAQPage JSON-LD) |
/llms.txt | The llmstxt.org file, the curated overview AI assistants read |
.md mirrors | Markdown mirrors per the llms.txt convention |
Plumbing to support it: public/robots.txt explicitly permits the AI crawlers
while app routes are blocked, and public/sitemap.xml lists the public pages.
These routes are public (see the root loader in app/root.tsx); everything
else still requires a session.
YYYY-MM-DD_REPORT_FILE.ext).The app speaks the Model Context Protocol at https://expense.labnotes.org/mcp
(auth: OAuth 2.1 authorization-code + PKCE, where you sign in and authorize
the connection; no API keys). An assistant linked with to your account can:
Connect any MCP client:
The client will open your browser; you'll log in and click Allow. You can manage
connections (delete per token, disconnect completely) in Settings -> Agents &
API (MCP). For the full reference, see docs/mcp.md and for
directory listings: docs/mcp-directories.md.
The storage is Postgres-only via Prisma 8 (prisma/contract.prisma is
the source of truth; pnpm build:prisma emits the contract artifacts the
runtime reads). DATABASE_URL is required upon launch (otherwise the app will
crash with an error). Image blobs are stored inside Postgres BYTEA
(image_blobs) in production and development; there is no additional storage
service.
| Data | Images |
|---|---|
accounts / users / | Postgres BYTEA (image_blobs, |
expenses / reports / | prod and dev) |
categories / settings / | |
mileage / image_blobs |
All reads/writes go through the modules in app/lib/db/ (Prisma queries scoped
by accountId); image storage is handled by app/lib/images.server.ts (Prisma
imageBlob). Image blobs are kept in images/{accountId}/... pathnames on all
backends. They are namespaced per account, so two accounts can never have a
name conflict. Schema changes: edit prisma/contract.prisma, run
pnpm build:prisma (contract emit), then pnpm db:push locally and on
deploy (see docs/deploy.md).
Load order: real process.env (Vercel dashboard, or inline) wins; a local
.env is used to fill holes. DATABASE_URL is required; .env is gitignored.
dev / test (local .env):
On an empty database the first account + user are bootstrapped from
APP_EMAIL/APP_PASSWORD (fail-closed if missing); thereafter, users are
created via the app's signup/join flow. SESSION_SECRET is always required.
APP_EMAIL/APP_PASSWORD can be omitted from .env after you have at least
one user.
Accounts created before email login (username era) retain their original
username as the stored email until APP_EMAIL is set; initStore then adds
that address to the bootstrap (oldest) user, so the configured credentials
continue to work.
Tests deliberately hardcode expense_test (Postgres with blobs/images), ignore
the local database, and reset the schema from Prisma on every run (pnpm test:db:push in the test setup).
prod (Vercel): set env vars in the project dashboard (Settings →
Environment Variables): DATABASE_URL (Supabase Supavisor pooled URL),
SESSION_SECRET,
and (only until the first user exists) APP_EMAIL / APP_PASSWORD.
Vercel sets them during runtime; .env does not exist.
Forward an email receipt to your inbox address and it will be parsed and imported automatically: the merchant, amount, and category are extracted, the receipt is uploaded as an image, and the expense date is the date when the email was forwarded. In case something could not be parsed, an explanation email is sent back.
How it determines what to import:
LLM_VISION_MODEL), and tesseract.js (worker/fonts fetched from a CDN at
runtime) runs only when the provider errors. RECEIPT_OCR_MODE=tesseract
forces local OCR, =deepseek forces the model..eml is not
parsed; use normal inline forwarding (for example, Gmail/iOS includes original
email in the body).maxDuration),
enough to download attachment + OCR + parse.OpenStreetMap services (Nominatim geocoding, OSRM routing, OSM raster tiles maps). Rate limited but good enough for personal use. If OSRM is not available, distance calculation fallbacks to the straight line (marked "approx.").