A fast, local-only tax estimator and time tracker for 1099 / self-employed work. Hosted at https://fiscode.app (docs at https://docs.fiscode.app) and installable as a PWA.
fiscode is not a tax filing tool. It is an estimator and organizer that gets you most of the way to a quarterly estimate and a year-end packet. A tax accountant (or you) finishes from there.
⚠️ Use at your own risk. fiscode is a personal project I built for my own self-employed tax planning. It is not legal, tax, financial, or accounting advice. Estimates are best-effort approximations based on annually-changing tax constants that may be stale or wrong. Always consult a qualified tax professional before filing or making financial decisions. No warranty, no support, no guarantee of accuracy. By using fiscode you accept full responsibility for any decisions made on the basis of its output.
- Local only. All data lives on device (SQLite via OPFS). No backend, no network calls for app data.
- CSV is the source of truth. State round-trips losslessly through a single CSV file. Hand the yearly CSV to your accountant; reload it later to reconstruct full state.
- Instant. No spinners. Reads and writes are local and should feel synchronous.
- Non-destructive. Every mutation appends to an on-device version history. Deletes are soft and confirmed.
- Self-healing. Only the minimum profile is required (filing status, state, SE start date, entity type). Everything else degrades gracefully when missing.
See PLAN.md for what's intentionally future work (auth, E2EE
sync, docs site, native desktop).
- bun + turborepo monorepo
- TanStack Router SPA on Vite (apps/web)
- TanStack Start + Nitro (node-server preset) + fumadocs-mdx/ui (apps/fumadocs)
- shadcn/ui + Tailwind v4
- Drizzle + SQLocal (sqlite-wasm over OPFS)
- Papaparse for CSV
- vite-plugin-pwa
- vitest, oxlint + oxfmt
A full docs site lives in apps/fumadocs (TanStack Start + Nitro + fumadocs-mdx/ui).
- Local:
bun run dev:fumadocs→ http://localhost:4000 - Deployed: https://docs.fiscode.app via Dokploy. Nitro's
node-serverpreset producesapps/fumadocs/.output/server/index.mjs; the container runsnode ./server/index.mjs. See Deploy for the Docker setup. - Highlights: /docs/csv-format · /docs/tax-engine · /docs/year-end-packet
Both apps target Dokploy (self-hosted Docker PaaS). Not Vercel, not Cloudflare.
Each app has a multi-stage Dockerfile next to its source. The build context must be the repo root — both Dockerfiles install the workspace graph (bun install --frozen-lockfile) and then build their app from inside it.
Vite SPA → static dist/ served by nginx. Image: nginx:1.29-alpine with the SPA fallback and the COOP/COEP headers OPFS needs.
| Dokploy field | Value |
|---|---|
| Build context | repo root |
| Dockerfile | apps/web/Dockerfile |
| Port | 80 |
| Domain | fiscode.app |
| Health check | /healthz |
| Build args (optional) | VITE_TAX_DATA_BASE_URL=https://… to override the tax-data mirror |
Local smoke test from the repo root:
docker build -f apps/web/Dockerfile -t fiscode-web .
docker run --rm -p 8080:80 fiscode-web
# open http://localhost:8080TanStack Start + Nitro node-server build → node ./server/index.mjs in a node:22-slim container.
| Dokploy field | Value |
|---|---|
| Build context | repo root |
| Dockerfile | apps/fumadocs/Dockerfile |
| Port | 3000 |
| Domain | docs.fiscode.app |
| Env (optional) | PORT, HOST (default 3000 / 0.0.0.0) |
Local smoke test from the repo root:
docker build -f apps/fumadocs/Dockerfile -t fiscode-docs .
docker run --rm -p 3000:3000 fiscode-docs
# open http://localhost:3000Runs Bun as the desktop shell process. That's where bun:sqlite becomes the obvious swap-in for sqlite-wasm (same SQLite file on disk, native bindings, no WASM overhead). The browser PWA stays on sqlite-wasm + OPFS because there is no Bun runtime in a browser tab. No Dockerfile — it ships as a native installer.
fiscode/
├── apps/
│ ├── web/ # the PWA (UI only)
│ ├── fumadocs/ # docs site (TanStack Start + fumadocs-mdx/ui, port 4000)
│ └── desktop/ # stub: Electrobun wrapper (part 3)
└── packages/
├── core/ # money, dates, ids, shared types
├── tax/ # year configs + strategies + engine (pure)
├── csv/ # parse / build / round-trip (pure)
├── db/ # SQLocal + Drizzle schema + repos + history
├── ui/ # shared shadcn primitives
└── config/ # shared tsconfig bases
bun install
bun run dev:web # vite dev server on http://localhost:3001
bun run dev:fumadocs # docs dev server on http://localhost:4000
bun run build # turbo build across all apps + packages
bun run test # vitest run, all packages
bun run check-types # tsc --noEmit, all packages
bun run check # oxlint + oxfmt --writeIf your editor's
oxlint/oxfmtLSP errors withCannot find module '.../node_modules/oxfmt/bin/oxfmt'after a freshbun install, restart the editor's language server. Bun's isolated linker creates the symlinks correctly, but some LSPs cache file-not-found from before install finished.
- Export the yearly CSV at year end. Hand that file to your accountant.
- Import a CSV in a fresh browser (or after clearing OPFS) to fully reconstruct state. Edit history does not carry over — that's a SQLite concept, not a CSV one.
- The CSV begins with
#-prefixed provenance lines. Excel will not strip them; the fiscode importer does.
- Every annually-changing tax figure (brackets, standard deduction, SS wage
base, mileage rate, state rate, quarterly due dates, safe-harbor thresholds)
lives in
packages/tax/src/config/<year>.ts. - Each figure is marked
// todo: verifyfor the year it was seeded. Treat the engine as giving you a working estimate, not a tax return. - Sole-prop strategy is implemented today. S-corp seam exists as a
NotImplementedstub at the same interface.
TanStack Router file-based routes live in apps/web/src/routes/. Run
bun run dev:web to generate routeTree.gen.ts and start the dev server.
Tests for the tax engine and CSV round-trip live next to their source. SQLocal (in-browser SQLite) cannot run headlessly under Node, so DB code is exercised through the app itself.
See PLAN.md for future work.