Files
2026-05-05 07:38:31 -05:00

5.8 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Commands

Backend (ASP.NET Core)

# Run API (must listen on port 5000 for the Vite proxy to work)
cd src/Budget.Api
ASPNETCORE_URLS=http://localhost:5000 dotnet run

# Build
dotnet build src/Budget.Api/Budget.Api.csproj

# Add an EF Core migration
dotnet ef migrations add <Name> --project src/Budget.Infrastructure --startup-project src/Budget.Api

# Remove last migration (before it has been applied)
dotnet ef migrations remove --project src/Budget.Infrastructure --startup-project src/Budget.Api

Frontend (Vite/React)

cd src/Budget.Client
npm run dev       # dev server at http://localhost:5173
npm run build     # tsc + vite build (what CI and Docker use)
npm run lint      # eslint

Docker

.\deploy.ps1   # builds image, pushes to docker.stwaddle.com/budget:latest, and deploys to prod via SSH

Architecture

The app is a single Docker container: the .NET API serves the compiled React SPA as static files and also handles all /api requests. In development the two processes run separately; Vite proxies /api to http://localhost:5000.

Project layout

src/
  Budget.Core/          # Domain models, DTOs, FrequencyCalculator — no EF, no ASP.NET
  Budget.Infrastructure/# EF Core DbContext, migrations, BudgetAuthorizationService
  Budget.Api/           # ASP.NET Core controllers, middleware, Program.cs
  Budget.Client/        # Vite/React SPA

Data model

  • Budget (owner, name) → owns Incomes, Outgos, BudgetShares
  • All entities use soft delete (IsDeleted / DeletedAt) with a global EF query filter — hard deletes are not used
  • Budget uses PostgreSQL's xmin shadow property as an EF concurrency token
  • BudgetShare links a budget to another user by email; IsPending = true until the target user logs in (resolved by KnownUserMiddleware)
  • Frequency is a C# enum (Monthly, Biweekly, Annually, etc.); FrequencyCalculator converts any frequency to monthly/annual equivalents
  • The 50/30/20 rule is hardcoded in SummaryController: Need=50%, Want=30%, Save=20%

API conventions

  • All controllers require [Authorize(Roles = "admin,user")] — tokens that lack a role claim matching either value get a 403
  • RoleClaimType = "role" and NameClaimType = "sub" are set in JWT options; role checks work with User.IsInRole()
  • BudgetAuthorizationService centralises read/write/owner checks against the budget's owner and any accepted shares — always call CanReadAsync / CanWriteAsync / IsOwnerAsync before touching budget data
  • The TryGetUserId helper pattern (returns an IActionResult? error or out-params the user id) is used consistently across all controllers
  • Enum serialisation: JsonStringEnumConverter is registered globally, so all C# enums are sent/received as strings (e.g. "Monthly", not 5)
  • Write endpoints use the "writes" rate-limit policy (30 req/min per user); reads use the global limiter (120 req/min)
  • EF migrations run automatically via MigrateAsync() at startup

Frontend conventions

  • All server state goes through TanStack Query (src/api/). Query keys follow the pattern ['budgets'], ['budgets', id], ['budgets', id, 'summary'], etc.
  • All forms use react-hook-form + zod schemas defined in src/schemas/index.ts. Never use uncontrolled inputs outside of RHF.
  • CSS design system lives entirely in src/index.css as CSS custom properties — no Tailwind, no CSS modules. Use the existing variables (--color-primary, --green-*, etc.) rather than hardcoded hex values. App.css is intentionally near-empty.
  • Icons come from lucide-react (tree-shaken per-import). Use btn-icon class for icon-only buttons; always add a title attribute for accessibility.
  • Inline table editing: rows show read-only data; clicking a cell enters edit mode in-place using a useState(editing) toggle. New rows use a separate input row appended to <tbody> (not a modal).
  • Sorting: SortableHeader component and SortState type in src/components/SortableHeader.tsx. Sorting is client-side only and derives a view from the drag-ordered displayItems array — clearing sort restores drag order. Drag handles are disabled while a sort is active.
  • Frequency calculations for real-time previews are in src/utils/frequency.ts, mirroring the C# FrequencyCalculator exactly.
  • Drag-and-drop ordering: @dnd-kit is used on income/outgo rows. Row order is persisted to the server. Sort handles are hidden while a column sort is active.
  • Auth: react-oidc-context wraps the whole app; AuthGuard protects all budget routes; TokenSync wires the OIDC access token into the API client on every auth state change. After OIDC callback, navigation uses window.location.replace('/') (not history.replaceState) so React Router picks up the URL change.

Local development environment

Required env vars when running the API locally (set in appsettings.Development.json or shell):

  • ConnectionStrings__DefaultConnection — Postgres connection string
  • Oidc__Audiencebudget_api
  • OTEL_EXPORTER_OTLP_ENDPOINT — optional; omit or set to skip telemetry export

Auth / OIDC

  • Authority: https://auth.stwaddle.com/ (OpenIddict-based, at src.Auth.Server in a sibling repo)
  • API audience claim: budget_api (underscore — must match Oidc__Audience env var)
  • Scope: budget_api (underscore, not hyphen)
  • Roles admin and user are granted per-user on the client registration in the auth server admin panel. A user with no role gets access_denied at the authorize endpoint.
  • Frontend OIDC config is baked into the build via VITE_* env vars; override locally in src/Budget.Client/.env.local