Spencer Twaddle 489f376253 Move OIDC config to appsettings.json and add MetadataAddress
Authority, Audience, and MetadataAddress are not secrets so they belong
in committed config rather than runtime env vars. MetadataAddress points
to the internal Docker URL for JWKS fetch, avoiding nginx hairpinning;
it is blanked in Development so the JWT middleware falls back to
Authority-based discovery. RequireHttpsMetadata is disabled only when
MetadataAddress is set (internal http URL).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 15:54:39 -05:00

Budget

A multi-user budget web app built around the 50/30/20 framework. Users manage Income and Outgo entries, view a Summary breakdown by type (Need / Want / Save), and can share budgets with other users in edit or view-only mode.

Stack: ASP.NET Core 10 + Vite/React (TypeScript) — served as a single Docker container. PostgreSQL via EF Core. OIDC authentication against auth.stwaddle.com.


Environment Variables

Backend (ASP.NET Core)

Variable Required Description
ConnectionStrings__DefaultConnection Yes Full Npgsql connection string
AUTH__AUTHORITY Yes OIDC authority base URL (trailing slash required)
AUTH__AUDIENCE Yes Expected aud claim in access tokens — must match the scope's Resource value in the auth server
ASPNETCORE_ENVIRONMENT No Production or Development (default: Production in Docker)

If ConnectionStrings__DefaultConnection is absent, the app falls back to assembling a connection string from individual variables:

Variable Default Description
POSTGRES_HOST localhost Database host
POSTGRES_PORT 5432 Database port
POSTGRES_DB budget Database name
POSTGRES_USER budget Database user
POSTGRES_PASSWORD changeme Database password

Frontend (Vite — baked in at build time)

These are compiled into the static assets during the Docker build and cannot be changed at runtime.

Variable Description
VITE_AUTH_AUTHORITY OIDC authority base URL — must match AUTH__AUTHORITY
VITE_AUTH_CLIENT_ID OIDC client ID registered in the auth server
VITE_AUTH_REDIRECT_URI Full URL of the /callback route (e.g. https://budget.stwaddle.com/callback)

To change frontend auth config after the image is built, rebuild the image with updated build args. See the Dockerfile.


Production Deployment

The service is defined in docker/docker-compose.yaml and managed alongside the rest of the stwaddle.com infrastructure.

docker-compose service

budget:
  image: docker.stwaddle.com/budget:latest
  environment:
    - VIRTUAL_HOST=budget.stwaddle.com
    - LETSENCRYPT_HOST=budget.stwaddle.com
    - VIRTUAL_PORT=8080
    - ASPNETCORE_ENVIRONMENT=Production
    - ConnectionStrings__DefaultConnection=Host=apps-db;Port=5432;Database=budget;Username=postgres;Password=${APPS_DB_PASSWORD}
    - AUTH__AUTHORITY=https://auth.stwaddle.com/
    - AUTH__AUDIENCE=budget-api
  depends_on:
    - apps-db
    - auth
  networks:
    - web
    - apps-internal
  restart: unless-stopped

.env entries

Add to the .env file on the server (alongside the existing entries):

BUDGET_VIRTUAL_HOST=budget.stwaddle.com
BUDGET_LETSENCRYPT_HOST=budget.stwaddle.com

APPS_DB_PASSWORD is already set in .env for the recipes app and is shared. The budget app gets its own budget database within the same Postgres instance — EF Core's MigrateAsync() creates it automatically on first startup.

Build and push

.\build-budget-image.ps1

This builds the multi-stage Docker image (Node → .NET SDK → ASP.NET runtime) and pushes it to docker.stwaddle.com/budget:latest.

The VITE_* build args default to the production values in the Dockerfile. To override:

docker build `
  --build-arg VITE_AUTH_AUTHORITY=https://auth.stwaddle.com/ `
  --build-arg VITE_AUTH_CLIENT_ID=budget-client `
  --build-arg VITE_AUTH_REDIRECT_URI=https://budget.stwaddle.com/callback `
  -t docker.stwaddle.com/budget:latest .
docker push docker.stwaddle.com/budget:latest

DNS

Add an A record for budget.stwaddle.com pointing to the Linode server IP before deploying. The acme-companion will issue the Let's Encrypt certificate automatically. If DNS hasn't propagated when the container first starts:

docker compose restart acme-companion

Auth Server Setup

The budget app requires two registrations in the auth server admin panel at https://auth.stwaddle.com/Admin/ (requires an account with the Admin role).

1. Create the API Scope

Admin → Scopes → Create

Field Value
Name budget_api
Display Name Budget API
Resources budget-api

The Resources field is what gets written into the aud claim of access tokens. It must exactly match AUTH__AUDIENCE=budget-api set on the API container.

2. Register the Client Application

Admin → Sites → Create

Field Value
Client ID budget-client
Display Name Budget
Redirect URIs https://budget.stwaddle.com/callback
Post-Logout Redirect URIs https://budget.stwaddle.com
API Scopes budget_api

The client is always created as Public (no secret) with Authorization Code + PKCE + Refresh Token — this is enforced by the auth server for all SPA clients.

For local development, also add the dev URIs (you can edit the client after creation):

  • Redirect URI: http://localhost:5173/callback
  • Post-Logout URI: http://localhost:5173

3. Grant Users Access

Users must be explicitly granted a role on the client before they can log in.

Admin → Sites → [Budget] → Roles → Grant

Assign any user who needs access at least the user role. Without a role entry the auth server returns access_denied at the authorize endpoint.

Token details

Property Value
Flow Authorization Code + PKCE
Access token lifetime 15 minutes
Refresh token lifetime 14 days
Claims in access token sub, email, name, role, aud (budget-api)

The frontend uses oidc-client-ts with automatic silent renewal, so users stay logged in across the 15-minute access token window without re-authenticating.


Local Development

Prerequisites

  • .NET 10 SDK
  • Node 22
  • PostgreSQL (or Docker)

Setup

  1. Copy .env.example to .env and fill in values for your local Postgres instance.

  2. Start the API:

    cd src/Budget.Api
    dotnet run
    

    The API listens on http://localhost:5000 by default. EF migrations run automatically on startup.

  3. Start the frontend dev server:

    cd src/Budget.Client
    npm install
    npm run dev
    

    Vite proxies /api requests to http://localhost:5000. The app is available at http://localhost:5173.

  4. Set the following in src/Budget.Client/.env.local (Vite picks this up automatically, it is gitignored):

    VITE_AUTH_AUTHORITY=https://auth.stwaddle.com/
    VITE_AUTH_CLIENT_ID=budget-client
    VITE_AUTH_REDIRECT_URI=http://localhost:5173/callback
    
  5. Set the following in src/Budget.Api/appsettings.Development.json:

    {
      "AUTH__AUTHORITY": "https://auth.stwaddle.com/",
      "AUTH__AUDIENCE": "budget-api"
    }
    

The auth server's dev redirect URI (http://localhost:5173/callback) must be registered on the client as described in the Auth Server Setup section above.

S
Description
No description provided
Readme 240 KiB
Languages
TypeScript 46.8%
C# 43.9%
CSS 7.8%
Dockerfile 0.5%
JavaScript 0.4%
Other 0.6%