How to get the full stack running locally: .NET 10 API + EF Core + SQL Server + SignalR on the backend, Vue 3 + Vite + Pinia + TypeScript on the frontend, plus the quality tooling (SonarCloud, ESLint, tests). Everything here runs without any Azure resources — local equivalents are used for SQL and SignalR.
| Tool | Version | Purpose | Install |
|---|---|---|---|
| .NET SDK | 10.0 | Backend build/run/test | https://dotnet.microsoft.com/download |
| Node.js | 20 LTS | Frontend (Vite) | https://nodejs.org (or nvm install 20) |
| Git | latest | Version control | https://git-scm.com |
| Docker Desktop | latest | Local SQL Server (recommended) | https://docker.com |
| EF Core tools | matches SDK | Migrations | dotnet tool install -g dotnet-ef |
| Java (JRE/JDK) | 17 | Required by SonarScanner | https://adoptium.net |
| Sonar scanner | latest | Local static analysis | dotnet tool install -g dotnet-sonarscanner |
| Azure CLI (optional) | latest | Cloud deploy / KV / SQL | https://aka.ms/azcli |
- Backend: Visual Studio 2022 (17.12+), JetBrains Rider, or VS Code + C# Dev Kit.
- Frontend: VS Code + Vue - Official (Volar), ESLint, Prettier extensions.
- Shared: EditorConfig extension so formatting rules apply in every editor.
Verify the toolchain:
dotnet --version # 10.0.x
node --version # v20.x
npm --version
docker --version
dotnet ef --versionPinned for reproducibility: the repo root includes
global.json(locks the .NET SDK band) and.nvmrc(locks Node to 20). Runnvm usein the repo; the SDK is auto-selected byglobal.json. This keeps every machine — and CI — on the same versions.
taskflow/
├── backend/
│ ├── src/
│ │ ├── Domain/ # entities — no dependencies
│ │ ├── Application/ # use cases, DTOs, interfaces
│ │ ├── Infrastructure/ # EF Core, repos, SignalR, external services
│ │ └── Api/ # controllers + Program.cs (composition root)
│ └── tests/ # xUnit unit + integration tests
├── frontend/
│ └── src/
│ ├── api/ # axios client + endpoints
│ ├── components/ # reusable UI
│ ├── views/ # routed pages
│ ├── stores/ # Pinia stores
│ ├── router/ # Vue Router + guards
│ └── types/ # shared TS types
├── infra/ # Bicep IaC
├── .github/ # workflows, dependabot
└── docs/
Option A — SQL Server in Docker (recommended): simplest is docker compose up -d (see §6). Manual equivalent:
docker run -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=Your_strong_Pass123" \
-p 1433:1433 --name taskflow-sql \
-v taskflow-sqldata:/var/opt/mssql \
-d mcr.microsoft.com/mssql/server:2022-latestApple Silicon (M-series Macs): the image above has no native ARM64 build — use Azure SQL Edge instead (same env vars/port): swap the image for
mcr.microsoft.com/azure-sql-edge:latest. The-vnamed volume persists data across container restarts.
Local connection string:
Server=localhost,1433;Database=taskflowdb;User Id=sa;Password=Your_strong_Pass123;TrustServerCertificate=True;
Option B — SQL Server LocalDB (Windows only):
Server=(localdb)\MSSQLLocalDB;Database=taskflowdb;Trusted_Connection=True;
Cloud uses managed identity (no password). Local uses SQL auth — that's expected. Never commit local credentials; use user-secrets below.
The API reads ConnectionStrings:Default, Jwt:SigningKey, and SignalR config. Store them with the .NET Secret Manager:
cd backend/src/Api
dotnet user-secrets init
dotnet user-secrets set "ConnectionStrings:Default" "Server=localhost,1433;Database=taskflowdb;User Id=sa;Password=Your_strong_Pass123;TrustServerCertificate=True;"
dotnet user-secrets set "Jwt:SigningKey" "$(openssl rand -base64 48)"Windows PowerShell (no
openssl):dotnet user-secrets set "Jwt:SigningKey" ([Convert]::ToBase64String((1..48 | % { Get-Random -Max 256 })))
Production runs Azure SignalR in Default mode, which uses normal server-side Hubs — so local in-process SignalR mirrors production exactly. No emulator, /negotiate shim, or upstream webhooks needed.
- Locally, keep
Azure:SignalR:Enabled=false(an app-implemented flag) so the API uses in-process ASP.NET Core SignalR:services.AddSignalR(); - In the cloud the same Hub code runs through Azure SignalR via
services.AddSignalR().AddAzureSignalR();— the only difference is the connection string. Your Hub classes and client code are identical in both environments.
This dev/prod parity is the whole reason the infra uses Default mode rather than Serverless.
cd backend
dotnet dev-certs https --trust # one-time: trust the local HTTPS dev cert
dotnet restore
dotnet ef database update --project src/Infrastructure --startup-project src/Api
dotnet run --project src/Api- API:
https://localhost:7xxx· Swagger UI:/swagger /health— liveness, DB-free (this is what the keep-warm pinger hits, so it never resumes a paused DB)./health/ready— readiness, checks SQL (returns unhealthy if the database is down). Don't collapse the two into one DB-free check, or an outage looks healthy.
dotnet ef migrations add <Name> --project src/Infrastructure --startup-project src/ApiFollow the expand-contract rule (see devops-setup.md §4): additive/backward-compatible changes only in a release; drops come in a later release.
dotnet test backend
# with coverage (same as CI)
dotnet test backend --collect:"XPlat Code Coverage;Format=opencover"Vite exposes only vars prefixed VITE_. Create frontend/.env.local (git-ignored):
VITE_API_BASE_URL=https://localhost:7xxx
VITE_SIGNALR_HUB_URL=https://localhost:7xxx/hubs/board
⚠️ VITE_variables are NOT secret. Vite inlines them into the client bundle shipped to the browser, so anyone can read them. Use them only for public config (URLs, feature flags). Never put a JWT key, API secret, or connection string in aVITE_var.
Commit a frontend/.env.example with the same keys and placeholder values.
cd frontend
npm ci # clean install from package-lock
npm run dev # Vite dev server, hot reload → http://localhost:5173- Vite — dev server + build tool.
- Vue 3 with
<script setup>(Composition API). - Pinia — state (auth store, board store). Devtools-friendly.
- Vue Router — routes + navigation guards (redirect unauthenticated).
- Axios —
src/api/client.tswith an interceptor attaching the JWT and handling 401s. - @microsoft/signalr — real-time client; connects to
VITE_SIGNALR_HUB_URL. - TypeScript —
vue-tscfor type-checking. - Styling: Tailwind / PrimeVue / Vuetify (pick one, stay consistent).
npm run dev # start dev server
npm run build # type-check + production build → dist/
npm run preview # serve the production build locally
npm run lint # ESLint
npm run format # Prettier
npm run test # Vitest (unit/component tests)The API must allow the Vite origin. In Program.cs (Development), configure CORS in code:
builder.Services.AddCors(o => o.AddPolicy("dev", p => p
.WithOrigins("http://localhost:5173")
.AllowAnyHeader().AllowAnyMethod()
.AllowCredentials())); // required for SignalRCORS is owned in code, not at the Azure App Service level (see
devops-setup.md§6).
Alternative — skip CORS entirely in dev with a Vite proxy. The browser then talks same-origin to the Vite server, which forwards to the API (also avoids the self-signed-cert prompt):
// vite.config.ts
export default defineConfig({
server: {
proxy: {
'/api': { target: 'https://localhost:7xxx', changeOrigin: true, secure: false },
'/hubs': { target: 'https://localhost:7xxx', changeOrigin: true, secure: false, ws: true }
}
}
})Mirrors the CI quality gate. Needs Java 17 + a Sonar token.
export SONAR_TOKEN=<your-token>
dotnet sonarscanner begin \
/k:"<project-key>" /o:"<org>" \
/d:sonar.host.url="https://sonarcloud.io" \
/d:sonar.token="$SONAR_TOKEN" \
/d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml"
dotnet build backend -c Release
dotnet test backend --collect:"XPlat Code Coverage;Format=opencover"
dotnet sonarscanner end /d:sonar.token="$SONAR_TOKEN"CI runs the same with sonar.qualitygate.wait=true, so a failing gate fails the build.
- Backend:
.editorconfigat the repo root drives C# style;dotnet formatto auto-fix. - Frontend: ESLint (
eslint.config.js) + Prettier. Runnpm run lintbefore pushing. - (Optional) Husky + lint-staged pre-commit hook to run
dotnet formatandnpm run linton staged files.
These run in CI only (GitHub-hosted) — nothing to install locally. See .github/workflows/codeql.yml and .github/dependabot.yml.
Golden path — one command for dependencies (uses docker-compose.yml at the repo root):
docker compose up -d # starts SQL Server with a persistent named volumeThen run the apps on the host:
cd backend && dotnet run --project src/Api # terminal 1 (or: dotnet watch)
cd frontend && npm run dev # terminal 2Open http://localhost:5173. SignalR runs in-process locally (Default-mode parity with prod), so there's nothing extra to start.
Stop dependencies with
docker compose down(add-vto also wipe the DB volume).
Backend (user-secrets in dev / App Settings + Key Vault in cloud)
| Key | Dev value | Cloud value |
|---|---|---|
ConnectionStrings:Default |
local SQL (sa) | Authentication=Active Directory Managed Identity |
Jwt:SigningKey |
user-secret | Key Vault reference |
Azure:SignalR:Enabled |
false (or emulator) |
true |
Azure:SignalR:ConnectionString |
emulator string | AuthType=azure.msi |
APPLICATIONINSIGHTS_CONNECTION_STRING |
(empty) | from Bicep |
Frontend (.env.local)
| Key | Example |
|---|---|
VITE_API_BASE_URL |
https://localhost:7xxx |
VITE_SIGNALR_HUB_URL |
https://localhost:7xxx/hubs/board |
| Symptom | Fix |
|---|---|
dotnet ef not found |
dotnet tool install -g dotnet-ef; ensure ~/.dotnet/tools is on PATH |
| Cannot connect to SQL in Docker | container running? docker ps; password meets complexity rules; port 1433 free |
| HTTPS dev cert warnings | dotnet dev-certs https --trust |
| CORS error in browser | API origin must list http://localhost:5173 with AllowCredentials |
| SignalR won't connect locally | use in-process mode (Azure:SignalR:Enabled=false); check the hub route + JWT on the connection |
Vite env var is undefined |
must be prefixed VITE_; restart dev server after editing .env.local |
| Sonar scanner fails to start | Java 17 installed and on PATH |
npm run build type errors |
run npm run lint and vue-tsc --noEmit to locate them |