Local-first Bike Tracking application built with .NET Aspire orchestration, .NET 10 Minimal API, F# domain modules, and a React frontend.
- Local user signup with name and PIN
- PIN protection through salted, non-reversible hashing (PBKDF2)
- Duplicate-name rejection using trimmed, case-insensitive normalization
- User identification with progressive retry delay (up to 30 seconds)
- User registration outbox with background retry until successful publication
- Manual bike expense entry with date, amount, optional note, and optional receipt
- Expense history with date filtering, inline edit, soft delete, and receipt management
- Dashboard expense summary with manual totals, oil-change savings, and net position
- src/BikeTracking.AppHost - Aspire orchestration host
- src/BikeTracking.Api - Minimal API service
- src/BikeTracking.ServiceDefaults - Shared Aspire defaults and telemetry wiring
- src/BikeTracking.Domain.FSharp - Domain event and type modules (F#)
- src/BikeTracking.Frontend - React + Vite frontend app
This project is optimized for development inside a DevContainer. All tools, runtimes, and dependencies are pre-configured.
- Install Visual Studio Code and the Dev Containers extension
- Open the repository in VS Code
- Press
Ctrl+Shift+P(orCmd+Shift+Pon macOS), type "Dev Containers: Open Folder in Container", and select it - VS Code will build and start the DevContainer (first run takes ~2-3 minutes)
- Once connected, all dependencies are ready:
- .NET 10 SDK
- Node.js 24+ with npm
- CSharpier for code formatting
- Recommended VS Code extensions pre-installed
- .NET SDK 10.x
- Node.js 24+ and npm
- CSharpier global tool (required for formatting checks):
dotnet tool install csharpier -grun it with csharpier format . from the repo root to format all C# code.
- Helpful editor integration: VS Code CSharpier extension (
csharpier.csharpier-vscode)
Once the DevContainer is connected (see Prerequisites above), all dependencies are pre-installed. Open a terminal in VS Code and run:
dotnet run --project src/BikeTracking.AppHostThe Aspire AppHost will:
- Build the entire solution
- Start the API service
- Start the React frontend (compiled)
- Open the Aspire Dashboard at
http://localhost:19629
From the dashboard, launch the frontend and API services.
- Install frontend dependencies:
cd src/BikeTracking.Frontend
npm install- Run the full local app through Aspire:
cd ../..
dotnet run --project src/BikeTracking.AppHost- Open Aspire dashboard and launch:
- frontend service for the signup and identify screen
- api service for local identity endpoints
https://aspire.dev/whats-new/aspire-13-2/#-upgrade-to-aspire-132
aspire update --self
aspire update
https://code.visualstudio.com/remote/advancedcontainers/sharing-git-credentials was very helpful.
Set-Service ssh-agent -StartupType Automatic Start-Service ssh-agent Get-Service ssh-agent
I did this to avoid getting commits from the wrong user. This is optional for you. If you have the straight forward setup, you don't need to mount ~/.ssh as the Forwarding should work fine.
The DevContainer mounts your host ~/.ssh directory read-only at /root/.ssh-host, then copies it to /root/.ssh with correct permissions during postCreateCommand. This is necessary because SSH rejects config files with world-writable permissions (common on Windows-mounted filesystems).
The SSH config can be set up for two GitHub accounts — a personal account and a company account — using named hosts:
Host github.com-personal
HostName github.com
IdentityFile ~/.ssh/youruser_github
Host github.com
HostName github.com
IdentityFile ~/.ssh/yourcompany_github
Use the appropriate host alias when cloning:
# Personal repos
git clone git@github.com-personal:your-username/your-repo.git
# Company repos
git clone git@github.com:omnitech-org/your-repo.gitVerify connectivity:
ssh -T git@github.com-personal
ssh -T git@github.com- GET / - API status
- POST /api/users/signup - create local user record and queue UserRegistered event
- POST /api/users/identify - authorize user by normalized name and PIN
- This slice is local-only and intentionally excludes OAuth and Azure hosting.
- Name and PIN are validated on client and server.
- PIN plaintext is never stored or emitted in events.
- Future cloud and OAuth expansion will be delivered in a separate feature.
For local-first deployment to end-user machines, the default persistence model is a local SQLite file.
- No separate database installation or database service is required.
- The API currently defaults to a local SQLite file named biketracking.local.db.
- Startup applies EF Core migrations automatically to create or update schema.
- For packaged installs, place the SQLite file in a user-writable application-data folder rather than the application install directory.
- Before schema upgrades, create a safety backup copy of the SQLite file.
- Use SQL Server LocalDB or SQL Server Express only when local multi-user requirements exceed the single-user SQLite profile.
The expense tracking slice adds a full local-first workflow for bike ownership costs.
- Record manual expenses with required date and amount.
- Attach an optional receipt in JPEG, PNG, WEBP, or PDF format up to 5 MB.
- Browse expense history with date filters, inline edit, delete, and receipt replacement/removal.
- View dashboard totals that combine manual expenses with automatic oil-change savings.
Receipt upload failures are handled as non-fatal storage errors when recording a new expense. If receipt storage fails because of a non-writable path, permission issue, or disk/storage error, the expense is still saved and the UI shows that the receipt was not attached.
Receipt files are stored in a receipts/ folder next to the configured SQLite database file.
- Default development database:
src/BikeTracking.Api/biketracking.local.db - Default development receipt root:
src/BikeTracking.Api/receipts/ - If
ConnectionStrings:BikeTrackingpoints to a different SQLite path, the receipt root moves with it.
The storage rule is:
- Database:
/path/to/biketracking.local.db - Receipts:
/path/to/receipts/
For packaged installs, configure the SQLite database in a user-writable app-data directory and allow the app to create the sibling receipts/ directory there.
Suggested packaged-install locations:
- Windows:
%LocalAppData%/CommuteBikeTracker/biketracking.local.dband%LocalAppData%/CommuteBikeTracker/receipts/ - macOS:
~/Library/Application Support/CommuteBikeTracker/biketracking.local.dband~/Library/Application Support/CommuteBikeTracker/receipts/ - Linux:
${XDG_DATA_HOME:-~/.local/share}/CommuteBikeTracker/biketracking.local.dband${XDG_DATA_HOME:-~/.local/share}/CommuteBikeTracker/receipts/
When deploying outside development, prefer setting the database path explicitly with configuration such as ConnectionStrings__BikeTracking so both the database and receipt root land in the intended writable directory.
Back up the SQLite database file and the sibling receipts/ directory together. Keeping only one of them can leave expense records pointing at missing attachments or leave orphaned receipt files with no matching expense rows.
Recommended backup workflow:
- Stop the application.
- Copy
biketracking.local.db. - Copy the entire
receipts/directory from the same parent folder. - Store both copies together with the same timestamp.
Recommended restore workflow:
- Stop the application.
- Restore
biketracking.local.dbto the configured data directory. - Restore the matching
receipts/directory to the same parent folder. - Start the application and verify expense history plus a few receipt downloads.
If the application is upgraded and migrations are about to run, make the backup before first startup on the new version.
frontend unit tests: npm run test:unit (Vitest)
frontend end-to-end tests: npm run test:e2e (Playwright)
- These use the local SQLlite database, so they are more like integration tests. The values are thrown away after each test, but they do test the full stack of the API and database layers.
backend tests: dotnet test from repo root (xUnit)
formatting: dotnet csharpier format .
frontend lint: cd src/BikeTracking.Frontend && npm run lint
These are ran in the .github\workflows\ci.yml pipeline on every PR