Booking is a UC Davis application built from web-app-template, with a .NET 10 backend and React frontend.
- Repository: ucdavis/booking.
- Local development uses a dedicated
booking_devcontainerSQL container on127.0.0.1:14335, with databaseBooking. - To run before Entra is configured, copy
server/.env.exampletoserver/.envand setAuth__UseLocal="true". SetDevelopmentData__SeedOnStartup="true"to load the sample weather records. - Test targets
rg-booking-testusingAPP_NAME=bookingand the sharedDefaultPlan2plan. Its public URL is https://booking-test.ucdavis.edu. - Production targets
rg-booking-produsingAPP_NAME=bookingand the sharedNibblerplan. Its public URL is https://booking.ucdavis.edu. - Pushes and pull requests run validation. Automatic test deployment is enabled with
AZURE_TEST_READY=true. - The starter's sample routes, weather schema, and notification examples remain as development references. Replace them as Booking's application features are implemented. Application roles still use the starter's sample policy in
UserService.cs. - GA4, SMTP, and external telemetry are not configured.
See the customization guide for the remaining setup steps.
- Backend: .NET 10 Web API with ASP.NET Core
- Frontend: React 19 with Vite, TypeScript, and TanStack Router/Query/Table
- Authentication: See Auth Configuration for the default Entra flow and optional local sign-in
- Styling: Tailwind CSS
- Development: Hot reload for both frontend and backend
- Development Integration: ASP.NET Core
SpaProxylaunches Vite for Visual Studio users, while Vite proxies API and auth routes back to ASP.NET Core during development
With Docker and Compose installed, choose a unique project name for this checkout or worktree, then run these commands from its root:
export SANDBOX_PROJECT=booking-sandbox
export SANDBOX_PORT=5280
export SANDBOX_MAIL_PORT=8025
docker compose -p "$SANDBOX_PROJECT" -f .devcontainer/docker-compose.sandbox.yml up --build --waitUse a distinct SANDBOX_PROJECT for every checkout and different app and inbox ports for sandboxes running concurrently. Keep these same values for every command that manages this sandbox, including in a new terminal. Compose rejects an unset or empty SANDBOX_PROJECT. See multiple sandboxes for a second worktree example.
Open the sandbox and choose Sign in as Sample User. The image builds the current checkout's React app and .NET server. SQL Server starts first, then the app applies migrations and seeds ten weather records dated January 1–10, 2025. No host Node, .NET, .env, Entra registration, or SMTP account is needed. The first build needs internet access to download images and dependencies.
The local inbox captures mail from the Notification page. Use Basic User at local sign-in to test the weather API's 403 response, or sign out there. Sample User and Basic User have fixed identities and claims. The Sign in as a person option also accepts an exact email, IAM ID, or Kerberos ID from the sandbox's People table.
Restarting preserves database changes. To return to the original fixtures, remove this sandbox's containers and volumes, then start it again:
docker compose -p "$SANDBOX_PROJECT" -f .devcontainer/docker-compose.sandbox.yml down --volumes
docker compose -p "$SANDBOX_PROJECT" -f .devcontainer/docker-compose.sandbox.yml up --build --waitThis deletes the sandbox database and local sign-in keys. It does not affect the regular development database. Rebuild with up --build --wait after editing source. See the sandbox guide for ports, logs, and agent use.
-
Clone the repository
git clone https://github.com/ucdavis/booking/ cd booking -
Configure authentication before starting
Copy the local configuration example:
cp server/.env.example server/.env
Choose an authentication mode under Auth Configuration and configure it in
server/.envbefore starting the backend. This step also applies before opening the DevContainer.SMTP and external telemetry are optional. Leave their example settings disabled until you configure those services.
-
Open In DevContainer
- Open the project folder in Visual Studio Code.
- Click the prompt to open in container (or manually select from the command palette).
Using the DevContainer is optional, but it will get you the right version of dotnet + node, plus install all dependencies and setup a local SQL instance for you
-
Start the application
Inside DevContainer: After the authentication configuration above,
postStartCommandstarts the application automatically.Outside DevContainer (command line):
Prerequisites:
- .NET 10 SDK
- Node.js 22.18+ (includes npm)
- Docker (for the local SQL Server container)
With nvm, run
nvm installandnvm usefrom the repo root to use the supported Node 22 line.On macOS, if .NET is installed under
/usr/local/share/dotnetbutdotnetis not on your shell's PATH, runexport PATH="/usr/local/share/dotnet:$PATH"before the commands below.Install dependencies and start the app:
npm ci cd client && npm ci && cd .. dotnet tool restore npm run db:up npm start
npm run db:upstarts the SQL Server container from the same Compose file used by the DevContainer.npm startstarts the .NET backend on port5165with a CLI-specific launch profile, waits for health check, and then starts the Vite dev server on port5173which opens the browser.Visual Studio (Windows):
Prerequisites:
- Visual Studio 2026 version 18.0 or later (for
net10.0support) - Node.js 22.18+ (includes npm)
- Docker (for the local SQL Server container)
Install dependencies and start the database:
npm ci cd client && npm ci && cd .. dotnet tool restore npm run db:up
Then open
booking.sln, set theserverproject as the startup project, and pressF5.SpaProxystarts Vite if needed and redirects the browser to the frontend dev server.Visual Studio Code:
Prerequisites:
- .NET 10 SDK
- Node.js 22.18+ (includes npm)
- Docker (for the local SQL Server container)
Install dependencies and start the database:
npm ci cd client && npm ci && cd .. dotnet tool restore npm run db:up
Then open the repo root in VS Code, install the recommended extensions when prompted (at minimum the Microsoft C# extension), choose
Full Stack: VS Codein Run and Debug, and pressF5. VS Code builds and launches the backend with thehttp-clilaunch profile, starts Vite after the backend health check passes, and opens the app in your default external browser athttp://localhost:5173. For backend-only debugging, chooseBackend: ASP.NET Core + Swagger. -
Access the application
In development, the frontend runs from http://localhost:5173 and proxies backend requests to ASP.NET Core on http://localhost:5165.
- Main App: http://localhost:5173
- Backend API: http://localhost:5165/api/*
- API Documentation (Swagger): http://localhost:5165/swagger
- Health Check: http://localhost:5165/health
- Visual Studio F5: launches through the backend profile, then redirects to the Vite dev server on
:5173
The backend requires a SQL Server connection string.
Startup always applies migrations. Sample weather data is inserted only when DevelopmentData__SeedOnStartup=true, and only if the weather table is empty. The Docker sandbox sets this flag. For ordinary development, opt in through server/.env when you want the sample records.
- Outside DevContainer, the default development connection points to the SQL Server container published on
localhost:14335. - Inside DevContainer,
devcontainer.jsonoverridesDB_CONNECTIONto use the internal Docker hostnamesql:1433.
When you want to specify your own DB connection, provide it by setting the DB_CONNECTION environment variable (for example in a .env file) or by updating ConnectionStrings:DefaultConnection in appsettings.*.json (.env is recommended)
To run only the database outside DevContainer:
npm run db:upThis runs the sql service from .devcontainer/docker-compose.yml and exposes SQL Server on localhost:14335.
Useful companion commands:
npm run db:logsto watch SQL Server startup logsnpm run db:downto stop the container when you're done
By default, the app uses OIDC with Microsoft Entra ID (Azure AD). In this mode, set Auth__ClientId in server/.env to your app registration's client ID before starting the backend. Startup rejects the template's placeholder so copied projects cannot accidentally authenticate as the template app.
The Docker sandbox enables local sign-in with Auth__UseLocal=true, bypassing Entra configuration. To use it in ordinary development, set the same flag in server/.env. The flag defaults to false, and startup rejects it outside the Development environment.
Local sign-in keeps the Sample User and Basic User choices and adds Sign in as a person. Enter an exact email, IAM ID, or Kerberos ID from People; if more than one person matches, use their unique IAM ID. The person must be active in IAM, and an existing inactive application user cannot sign in. No password is required in this development-only mode. The local identity uses the person's IAM ID, name, and email, receives the User application role, and uses their existing database site-admin flag and team memberships. Signing in creates their Users row if needed without creating team memberships or granting extra permissions, except for any explicitly configured development admin IAM IDs described below.
For a new application registration, redirect URIs, and app-specific auth settings, follow the customization guide.
Each login creates or updates a Users row by IAM ID, refreshing the name, email, and login timestamps while preserving application-managed flags. Sign-in requires a name and the ucdPersonIAMID claim and fails if the user record cannot be saved. To configure the IAM claim, follow Authentication.
To grant yourself or other users admin status during development, add a comma-separated list of IAM IDs to server/.env:
DevelopmentData__AdminIamIds="123456789,987654321"Restart the backend and sign in again. In the Development environment, a matching IAM ID sets Users.IsAdmin to true when the user record is created or updated at login. The record does not need to exist beforehand. Matching uses complete, case-sensitive IAM IDs; surrounding spaces and empty entries are ignored. This setting is independent of sample-data seeding and is ignored outside Development. It leaves IsActive and other users' admin flags unchanged. Removing an ID or clearing the setting does not revoke an admin flag already saved in the database.
For local sandbox users, the IAM IDs are sandbox-10001 (Sample User) and sandbox-10002 (Basic User). This setting updates the database admin flag; the template's fixed User and SampleRole claims stay unchanged. The Docker sandbox excludes host .env files, so pass DevelopmentData__AdminIamIds to the app container's environment when using Docker.
Booking retains the starter's route-change tracking helper:
- Add the GA bootstrap script in
client/index.htmlwhen analytics is configured - Route-change page view tracking is in
client/src/shared/analytics/AnalyticsListener.tsx
The GA bootstrap script is disabled until Booking has a real measurement ID. Add the script to client/index.html with that ID in both places:
https://www.googletagmanager.com/gtag/js?id=...gtag('config', '...')
The health check endpoint (/health) is configured to return the status of the application and its dependencies. It includes a database health check to ensure the SQL Server connection is healthy. See Health Checks.
GitHub Actions is the primary deployment path. Start with the Azure deployment guide; it links to the detailed bootstrap instructions and describes the production SQL networking prerequisite.
Cloud deployments use APP_NAME=booking, with rg-booking-test and rg-booking-prod in their respective subscriptions. Shared plans remain DefaultPlan2 and Nibbler. Production SQL permits the App Service’s possible outbound addresses through individual firewall rules. See Azure deployment for rebuild and DNS cutover status.
For GitHub Environments, the one-time OIDC bootstrap, deployment settings sync, local deploy scripts, and first-deploy caveats, see Azure Deployment Setup. For the hosting flow and key deployment files, see Development Architecture.
In development, ASP.NET Core runs on port 5165, Vite serves the frontend on port 5173, and Vite proxies backend routes to ASP.NET Core. Visual Studio uses SpaProxy to start Vite and redirect the browser to it.
For request-flow diagrams, production behavior, and key file responsibilities, see Development Architecture.
The backend is configured with hot reload via dotnet watch. Any changes to C# files automatically restart the server. Visual Studio users can also run the server project directly with SpaProxy.
The frontend uses Vite's hot module replacement (HMR). Changes to React components, TypeScript files, and CSS are reflected immediately by the Vite dev server.
The repository includes .vscode/launch.json and .vscode/tasks.json so the standard VS Code workflow works out of the box:
Full Stack: VS Codelaunches the backend debugger, starts the Vite dev server, and opens the frontend in your default external browser.Backend: ASP.NET Core + Swaggerlaunches only the backend and opens Swagger when Kestrel is ready.
The VS Code flow intentionally uses the http-cli launch profile instead of the SpaProxy profile so terminal and editor-driven debugging both avoid the duplicate browser-launch behavior from the ASP.NET Core side.
- Frontend routes requiring authentication redirect to the backend's login endpoint
- Backend uses the mode described under Auth Configuration
- Upon successful authentication, a same-site cookie is set
- Frontend API calls automatically include the authentication cookie
- Backend validates the cookie for protected endpoints
After sign-in, /login?returnUrl=... accepts only local paths such as /fetch?sort=date. Missing or external destinations fall back to /.
- Run
cd client && npm test -- --runto execute the Vitest suite once. - Use
npm run test:watchinsideclient/for red/green feedback while you work. - Tests run against a jsdom environment with Testing Library so you do not need the backend running.
- Run
dotnet testfrom the repository root to execute the .NET test project included inbooking.sln. - Alternatively, target the project directly with
dotnet test tests/server.tests/server.tests.csproj. - The tests use EF Core's in-memory provider (see
tests/server.tests/TestDbContextFactory.cs) so no SQL Server instance is required.
- JavaScript/TypeScript packages: run
npm outdatedat the repository root and insideclient/to see what can be updated. Usenpm updatein each location for compatible updates, ornpm install <package>@latestwhen you need to jump to a new major version. - After updating Node packages, reinstall if needed (
npm install,cd client && npm install) and rerun key checks likecd client && npm run lint,cd client && npm test, anddotnet test.
.Net is a bit more complicated, but we're going to use the dotnet-outdated tool to help.
Run the following command from the repository root:
dotnet-outdated
and it'll show you a nice table of what can be updated. Be careful when updating major versions, especially with packages that are pinned to the .net version.
You can update individual packages or you can use the --upgrade flag to update all at once. Here's a nice way to do it and only update minor/patch versions:
dotnet-outdated --upgrade --version-lock Major
If you update Microsoft.EntityFrameworkCore.Design or another package that a tool depends on, you'll want to update that tool as well to match, ex: dotnet tool update dotnet-ef --local --version 10.0.1. That will update it for you but also set the value in our dotnet-tools.json so it's consistent for everyone.
And as always, after updating dependencies, make sure to run dotnet build and dotnet test to verify everything is working.
.
├── client/ # React frontend
│ ├── src/
│ │ ├── routes/ # TanStack Router routes
│ │ ├── queries/ # TanStack Query hooks
│ │ ├── lib/ # API client and utilities
│ │ └── shared/ # Shared components
│ ├── package.json
│ └── vite.config.ts
├── server/ # .NET backend
│ ├── Controllers/ # API controllers
│ ├── Helpers/ # Utility classes
│ ├── Properties/ # Launch settings
│ ├── Program.cs # Application entry point
│ └── server.csproj # SpaProxy + publish integration
├── infrastructure/azure/ # Azure Bicep templates and local deployment scripts
├── .github/workflows/ # CI/CD and reusable Azure App Service deployment workflow
├── package.json # Root dev orchestration scripts
└── booking.sln # Visual Studio solution file
npm start- Starts both backend and frontend with hot reloadnpm run start:server- Starts only the ASP.NET Core backendnpm run start:client- Starts only the Vite dev server
npm run dev- Start Vite development servernpm run dev:open- Start Vite development server and open the browsernpm run build- Build for productionnpm run lint- Run ESLintnpm run preview- Preview production buildnpm test- Run tests
dotnet run- Start the .NET applicationdotnet watch- Start with hot reloaddotnet build- Build the applicationdotnet test- Run tests