LittleHelpers is a family chore management app. Parents create chores, assign them to children, and award points based on difficulty. Children mark chores as completed through the frontend. Each child can be given a monthly allowance goal – once enough points are earned, the app calculates how much of the allowance has been earned that month.
Key features:
- Parent role: manage users, create/edit chores, view all children and their progress
- Child role: view and complete assigned chores
- Monthly allowance tracking with point goals and payout calculation
- Per-child score history with a monthly chart
- Parent bonus/deduction chores that affect a child's points directly
- Full audit log of every completed chore including who performed it
| Layer | Technology |
|---|---|
| Orchestration | .NET Aspire |
| Backend | ASP.NET Core (.NET 10) – Controllers, HATEOAS |
| Frontend | Angular (standalone components, signals, Bootstrap 5 dark mode) |
| Database | PostgreSQL 17 |
| Auth | JWT (Bearer tokens) |
| ORM | Entity Framework Core + Npgsql |
| Passwords | BCrypt |
| Observability | OpenTelemetry (traces, logs, metrics via Aspire Dashboard) |
LittleHelpers.sln
├── LittleHelpers.AppHost/ # Aspire orchestrator – defines all resources
├── LittleHelpers.ApiService/ # ASP.NET Core API
│ ├── Controllers/
│ ├── Data/ # DbContext + EF migrations
│ ├── Models/ # Entities and DTOs
│ └── Services/ # Business logic, LinkWriter<T> (HATEOAS)
├── LittleHelpers.ServiceDefaults/# Shared Aspire service defaults (telemetry, health)
├── LittleHelpers.Tests/ # Unit and integration tests
└── LittleHelpers.Web/
# Angular app (src/app/{core,features,shared})
- .NET 10 SDK
- Aspire CLI (
dotnet tool install -g aspire-clioraspire update) - Docker (used by Aspire to run PostgreSQL)
- Node.js 22+
The committed LittleHelpers.ApiService/appsettings.json now lists every supported API setting and keeps sensitive values blank. For local development, create LittleHelpers.ApiService/appsettings.Development.json (gitignored) and override the sensitive values you need:
{
"ConnectionStrings": {
"littlehelpers": "Host=localhost;Port=5432;Username=littlehelpers;Password=CHANGE_ME;Database=littlehelpers"
},
"Jwt": {
"Key": "your-secret-key-min-32-characters-long",
"Issuer": "littlehelpers-api",
"Audience": "littlehelpers-client",
"AccessTokenLifetimeHours": 168,
"RenewTokenLifetimeHours": 336
},
"SeedAdminPassword": "YourAdminPassword123!",
"MonthlyCycle": {
"BreakpointDay": 27
}
}Tip: Generate a strong key with
openssl rand -base64 48
These are the properties currently read by the API from appsettings*.json / environment variables:
| Property | Default in committed appsettings.json |
Notes |
|---|---|---|
Logging:LogLevel:Default |
Information |
Baseline application log level |
Logging:LogLevel:Microsoft.AspNetCore |
Warning |
Reduces ASP.NET Core framework log noise |
AllowedHosts |
* |
Standard ASP.NET Core host filtering |
ConnectionStrings:littlehelpers |
(empty) | PostgreSQL connection string; usually injected by Aspire or set explicitly outside Aspire |
Jwt:Key |
(empty) | Required secret, minimum 32 characters |
Jwt:Issuer |
littlehelpers-api |
Used both when issuing and validating JWTs |
Jwt:Audience |
littlehelpers-client |
Used both when issuing and validating JWTs |
Jwt:AccessTokenLifetimeHours |
168 |
Access token lifetime in hours |
Jwt:RenewTokenLifetimeHours |
336 |
Renewed token lifetime in hours |
SeedAdminPassword |
(empty) | Required on first startup when the database has no users |
MonthlyCycle:BreakpointDay |
27 |
Day of month when a new allowance cycle starts |
Jwt:AccessTokenLifetimeHours and Jwt:RenewTokenLifetimeHours also fall back to 168 and 336 in code if omitted. MonthlyCycle:BreakpointDay validates 1-31; if the setting is omitted entirely, the code fallback is 1.
aspire runAspire will start PostgreSQL in Docker, run the API, and serve the Angular frontend. The Aspire Dashboard (traces, logs, metrics) opens automatically.
If a previous instance is already running, the CLI will prompt to stop it first.
# Start the application
aspire run
# List resources and their status
aspire resource list
# View logs for a specific resource
aspire logs apiservice
# Stop the application
aspire stopChanges to
LittleHelpers.AppHost/AppHost.csrequire restarting the application. Changes to the API or frontend hot-reload automatically.
The project ships two Dockerfiles, both built from the repository root (they need access to multiple projects).
Multi-stage build: sdk:10.0 for compilation → aspnet:10.0 runtime (~95 MB). Runs as a non-root user.
# Build
docker build -f Dockerfile.api -t littlehelpers-api:latest .
# Tag for a registry
docker tag littlehelpers-api:latest ghcr.io/yourorg/littlehelpers-api:1.0.0
# Push
docker push ghcr.io/yourorg/littlehelpers-api:1.0.0
# Run standalone (requires a running PostgreSQL)
docker run -d \
-p 80:80 \
-e Jwt__Key="your-secret-key-min-32-characters" \
-e Jwt__AccessTokenLifetimeHours="168" \
-e Jwt__RenewTokenLifetimeHours="336" \
-e SeedAdminPassword="YourAdminPassword123!" \
-e MonthlyCycle__BreakpointDay="1" \
-e ConnectionStrings__littlehelpers="Host=localhost;Port=5432;Username=littlehelpers;Password=secret;Database=littlehelpers" \
littlehelpers-api:latestMulti-stage build: node:22-alpine for ng build → nginx:1.27-alpine serving static files (~21 MB). The nginx config is a template – API_URL is substituted at container startup via envsubst, so the proxy target can be changed without rebuilding. The same startup templating also supports runtime override of web app metadata (name, short_name, description, lang, and HTML <title>).
# Build
docker build -f Dockerfile.web -t littlehelpers-web:latest .
# Tag for a registry
docker tag littlehelpers-web:latest ghcr.io/yourorg/littlehelpers-web:1.0.0
# Push
docker push ghcr.io/yourorg/littlehelpers-web:1.0.0
# Run standalone (proxies /api/* to the API)
docker run -d \
-p 80:80 \
-e API_URL="http://apiservice" \
-e WEBAPP_NAME="My Family App" \
-e WEBAPP_SHORT_NAME="FamilyApp" \
-e WEBAPP_DESCRIPTION="Household chores and rewards" \
-e WEBAPP_LANG="en" \
-e WEBAPP_TITLE="My Family App" \
littlehelpers-web:latestThe API_URL environment variable must point to the API service. When running both containers together, use the service name from docker-compose (http://apiservice).
The docs/compose/ directory contains a ready-to-use Compose file and an example environment file.
docs/compose/
├── docker-compose.yml # Full stack: postgres, apiservice, webfrontend
└── .env.example # Template – copy to .env and fill in values
cd docs/compose
cp .env.example .envOpen .env and fill in all CHANGE_ME values:
| Variable | Description |
|---|---|
POSTGRES_PASSWORD |
PostgreSQL password |
JWT_KEY |
JWT signing key, minimum 32 characters |
JWT_ISSUER |
JWT issuer claim (default: littlehelpers) |
JWT_AUDIENCE |
JWT audience claim (default: littlehelpers) |
JWT_ACCESS_TOKEN_LIFETIME_HOURS |
Access token lifetime in hours (default: 168) |
JWT_RENEW_TOKEN_LIFETIME_HOURS |
Renewed token lifetime in hours (default: 336) |
SEED_ADMIN_PASSWORD |
Password for the initial admin account |
MONTHLY_CYCLE_BREAKPOINT_DAY |
Day in month when a new allowance cycle starts (1-31, default: 1; falls back to last day when needed) |
WEB_PORT |
Host port the frontend listens on (default: 80) |
WEBAPP_NAME |
PWA name in manifest (default: LittleHelpers) |
WEBAPP_SHORT_NAME |
PWA short_name in manifest (default: LittleHelpers) |
WEBAPP_DESCRIPTION |
PWA description in manifest |
WEBAPP_LANG |
Language used in manifest and <html lang> (default: en) |
WEBAPP_TITLE |
Browser tab title (<title>) (default: LittleHelpers) |
APISERVICE_IMAGE |
Override API image (optional, default: littlehelpers-api:latest) |
WEBFRONTEND_IMAGE |
Override frontend image (optional, default: littlehelpers-web:latest) |
Never commit
.envto version control. It is listed in.gitignore.
# Build images locally and start all services
docker compose up -d --build
# Or pull pre-built images (if APISERVICE_IMAGE / WEBFRONTEND_IMAGE are set)
docker compose up -dThe frontend will be available at http://localhost (or whatever WEB_PORT is set to). The API is not exposed to the host – it is only reachable from within the Docker network via the frontend nginx proxy.
# View logs
docker compose logs -f
# Logs for a single service
docker compose logs -f apiservice
# Stop all services (data volume is preserved)
docker compose down
# Stop and remove all data (destructive)
docker compose down -v
# Restart a single service after a rebuild
docker compose up -d --build apiserviceThe API seeds a default admin account at startup if it does not already exist:
| Field | Value |
|---|---|
| Username | admin |
| Password | Value of SeedAdminPassword |
| Role | Parent |
Log in at /login in the frontend, or call POST /api/auth/login directly:
curl -X POST http://localhost/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"YourAdminPassword123!"}'The response includes a JWT Bearer token. All endpoints except /api/auth/login and /api/health require the Authorization: Bearer <token> header.
To renew an active token, call:
curl -X POST http://localhost/api/auth/renew \
-H "Authorization: Bearer <token>"EF Core migrations are applied automatically at startup. To run them manually or create new ones:
# Apply migrations
dotnet ef database update \
--project LittleHelpers.ApiService \
--connection "Host=localhost;Port=5432;Username=littlehelpers;Password=secret;Database=littlehelpers"
# Create a new migration
dotnet ef migrations add <MigrationName> \
--project LittleHelpers.ApiService \
--output-dir Data/MigrationsThe design-time factory (AppDbContextFactory) reads the connection string from the LITTLEHELPERS_CONNSTR environment variable when running dotnet ef commands outside of Aspire.
dotnet testAll tests are in LittleHelpers.Tests/. The test suite uses an in-memory JWT key and does not require a running database or Docker.
Releases are created automatically by GitHub Actions when you push a version tag that points to a commit on main.
| Tag example | Type | GitHub release | Docker tags |
|---|---|---|---|
v0.0.7 |
Stable release | Release | v0.0.7 + latest |
v0.0.7-pre1 |
Pre-release | Pre-release | v0.0.7-pre1 + prerelease |
- CI tests run first (.NET + Angular build/tests).
- Docker images are built and pushed (
ferenyl/littlehelpers.apiandferenyl/littlehelpers.web). - A GitHub release is created from the tag.
- Release notes are auto-generated from commits since the previous release and include GitHub compare links.
git checkout main
git pull --ff-only
git tag v0.0.7
git push origin v0.0.7git checkout main
git pull --ff-only
git tag v0.0.7-pre1
git push origin v0.0.7-pre1If a tag does not point to a commit contained in
main, release/publish jobs are skipped.
The frontend uses @jsverse/transloco for internationalization. Supported languages are English (default) and Swedish.
On startup, the app detects the browser language via navigator.language. If the language starts with sv, Swedish is used – otherwise English is the fallback.
Translation files are static JSON files served as regular assets:
LittleHelpers.Web/public/i18n/
├── en.json # English (default)
└── sv.json # Swedish
The files are fetched at runtime via HttpClient. There is no build step required when adding or editing translations.
Translations are organized by feature:
| Prefix | Covers |
|---|---|
common.* |
Shared labels: loading, save, cancel, edit, delete, points |
nav.* |
App name, logout button |
login.* |
Login form and error messages |
users.* |
User list, user form, role labels |
chores.* |
Chore list, chore form, difficulty levels |
children.* |
Children list view |
childDetail.* |
Child detail view, allowance section, chart |
months.1–12 |
Month names (used in chart/history labels) |
- Create
public/i18n/<code>.jsonusingen.jsonas a template. - Register the language code in
app.config.ts:
// in provideTransloco({ ... })
availableLangs: ['en', 'sv', '<code>'],- Update
detectLanguage()if you want the new language to be auto-detected:
function detectLanguage(): string {
const lang = navigator.language.split('-')[0];
return ['en', 'sv', '<code>'].includes(lang) ? lang : 'en';
}In templates, use the transloco pipe:
<p>{{ 'common.save' | transloco }}</p>
<!-- With interpolation parameters -->
<h2>{{ 'childDetail.allowance.title' | transloco: { month: translatedMonth(), year: year() } }}</h2>In TypeScript (e.g. confirm dialogs), inject the service:
private transloco = inject(TranslocoService);
if (!confirm(this.transloco.translate('chores.confirmDelete'))) return;- JWT key: stored in
appsettings.Development.jsonlocally (gitignored). In production, set via theJwt__Keyenvironment variable. - Seed password: stored in
appsettings.Development.jsonlocally (gitignored). In production, set viaSeedAdminPassword. - Database credentials: never hardcoded. Always supplied via environment variables or Aspire connection strings.
- The
appsettings.Development.jsonfile is gitignored. Do not commit it.