Inspiration

I own a car in the Philippines and I could not answer a basic question about it: what has this car cost me? The fuel receipts, the casa service invoice, the LTO registration, the insurance renewal, and the toll stubs all lived in different places. Nothing connected them, so my answer was always a guess based on the last oil change.

The tracker apps I tried wanted an account, a subscription, and a server. I wanted a record that stays on my phone and a number I can trust. Two apps showed the direction was viable: Mayi, for the paid-upfront offline model, and Tarsi, for depth inside one personal domain. Fillo copies neither, but they proved you can charge once, work offline, and still build something substantial.

What it does

Fillo is a paid-upfront iPhone app for personal car ownership. No account, no subscription, no network requirement, no ads, no third-party analytics. Pay once and every feature is included, along with later updates to the same app.

  • Keep any number of cars in one Garage, with one Active car.
  • Log fuel-ups and EV charging sessions with odometer, quantity, unit price, total cost, efficiency, station or provider, and receipts.
  • Log maintenance, repairs, and ownership expenses such as registration, insurance, tolls, parking, and accessories.
  • Track Running Cost, the main financial number, from saved records. Fillo never adds amounts in different currencies together.
  • See Cost Ahead estimates for 30, 90, and 365 days, drawn from saved history, confirmed maintenance intervals, recurring expenses, and document renewals.
  • Set reminders by date, by odometer, or both, with estimated due dates labeled as estimates rather than manufacturer requirements.
  • Keep registration and insurance paperwork in Glovebox, and export a Vehicle Passport PDF when a mechanic, insurer, or buyer needs the history.
  • Create a restorable Fillo Backup, export editable CSV, and recover deleted records for 30 days in Recently Deleted.
  • Protect access with Privacy Lock, and hide sensitive values with Privacy Mode without altering the saved data.

How I built it

Native SwiftUI and SwiftData, iOS 18 minimum, plus a Home Screen widget target. 127 Swift files, roughly 25,000 lines including tests, in commits from late April to early September.

The code splits into three layers. Domain types live in fillo/Domain: the SwiftData models, plus explicit value types for currency, units, snapshots, and drafts. Calculation engines live in fillo/Services and are pure and deterministic, including RunningCostEngine, CostAheadEngine, ReminderEngine, EfficiencyEngine, OwnershipInsightsEngine, VehiclePassportGenerator, and BackupCodec. Views live in fillo/Features. Verification is 28 unit test files plus one UI test that drives the offline core loop.

Two working rules mattered more than any framework choice.

The product specification in docs/offline-paid-product-spec.md is the only source of truth in the repo, and it is written as a contract for AI coding agents: if the code and the spec disagree, the spec wins. Decisions listed as open stay unset, and the agent asks rather than guessing. That kept scope from drifting during long implementation passes.

Every financial engine is pure, so I could verify the numbers without a simulator UI. A test asserts the totals a user sees, not the internals of a date type.

Fillo Assist, the assisted entry layer, runs on Apple's on-device Foundation Models with Vision for OCR. It returns a reviewable draft plus a list of missing required fields. It cannot save a record and cannot compute a financial result.

Challenges I ran into

The biggest one was a product pivot. The first version was a free download with a trial, an unlock, and access control. I replaced that with a paid-upfront model, which meant deleting about 12,000 lines in a single pass: AppState.swift, Views+Paywall.swift, the monetization service, and the paywall screens. Deleting working code is harder than writing it, and it was the right decision.

Migrating existing data. Anyone who used the earlier build has records in the old store, so LegacyStoreMigrator and LegacyUpgradeService move that data into the new schema, with MigrationTests covering the path.

A record counted twice. DateInterval.contains includes both endpoints, so a record sitting exactly on the cutoff between two comparison periods was counted in the current window and the previous window. I added a half-open membership predicate, containsHalfOpen, and wrote a regression test that asserts the two period totals directly, 100 and 0, instead of testing the framework type.

Honest forecasts. Cost Ahead has to be clear about what it does not know. The sampling window, minimum history span, and outlier thresholds are still open product decisions, so they stay in one isolated defaults enum rather than quietly becoming policy.

Flaky tests. Parallel simulator clones produced timing-sensitive failures in PrivacyLockServiceTests that had nothing to do with the code under test. The suite now runs non-parallel on a single booted device, and a recorded run had 210 tests passing with none failing.

Currency discipline. The rule that different currencies are never summed together forced currency to be explicit in the value types and in every aggregation path. More work up front, and it removes an entire class of wrong numbers.

Accomplishments that I'm proud of

The core loop works with no network path at all. There is no backend to run, no account to create, and no analytics SDK in the build.

The financial engines are deterministic and covered by tests: running cost, cost ahead, efficiency, reminders, insights, passport generation, and backup encoding.

Vehicle Passport and Fillo Backup landed as designed. The backup restores, CSV export is documented as not being a complete backup, and destructive operations preserve data before replacing it.

Recoverability is a default, not an afterthought. Recently Deleted holds records for 30 days, and restore paths were tested rather than assumed.

Accessibility is part of done: VoiceOver support for core tasks, Dynamic Type, 44 by 44 point targets, Reduce Motion, and Privacy Mode that also protects accessibility values.

The submission package is real. Listing copy with character counts verified by scripts/verify_app_store_assets.sh, screenshots generated from deterministic demonstration data, privacy answers prepared, and a release blocker checklist that lists what remains unchecked instead of pretending it is finished.

What I learned

Delete the layer you no longer believe in. Keeping the paywall around "just in case" would have cost more than removing it.

Boundary rules belong to the product. Whether a record on a cutoff counts once is a product decision, and DateInterval made that decision for me by default and got it wrong. The fix lives in the product layer, and the test asserts the user-visible totals.

Optional AI needs a narrow job. Fillo Assist drafts an entry and names what is missing. It never stores a fact or computes a total. That boundary is what makes on-device AI safe to ship and easy to explain.

The domain model is the cheapest place to be strict. Currency and units as types prevent mixed-currency sums structurally, instead of relying on review to catch them.

Test determinism is a feature. A flaky suite trains you to ignore failures.

Write the spec first and let it win. It is read by agents and by future me, and its open decisions list stopped me from inventing policy I had not approved.

What's next for Fillo: Car Expense Tracker

Version 1.1 is an included update, free to everyone who bought 1.0. The approved scope is CSV import from official templates, optional password-protected backups, Ownership Cash Flow, Net Cost of Ownership, Fillo Assist Quick Log, on-device OCR extraction, plain-language insight explanations, voice input, Lock Screen widgets, Control Center control, Action Button support, Siri and Shortcuts through App Intents, and Spotlight actions.

None of that changes the model. No account, no subscription, no network requirement.

Before launch I still have to close the open decisions rather than guess at them: launch price (₱299 is proposed, not confirmed), Family Sharing, whether iPad ships in the first release, final visual design, and final privacy and support copy. On the App Store side, the app record, the price, and the storefront list are still unchecked items.

Built With

Share this project:

Updates

Submission history