Local-first commute tracking app using .NET Aspire, Minimal API, F# domain, and React 19 frontend. Built for end-user machines (SQLite-based, no cloud infrastructure required).
Mandatory: Use DevContainer (all tooling pre-configured).
Ctrl+Shift+P→ "Dev Containers: Open Folder in Container"- Once connected, all dependencies ready (.NET 10 SDK, Node 24+, npm, CSharpier)
Start the app:
dotnet run --project src/BikeTracking.AppHostAspire Dashboard opens at http://localhost:19629 — launch frontend and API from there.
- Full solution tests:
dotnet test BikeTracking.slnx - Single test project:
dotnet test src/BikeTracking.Api.Tests/BikeTracking.Api.Tests.csproj - Restore deps:
dotnet restore BikeTracking.slnx - Code formatting:
csharpier format .(run from repo root; required before commits) - Watch mode (API):
dotnet watch --project src/BikeTracking.Api(auto-rebuild on changes)
From src/BikeTracking.Frontend:
- Dev server:
npm run dev(HMR on http://localhost:5173) - Build:
npm run build - Lint:
npm run lint(ESLint + Stylelint) - Unit tests:
npm run test:unit(Vitest; use--uiflag for interactive mode) - E2E tests:
npm run test:e2e(Playwright; runs against live API/DB). You must start the application with Aspire before running E2E tests. - Watch unit tests:
npm run test:unit:watch
- Full CI pipeline: Run locally before pushing:
dotnet test BikeTracking.slnx && cd src/BikeTracking.Frontend && npm run lint && npm run build && npm run test:unit && npm run test:e2e - Tests are run in
.github/workflows/ci.yamlon all PRs and pushes to main
- BikeTracking.AppHost — .NET Aspire orchestration; starts API, frontend, and dashboard for local dev
- BikeTracking.Api — Minimal API (C#); handles routes, EF Core migrations, outbox publishing
- BikeTracking.Api.Tests — xUnit backend tests
- BikeTracking.Domain.FSharp — Domain logic (F#): discriminated unions for events, pure functions for state transitions, immutable value objects
- BikeTracking.Frontend — React 19 + Vite + TypeScript; form-driven UI with react-router-dom v7 for navigation
- BikeTracking.ServiceDefaults — Shared Aspire telemetry and OpenTelemetry wiring (all services configured via this)
- SQLite (local user-machine deployment; no database service needed)
- EF Core Code-First migrations auto-applied on startup
- Outbox pattern: All domain events written to outbox table; background worker retries with progressive delay (up to 30s) until published
- Event sourcing: User registration, login, ride records are immutable events; current state derived from event history
F# Domain Layer (BikeTracking.Domain.FSharp):
- Pure functions: state transitions, calculations
- Discriminated unions: enforce valid event and command structures
- Immutable types: all domain state immutable by default
- No I/O; no dependencies; trivially testable
C# API Layer (BikeTracking.Api):
- Receives commands from frontend (JSON), validates with data annotations
- Calls F# domain functions; receives immutable events
- Writes events to SQLite outbox; EF Core persists
- Minimal API endpoints (no controllers)
- Dependency injection wired via Aspire
Frontend (React/TypeScript):
- Form-driven signup (name + PIN), login identify screen
- Client-side validation (required fields, format)
- Server-side validation enforced by API (defense-in-depth)
- sessionStorage for auth tokens (not persisted to disk)
- Mandatory red-green-refactor cycle: Write failing tests first, confirm failure with user, implement, validate all green
- Backend tests (xUnit) target pure domain logic (F#) — 85%+ coverage expected
- Frontend tests: unit tests (Vitest) for components; E2E tests (Playwright) for full-stack signup/login flow against live API
- E2E tests use SQLite DB, throw data away after each test (integration-like behavior)
- Always validate test failures before implementation (prove tests are meaningful, not vacuous)
- Railway Oriented Programming (Result<'T>): All domain functions return
Result<'T, Error>for explicit error handling - Discriminated unions: Commands and Events use DUs to enforce valid states (no invalid combinations)
- Active patterns: Optional patterns for complex state matching
- Immutable records: All domain types immutable; constructors enforce invariants
- Reference F# docs: https://fsharp.org/
- Data annotations on request DTOs: [Required], [StringLength], [EmailAddress], etc. (enforced by ASP.NET Core)
- Minimal API endpoints: Map HTTP verbs directly; no controller classes
- Dependency injection: Services registered in Program.cs; Aspire handles wiring
- EF Core DbContext: QueryTimeout for long-running queries; migrations auto-applied on startup
- Reference: https://learn.microsoft.com/aspnet/core/fundamentals/minimal-apis
- React 19 patterns: Hooks (useState, useEffect), functional components only
- React Router v7: useNavigate() for programmatic navigation, useParams() for route params
- Form handling: Uncontrolled forms (ref-based) or controlled components (useState); validate before submit
- Client-side validation: Required fields, format checks; must match server-side rules
- No inline CSS: Use .css files with Stylelint + ESLint rules; import in component
- Component organization: One component per file; descriptive PascalCase names; co-locate tests with components
- Reference: https://react.dev/
TypeScript Type Safety (Critical):
- NO
anytypes allowed — use explicit types for all variables, parameters, return values - Component props: Define explicit interface (e.g.,
interface SignupFormProps { onSuccess: () => void; }) - API contracts: Define TypeScript types matching backend DTOs (e.g.,
interface UserSignupRequest { name: string; pin: string; }) - React hooks: Type hook parameters and return values (e.g.,
const [name, setName] = useState<string>('')) - Form refs: Type refs explicitly (e.g.,
useRef<HTMLInputElement>(null)) - Event handlers: Type event objects (e.g.,
(e: React.FormEvent<HTMLFormElement>) => void) - Route params: Define param types (e.g.,
interface RouteParams { userId: string; }thenuseParams<RouteParams>()) - Service functions: Return typed Promises (e.g.,
async function identifyUser(name: string, pin: string): Promise<User> { ... }) - Use
unknownonly when truly dynamic; narrow with type guards - Use discriminated unions for state variants (e.g.,
type PageState = { status: 'loading' } | { status: 'success'; data: User } | { status: 'error'; message: string }) - Reference TypeScript handbook: https://www.typescriptlang.org/docs/
- No direct side effects in domain: Domain functions are pure; I/O happens at API layer
- Events are immutable: Once persisted, events never change; corrections via new events
- Outbox table: Every API write also writes to outbox; background job publishes with exponential backoff
- Idempotency: Handlers must be safe to re-execute (outbox may retry)
- Target deployment: Local user machines (Windows, macOS, Linux)
- Database: SQLite file (default:
biketracking.local.dbin app root; move to user-writable app-data folder for packaged installs) - Pre-install safety: Before schema upgrades, users should back up the SQLite file
- No separate services needed: No Docker, no separate database server, no cloud provider required
- Multi-user setup: For multi-user requirements on a single machine, consider SQL Server LocalDB or SQL Server Express (future phase)