fiscode

CSV format

The lossless source-of-truth format fiscode reads and writes — and how to author it by hand.

CSV is the canonical format for fiscode data. Every entity is a row in a single CSV, discriminated by the record_type column. The flat row shape lives at packages/csv/src/schema.ts; adding new fields means adding new optional columns at the end so old exports still parse.

Two workflows

fiscode supports two ways into the same data model:

  1. App-first — enter rows in the web app, click Export when you want a CSV (for backup, handoff to an accountant, or to load on a different device). This is the common path.
  2. CSV-first — author the CSV yourself (in Excel, Numbers, a text editor, a script), then Import it into the app. Everything you can do in the UI you can do directly in CSV; the schema below is the contract.

Both paths produce identical state. Round-tripping Export → Import is byte-stable across data rows; the only thing that changes is the provenance timestamp.

The manual-workflow example

A complete, ready-to-import example CSV — one row of every record_type — is available as a download:

To regenerate it from the latest schema:

bun run example:csv

The example exercises every record type so a hand-author has a concrete pattern for each row shape. Use it as a starting skeleton: keep the ones you need, delete the rest, edit values.

Authoring tips

  • Column order doesn't matter on import — the parser keys on the header row. But the export always emits columns in the canonical order from CSV_COLUMNS in packages/csv/src/schema.ts, so if you want a stable diff, follow that order.
  • Leave irrelevant columns empty. A flat row carries every column for every record type; for an income row, fields like office_sqft and make are empty strings.
  • IDs must be unique within a record_type. They can be any string but ULIDs are what the app emits. A simple convention works fine for hand-authored files: in_001, in_002, …
  • The profile row is a singleton. Its id is always the literal string profile.
  • Dates are ISO YYYY-MM-DD. Timestamps in created_at / updated_at are ISO 8601 with a Z suffix — but if you're hand-authoring, you can leave these empty and the importer will fill them.
  • Money is integer cents. $1,234.56 becomes 123456. No decimals; the importer rejects non-integer numeric cells loudly rather than silently zeroing them.
  • Booleans are 1 / 0 / empty. Anything else fails validation.

Provenance header

Each exported CSV begins with #-prefixed comment lines containing app version, export timestamp, scope (full vs yearly), and the source URL. Comments are stripped on import via Papa.parse({ comments: '#' }). You can omit these lines in a hand-authored file — they're purely informational.

# fiscode export
# source: https://fiscode.app
# schema: v1
# exported: 2026-06-04T12:00:00.000Z
# scope: full
record_type,id,...

Common columns

Every row carries these five columns regardless of record_type. Empty values are serialized as empty strings.

Prop

Type

Record types

Below: one table per record_type showing the columns that apply to that record. The common columns above always apply too; they're omitted here to keep each table focused.

profile

Single row. Drives the tax engine's filing status, state, and feature flags. Its id must be the literal string profile.

Prop

Type

entity

Dated entity periods. Currently one active per year, validated at import. Use end_date to mark a switch (e.g. sole-prop Jan–Jun, single-member LLC Jul onward = two rows).

Prop

Type

spouse

Dated spouse income/withholding spans. Optional; only relevant when filing_status is mfj or mfs and your spouse has W-2 income. The app uses the latest-overlapping span for the estimate year.

Prop

Type

client

Payers. Income rows reference clients by client_id.

Prop

Type

income

1099 or W-2 entries, per payment. Dated so the quarterly engine can bucket them. Tax math only sums rows where source_type is 1099; w2 rows are informational unless you also record them on a spouse span.

Prop

Type

time_entry

Time-tracking sessions. Independent of income; not consumed by the tax engine.

Prop

Type

vehicle

Vehicles available for business mileage. Each mileage row references one by vehicle_id.

Prop

Type

mileage

Per-trip business miles. The tax engine multiplies the year's total by the IRS standard mileage rate (configurable per year).

Prop

Type

home_office

Dated home-office configuration. Only the simplified method ($5/sqft, capped at 300 sqft / $1,500/yr) is calculated in v1; actual-method rows are stored but contribute $0 to the deduction until the actual-method calc lands.

Prop

Type

expense

Categorized business expenses. flag_for_section_179 marks candidates for §179 expensing — purely informational in v1 since every expense is already fully deducted in the year incurred.

Prop

Type

retirement_contribution

Gated by profile.uses_retirement. SEP / Solo 401(k) / Roth contributions. Informational in v1 — they're stored and surfaced in the dashboard but don't yet flow into the deduction.

Prop

Type

Import modes

When you upload a CSV to fiscode you pick one of three modes:

  • overwrite — snapshot the current DB state into history, truncate all data tables, load the CSV. Use this when the CSV is the source of truth (typical CSV-first workflow).
  • append — keep existing rows; merge new ones by ID. Collisions are reported and existing rows win. Use this when adding a batch (e.g. a month of receipts exported from a bank).
  • restore — alias for overwrite, with confirmation copy emphasizing the snapshot. Use this when reverting to a known-good backup.

Every import — regardless of mode — appends a __import_snapshot history entry containing the full pre-import bundle, so you can revert.

Validation

The importer surfaces errors per-row rather than crashing on the first bad cell. Common rejections:

  • Unknown record_type — typo or new record type the running app doesn't recognize.
  • Non-integer numeric cells — e.g., amount_cents: 12.50. Use integer cents.
  • Missing se_start_date on profile — required; the tax engine uses it to gate which rows count toward the year.
  • Bad boolean values1, 0, or empty only. true / yes / TRUE all reject.

Round-trip guarantee

Export full CSV → wipe → Import → Export full CSV is byte-stable across data rows. The provenance timestamp on line 4 is the only thing that changes. This invariant is enforced by a vitest test in packages/csv/src/round-trip.test.ts.

On this page