Developer Documentation

WIN Student Goal Tracker

April 2026

1. Project Description

The WIN Student Goal Tracker is a web-based case management system designed to support organizations working with adults with special needs. The platform enables staff to track student goals, document services, and log critical incidents while maintaining strong privacy, auditability, and compliance with FERPA, IDEA, and FAPE.

The system is designed with sustainability in mind, ensuring that future developers can easily maintain, extend, and redeploy the application.

2. Architecture

Technology Stack & Infrastructure

Frontend Angular 20.1.5
Backend .NET 9.0 (C#) with Dapper ORM
Authentication JWT + Refresh Tokens
Database MySQL
Infrastructure Docker + VPS + Traefik

Architecture Description

  1. Presentation Layer (Angular)
  2. Application Layer (.NET API)
  3. Data Layer (MySQL)

Traefik manages routing and HTTPS traffic.

3. Data Flow

Each scenario lists the UI file that initiates the action, the Angular service method called, the API endpoint hit, the .NET controller and repository method invoked, and the stored procedure that reads or writes the database.

1 — User Login

login.html (submit) → Auth.login() [auth.ts] → POST /api/Auth/Login → AuthController.Login() → UserRepository.GetByEmailAsync() + GetProgramsForUserIdAsync() → sp_User_GetByEmail, sp_UserPrograms_GetByUserId → session token + program list → login.html renders program selector

2 — View My Students (Home Dashboard)

home.ts (constructor) → StudentService.getMyStudents() [student.service.ts] → GET /api/Student/my → StudentController.GetMyStudents() → StudentRepository.GetMyStudentsAsync() → sp_Student_GetWithAssignments → student card list → home.html renders student cards

3 — Open Student Workspace (Full Profile)

home.html (card click → router) → workspace.ts (route param change) → StudentService.getFullProfile() [student.service.ts] → GET /api/Student/{id}/full → StudentController.GetFullProfile() → StudentRepository.GetFullProfileAsync() → sp_Student_GetFullProfile → goals + benchmarks + progress events → workspace.html renders detail view

4 — Create Goal

workspace.html (Add Goal button) → goal-modal.ts (onSubmit) → StudentService.createGoal() [student.service.ts] → POST /api/Student/{id}/goals → StudentController.CreateGoal() → PermissionService.IsAllowed() → StudentRepository.InsertGoalAsync() → sp_Goal_Insert → created goal returned → goal-modal closes, workspace.html refreshes

5 — Log Progress Event

workspace.html (Add Event) → edit-event-modal.ts (onSave) → StudentService.addProgressEvent() [student.service.ts] → POST /api/Student/{id}/progress-event → StudentController.AddProgressEvent() → StudentRepository.SaveProgressEventAsync() → sp_ProgressEvent_Save → new event ID returned → workspace.html progress list refreshes

4. Recommended Hosting

  • The entire application is currently hosted online with Hetzner (www.hetzner.com), on a plan donated by team members. The team has committed to hosting for at least the next two years, and if/when that changes, will help the partner transition to the hosting platform of their choice. The team recommends staying with Hetzner based on their reliability and low cost.

5. Application Installation

The application is composed of three moving parts — an Angular 20 SPA (ui/winstudentgoaltracker), a .NET 9 Web API (api/), and a MySQL 8 database initialised from the SQL objects in db/Objects/. Configuration for every tier is read from a single .env file at the repository root. Two workflows are supported: running the stack directly on a developer workstation, or deploying the full stack to a server with docker compose behind an existing Traefik reverse proxy.

5.1 Local Development Environment

The recommended local workflow runs MySQL in Docker (so the schema initialises itself from db/docker-init/01-schema.sh) while the API and UI run on the host for fast iteration and debugging.

Prerequisites

  • Node.js 22+ and npm — to run the Angular CLI
  • .NET 9 SDK — to build and run the API
  • Docker Desktop (or Docker Engine + Compose plugin) — to run MySQL
  • Git — to clone the repository

Step 1 — Clone and create .env

Clone the repository and create a .env file at the project root. The API reads it via DotNetEnv (which traverses parent directories), the UI/API containers read it through docker-compose.yml's env_file directive, and the MySQL container consumes the MYSQL_* variables natively.

git clone <repo-url> WinStudentGoalTracker
cd WinStudentGoalTracker

Create .env with the following keys (replace the secrets before sharing or deploying):

# MySQL container bootstrap
MYSQL_ROOT_PASSWORD=change_me_root
MYSQL_DATABASE=winstudentgoaltracker
MYSQL_USER=appuser
MYSQL_PASSWORD=change_me_app

# Connection used by the .NET API
MYSQL_HOST=127.0.0.1
MYSQL_PORT=3309
# (the api falls back to root/no-password if MYSQL_USER/PASSWORD are omitted)

# JWT signing key — must be >= 32 bytes
JWT_KEY=replace_with_a_long_random_string_at_least_32_chars

.env is listed in .gitignore; never commit it. For local dev, point MYSQL_HOST at 127.0.0.1 so the host-side API can reach the containerised MySQL through the published port 3309.

Step 2 — Start MySQL

Bring up only the database service from the root compose file. On first run it creates the volume, creates the database and application user, and executes db/docker-init/01-schema.sh which loads tables, functions, views, and stored procedures from db/Objects/ in the correct order.

docker compose up -d mysql
docker compose logs -f mysql   # watch for "Schema initialization complete"

MySQL is exposed on host port 3309 (mapped to container port 3306). To reset the database during development, stop the container and remove the named volume: docker compose down && docker volume rm winstudentgoaltracker_win_mysql_data.

Step 3 — Run the API

cd api
dotnet restore
dotnet run

The API listens on http://localhost:5000 (and https://localhost:5001) by default via Kestrel. Swagger UI is available at /swagger when running in the Development environment (the default for dotnet run). On startup the console prints the resolved MySQL connection string — verify it points at 127.0.0.1:3309 and the database name from your .env.

Step 4 — Run the UI

cd ui/winstudentgoaltracker
npm install
npm start

The Angular dev server starts on http://localhost:4200. The base URL the UI calls is defined in src/environments/environment.development.ts. For a fully local stack, edit that file so apiBaseUrl points at your locally running API (e.g. http://localhost:5000) instead of the deployed https://winapi.opelly.me. The API's CORS policy already allows any origin in development.

Step 5 — Seed a user

The schema initialises empty. To sign in, insert at least one user, one school_district, one program, and a user_program row linking the user to the program with a role. Passwords are hashed with PBKDF2 by PasswordHasher; the simplest path is to use an existing seeded dump (ask a team member for seed.sql) or register via a super_admin account using the admin endpoints.

5.2 Production Deployment (Docker Compose + Traefik)

Production deploys all four tiers — MySQL, .NET API, Nginx-served Angular build, and the Traefik reverse proxy — as Docker containers on a single VPS. The repository's docker-compose.yml brings up MySQL, the API, and the UI; Traefik runs in a separate compose stack on the same host and publishes ports 80/443 for the whole server. The two stacks communicate through an external Docker network named web.

Prerequisites on the server

  • Linux VPS with Docker Engine and the Compose plugin installed
  • Ports 80 and 443 open on the server's firewall
  • Two DNS A records pointing at the server's public IP: win.<your-domain> (UI) and winapi.<your-domain> (API)
  • A working Traefik stack on the host (see next section)

Setting up Traefik (one-time)

Traefik is the edge router that terminates TLS, requests Let's Encrypt certificates, and forwards traffic to the correct container based on the Host() rule. The WIN compose file assumes Traefik is already running on the host and configured with:

  • An external Docker network named web that Traefik is attached to. The application's UI and API containers join this network so Traefik can reach them.
    docker network create web
  • An entrypoint named websecure listening on :443 (and typically a redirect from :80).
  • A certificate resolver named letsencrypt (ACME, HTTP or DNS challenge) configured with an email and a persistent acme.json volume.
  • Two file-provider middlewares referenced by the labels in docker-compose.yml: gzip@file (compression) and security-headers@file (HSTS, frame-deny, etc.).

A minimal Traefik docker-compose.yml typically lives in /opt/traefik/ alongside a traefik.yml static config (declaring the entrypoints, providers, and ACME resolver) and a dynamic/ directory containing the gzip and security-headers middleware definitions. Traefik's official documentation at doc.traefik.io covers this setup in detail — the WIN stack does not prescribe a specific Traefik configuration, it only requires that the network, entrypoint, cert resolver, and middleware names above exist.

Step 1 — Prepare the repository on the server

git clone <repo-url> /opt/winstudentgoaltracker
cd /opt/winstudentgoaltracker

Step 2 — Populate .env

Same keys as the local workflow, but with production-safe values. For the containerised deployment, MYSQL_HOST must be the service name mysql (the API container resolves it over the internal backend Docker network) and MYSQL_PORT must be 3306 (the container's internal port, not the host-mapped 3309):

MYSQL_ROOT_PASSWORD=<strong-random>
MYSQL_DATABASE=winstudentgoaltracker
MYSQL_USER=appuser
MYSQL_PASSWORD=<strong-random>
MYSQL_HOST=mysql
MYSQL_PORT=3306
JWT_KEY=<32+ byte random secret>

Step 3 — Update domain labels (if not using opelly.me)

docker-compose.yml hard-codes the Traefik Host() rules winapi.opelly.me and win.opelly.me. Edit those two labels to match your DNS records before bringing the stack up. The UI also points at https://winapi.opelly.me via src/environments/environment.ts; update that file and rebuild the UI image so the browser calls your API host.

Step 4 — Build and start the stack

docker compose up -d --build

This creates:

  • winstudent-mysql — MySQL 8 on the internal backend network, volume win_mysql_data, schema loaded on first start.
  • winstudent-api — .NET 9 API on port 8005, attached to both backend (to reach MySQL) and web (to be reached by Traefik). Published at winapi.<your-domain> over HTTPS.
  • winstudent-ui — Angular build served by Nginx on port 8006, attached to web. Published at win.<your-domain> over HTTPS.

Traefik picks up the new containers automatically via its Docker provider, requests TLS certificates from Let's Encrypt on first request, and begins routing traffic. Confirm with docker compose ps (all services healthy/running) and docker compose logs -f api (look for the Kestrel startup line and the printed connection string).

Step 5 — Verify

  • Visit https://win.<your-domain> — the login page should load over HTTPS with a valid Let's Encrypt certificate.
  • Visit https://winapi.<your-domain>/swagger — should return 404 in production (Swagger is only enabled in Development); a 404 from the API itself confirms routing works.
  • Sign in with a seeded user and check docker compose logs api for successful /api/Auth/Login requests.

Updating a running deployment

cd /opt/winstudentgoaltracker
git pull
docker compose up -d --build    # rebuilds changed images and recreates containers
docker compose logs -f

MySQL is not rebuilt by --build (it uses the upstream image) and its volume persists across up/down cycles, so data survives code deployments. Schema migrations should be applied by running the appropriate SQL files from db/migrations/ against the running MySQL container; see section 7 for backup procedures before running migrations.

6. Authentication & Authorization

JWT-based authentication with refresh tokens.

Key files: AuthController.cs, JwtService.cs, middleware

7. Database Backup

Backup

mysqldump -u username -p dbname > backup.sql

Restore

mysql -u username -p dbname < backup.sql

From the UI

Administrators may back up the database via the "Backup" button in the Administrator panel

8. Environment Variables

DB_CONNECTION=...
JWT_SECRET=...
JWT_EXPIRATION=3600

Ensure .env is in .gitignore.

9. Partner Statement

The partner representatives Polly Balsillie and Fred Winter have reviewed the developer documentation and understands its scope and purpose.

Date: April 16, 2026

10. Installation Walkthrough Statement

N/A. An installation walkthrough was not appropriate for the partner, as they are non-technical. They are primarily users of the application, and at least two of our team members will continue to make ourselves available to support the application.

Date: April 16, 2026

11. Performance & UX Analysis

Lighthouse Results

Lighthouse Desktop Results
Desktop
Lighthouse Mobile Results
Mobile

Form Factor Analysis

  • Mobile Portrait: Fully responsive and functional - minimal UI and streamlined functionality for robust field use.
  • Mobile Landscape: Improved readability and layout - same features and user story as mobile portrait
  • Desktop: Full-featured user experience, with application configuration, administrator and edit/delete functionality

UX Observations

Clean navigation with consistent user workflows across devices.

Responsive Accessible High Performance SEO Ready

12. Known Liabilities & Improvements

Issues: Potential performance scaling and mobile optimization opportunities

Improvements: Lazy loading, bundle optimization, responsive enhancements

13. Sustainability Considerations

Docker deployment, very inexpensive hosting, and modular design support long-term maintainability. Addcitionally, the use of free, off-the-shelf technology choices (Angular, C#, MySQL) contribute a sustainable project tech stack.

Appendix A – ERD

The system includes a structured relational database supporting users, roles, permissions, programs, students, goals, progress tracking, and incident logging. The interactive diagram below shows all 17 tables and their relationships. Drag tables to rearrange, scroll to zoom, and hover a table to highlight its connections.

Open full-page ERD

⬡ ERD

Tables 17
Relations 0
Primary Key
Foreign Key
Relationship

Appendix B – Repository Structure

WinStudentGoalTracker/
├── api/                    .NET 9 backend — controllers, services, repositories, Dockerfile
├── db/                     MySQL schema and stored procedures; docker-init scripts for container startup
├── docs/                   HTML documentation (technical spec, user manual, ERD, architecture, API)
├── ui/                     Angular 20 frontend SPA
├── prototype/              Early role-assignment prototypes (not production code)
├── docker-compose.yml      Orchestrates API, Angular/Nginx, MySQL, and Traefik containers
└── WinStudentGoalTracker.sln  Visual Studio solution file

Appendix C – Architecture Overview

High-level diagrams of the system tiers, two-phase auth flow, data model, role hierarchy, deployment, and key design decisions.

Open full-page Architecture Overview
🏗 System Architecture
Client
Angular SPA v20
  • TypeScript 5.8 + SCSS
  • Angular Signals (state)
  • Lazy-loaded routes
  • JWT auth interceptor
  • Proactive token refresh
Desktop Layout
  • Home / Workspace
  • Student detail view
  • Goal & Benchmark modals
  • Progress event modals
  • Report generation UI
  • Admin panel
Mobile Layout
  • Student card list
  • Add progress event
  • Toggle benchmark
Shared Services
  • AuthService (signals)
  • ApiService (HTTP)
  • StudentService
  • AdminService
  • ReportPromptService
  • PlatformService
HTTPS / REST + JWT Bearer — win.opelly.me → winapi.opelly.me
Proxy
Traefik Reverse Proxy
  • win.opelly.me → Angular (Nginx :8006)
  • winapi.opelly.me → API (:8005)
  • Let's Encrypt TLS
  • Gzip compression
  • Security headers
HTTP (internal Docker network)
API
ASP.NET Core 9 C#
  • JWT authentication middleware
  • Swagger / OpenAPI
  • CORS policy
  • Dependency injection
Controllers
  • /api/Auth — login & tokens
  • /api/Student — CRUD
  • /api/Admin — district/users
  • /api/ReportPrompt — prompts
Services
  • TokenService (JWT)
  • PermissionService
  • PasswordHasher (PBKDF2)
  • RecommendationService
  • TranscriptionService
  • ProgressReportBuilder
Repositories Dapper
  • StudentRepository
  • UserRepository
  • AuthRepository
  • AdminRepository
  • ReportPromptRepository
Stored procedures via Dapper — TCP :3309 (Docker internal)
Data
MySQL 8 Docker
  • winstudentgoaltracker DB
  • 17 tables
  • 51 stored procedures
  • Initialised from db/docker-init/
Core Tables
  • user, school_district, program
  • user_program (roles junction)
  • student, user_student
  • goal (recursive), benchmark
  • progress_event + junction
  • refresh_token
Procedure Groups
  • sp_Student_* (CRUD)
  • sp_Goal_* / sp_Benchmark_*
  • sp_ProgressEvent_*
  • sp_RefreshToken_*
  • sp_Program_* / sp_User_*
  • sp_ProgressReport_*
Outbound HTTP from API tier only
External
Ollama LLM AI
  • llm.opelly.me
  • Model: gemma4:e2b
  • Benchmark recommendations
  • 5-min timeout
STT Service AI
  • stt.opelly.me
  • Speech-to-text transcription
  • Progress event dictation
  • 5-min timeout
🔐 Two-Phase Authentication Flow
1
User Submits Credentials email + password
POST /api/Auth/Login
2
Phase 1 Response Session token (5 min)
+ program list
Stored in localStorage
3
User Selects Program session token + program ID
POST /api/Auth/SelectProgram
4
Phase 2 Response JWT (1 min) +
Refresh token (30 days)
JWT includes role + program_id
5
Authenticated Requests Bearer JWT on all API calls
Auth interceptor injects token
6
Proactive Refresh 10 s before JWT expiry:
POST /api/Auth/RefreshToken
Rotated refresh token returned
Token storage: auth_jwt, auth_refresh_token, auth_session_token — all in localStorage.  |  401 interceptor: attempts one silent refresh; if that fails, redirects to /login.  |  Refresh token rotation: each use replaces the old token (tracked via replaced_by_token_id).
🗄 Core Data Model
school_district
PK id_school_district
name
contact_email
Top-level tenant
program
PK id_program
FK id_school_district
name, description
Scope boundary for data
user
PK id_user
email, name
password_hash, password_salt
locked_until
user_program (junction)
PK id_user_program
FK id_user
FK id_program
FK id_role
is_primary, status
student
PK id_student
FK id_program
identifier, program_year
enrollment_date
next_iep_date
user_student (junction)
PK id_user_student
FK id_user
FK id_student
is_primary
goal
PK id_goal
FK id_goal_parent (self-ref)
FK id_student
description, category
baseline, target_completion_date
close_date, achieved
benchmark
PK id_benchmark
FK id_goal
benchmark (description)
short_name
progress_event
PK id_progress_event
FK id_goal
FK id_user_created
content (rich text)
is_sensitive
progress_event_benchmark (junction)
FK id_progress_event
FK id_benchmark
Links events to benchmarks
refresh_token
PK id_refresh_token
FK id_user, id_program
token_hash, token_salt
expires_at, revoked_at
replaced_by_token_id
report_prompt
PK id_report_prompt
FK program_id
report_name
prompt_template (LLM)
Key relationships:  school_district 1→N program  ·  program 1→N student  ·  student 1→N goal (recursive parent/child)  ·  goal 1→N benchmark  ·  goal 1→N progress_event  ·  progress_event M↔N benchmark (junction)  ·  user M↔N program (via user_program + role)  ·  user M↔N student (via user_student)
👥 Role Hierarchy & Permissions
Role Tiers (highest → lowest)
super_admin  ·  full platform access
district_admin  ·  manages programs
program_admin  ·  manages users
teacher  ·  full student CRUD
paraeducator  ·  log events
Sample Permission Matrix
Entity / Action super_admin district_admin program_admin teacher paraeducator
Student — Create Allow Allow Allow Allow Deny
Student — Update Allow Allow Allow Mine Deny
Goal — Create Allow Allow Allow Mine Deny
Goal — Update Allow Allow Allow Mine Deny
ProgressEvent — Create Allow Allow Allow Allow Mine
ProgressEvent — Delete Allow Allow Allow Mine Deny
Program — Create Allow Mine Deny Deny Deny
User — Create Allow Mine Mine Deny Deny
Allow = full access   Mine = own records only   Deny = not permitted
🐳 Deployment & Infrastructure

Docker Containers

  • mysql:8 (port 3309)
  • .NET API (port 8005)
  • Angular / Nginx (port 8006)
  • Traefik reverse proxy

Docker Networks

  • web — external HTTPS routing
  • backend — internal service mesh
  • API depends on MySQL healthcheck
  • UI depends on API (build time)

Environment Config

  • MYSQL_ROOT_PASSWORD
  • MYSQL_DATABASE / USER / PASSWORD
  • JWT_KEY (signing secret)
  • MYSQL_HOST / PORT

Frontend Environments

  • Dev: localhost:5000 (API)
  • Prod: winapi.opelly.me (API)
  • Served by Nginx in production
  • Angular CLI dev server locally

DB Initialisation

  • Schemas from db/docker-init/
  • 51 stored procedures
  • Health check gates API startup
  • No ORM migrations — raw SQL

AI / External Services

  • Ollama LLM — llm.opelly.me
  • STT service — stt.opelly.me
  • Both called from API tier only
  • 5-minute timeout on both
⚙️ Key Design Decisions

Two-Phase JWT Auth

  • Phase 1: credentials → session token
  • Phase 2: program select → scoped JWT
  • Prevents data leakage before program chosen
  • JWT scoped to one program at a time

Refresh Token Rotation

  • New token issued on each refresh
  • Old token replaced (not deleted)
  • Genealogy tracked via replaced_by
  • Prevents replay attacks

Program-Scoped Multi-Tenancy

  • JWT contains program_id claim
  • All queries filtered by program
  • district_admin scoped to district
  • super_admin bypasses scope

Dapper + Stored Procedures

  • No ORM (Entity Framework)
  • Business logic close to data
  • Dapper maps rows to C# objects
  • 51 procedures for full coverage

Angular Signals (not RxJS)

  • Auth state as reactive signals
  • Computed signals derive user context
  • Simpler than Observable chains
  • Auto-update dependent UI

Centralised Permission Matrix

  • Declarative rules per entity + action
  • Allow / MineOnly / Deny granularity
  • Evaluated at request time in API
  • Single source of truth for authz