Star Tracker
A family star reward tracking app. Parents award stars to kids for good behavior, and kids redeem stars for rewards. Built with Go, SQLite, and server-rendered HTML templates.
Screenshots
| Login | Dashboard |
|---|---|
![]() |
![]() |
| Reward Translations | User & Reason Translations |
|---|---|
![]() |
![]() |
Features
- Star awarding with configurable reasons and star counts (positive or negative)
- Reward redemption system with cost tracking
- Multi-language support (English, Simplified Chinese, Traditional Chinese)
- User management with parent/kid roles
- Home Assistant TTS integration for announcements
- REST API for external integrations (e.g. Home Assistant automations)
- Data import/export as JSON
- Single binary deployment with embedded templates and static assets
Build & Run
go build -o star-app .
./star-app -port 8080 -db stars.dbFlags
| Flag | Default | Description |
|---|---|---|
-port |
8080 |
HTTP listen port |
-db |
stars.db |
SQLite database path |
Cross-compile for ARM64 Linux
GOOS=linux GOARCH=arm64 go build -o star-app-arm64 .Default Accounts
On first run with an empty database, four accounts are seeded:
| Username | Password | Role |
|---|---|---|
dad |
Generated at startup (printed to stdout) | Parent |
mom |
Generated at startup (printed to stdout) | Parent |
theo |
Generated at startup (printed to stdout) | Kid |
ray |
Generated at startup (printed to stdout) | Kid |
Set STAR_APP_DEFAULT_PASSWORD before first startup if you want one fixed bootstrap password for all seeded accounts.
Accounts can be added and removed from the admin panel.
Architecture
Single-package Go application:
| File | Purpose |
|---|---|
main.go |
Entry point, route registration, template loading |
models.go |
Data structs (User, Star, Reason, Reward, etc.) |
db.go |
SQLite schema, migrations, all database queries |
handlers.go |
HTTP handlers for web UI and REST API |
middleware.go |
Authentication middlewares (session, admin, API key) |
announce.go |
Home Assistant TTS announcement integration |
Templates are in templates/ and static assets in static/, both embedded into the binary via Go's embed package.
Authentication
Three authentication methods:
| Method | Mechanism | Used by |
|---|---|---|
| Session cookie | session cookie with random hex token |
Web UI routes |
| Admin check | Session cookie + is_admin flag |
Admin routes |
| API key | X-API-Key header, SHA256 hashed |
/api/* routes |
API keys are generated from the admin panel. The raw key is shown once at creation; only the SHA256 hash is stored.
REST API
All API endpoints require authentication via the X-API-Key header.
GET /api/users
Returns all users with their star counts.
Response:
[
{
"Username": "theo",
"DisplayNameEN": "Theo",
"DisplayNameCN": "西奥",
"DisplayNameTW": "西奧",
"StarCount": 42,
"CurrentStars": 15,
"IsAdmin": false
}
]| Field | Type | Description |
|---|---|---|
Username |
string | Login username |
DisplayNameEN |
string | English display name (from translations) |
DisplayNameCN |
string | Simplified Chinese display name |
DisplayNameTW |
string | Traditional Chinese display name |
StarCount |
int | Total stars ever earned |
CurrentStars |
int | Current balance (earned minus redeemed) |
IsAdmin |
bool | Whether the user is a parent (admin) |
GET /api/stars
Returns star award history with fully resolved translations for reasons, usernames, and awarder names.
Query Parameters:
| Param | Required | Description |
|---|---|---|
user |
No | Filter by username |
Response:
[
{
"id": 1,
"user_id": 3,
"username": "theo",
"username_en": "Theo",
"username_cn": "西奥",
"username_tw": "西奧",
"reason_id": 2,
"reason_text": "",
"reason_en": "Cleaned room",
"reason_cn": "打扫房间",
"reason_tw": "打掃房間",
"stars": 1,
"awarded_by": 1,
"awarded_by_name": "dad",
"awarded_by_name_en": "Dad",
"awarded_by_name_cn": "爸爸",
"awarded_by_name_tw": "爸爸",
"created_at": "2025-01-15T10:30:00Z"
}
]| Field | Type | Description |
|---|---|---|
id |
int | Star record ID |
user_id |
int | Recipient user ID |
username |
string | Recipient username |
username_en/cn/tw |
string | Translated display names |
reason_id |
int|null | Reference to a predefined reason (nullable) |
reason_text |
string | Free-text reason (used when no reason_id) |
reason_en/cn/tw |
string | Resolved reason translations |
stars |
int | Number of stars (negative for penalties) |
awarded_by |
int | User ID of the person who awarded the star |
awarded_by_name |
string | Username of awarder |
awarded_by_name_en/cn/tw |
string | Translated awarder names |
created_at |
datetime | When the star was awarded (RFC3339) |
POST /api/stars
Award stars to a user. Supports both predefined reasons (by ID) and custom text reasons — consistent with the web UI.
Request Body (JSON):
{
"username": "theo",
"reason_id": 2,
"reason": "Cleaned room",
"stars": 2
}| Field | Type | Required | Description |
|---|---|---|---|
username |
string | Yes | Recipient username |
reason_id |
int | No | ID of a predefined reason (uses its translations & default star count) |
reason |
string | No* | Custom reason text (*required if no reason_id) |
stars |
int | No | Number of stars (default: reason's configured count, or 1; negative for penalties) |
When reason_id is provided:
- The reason's configured star count is used unless
starsis explicitly set - Translations are linked automatically
- HA announcements use the translated reason text
Response:
{
"status": "ok",
"counts": [{"Username": "theo", "CurrentStars": 17, ...}],
"starId": 42
}Errors:
| Status | Body | Cause |
|---|---|---|
| 400 | {"error":"invalid JSON"} |
Malformed request body |
| 400 | {"error":"username and reason (or reason_id) required"} |
Missing required fields |
| 400 | {"error":"user not found: xyz"} |
Unknown username |
GET /api/reasons
Returns all predefined star reasons with translations and usage counts.
Response:
[
{
"ID": 1,
"Key": "cleaned_room",
"Translations": {
"en": "Cleaned room",
"zh-CN": "打扫房间",
"zh-TW": "打掃房間"
},
"Count": 15,
"Stars": 1
}
]| Field | Type | Description |
|---|---|---|
ID |
int | Reason ID |
Key |
string | Unique key identifier |
Translations |
map[string]string | Language code to translated text |
Count |
int | Number of times this reason has been used |
Stars |
int | Default star count for this reason |
GET /api/rewards
Returns all configured rewards with translations.
Response:
[
{
"ID": 1,
"Name": "Ice cream outing",
"Cost": 8,
"Icon": "🍦",
"Translations": {
"en": "Ice cream outing",
"zh-CN": "冰淇淋外出",
"zh-TW": "冰淇淋外出"
}
}
]| Field | Type | Description |
|---|---|---|
ID |
int | Reward ID |
Name |
string | English name (backward compat) |
Cost |
int | Star cost to redeem |
Icon |
string | Emoji icon |
Translations |
map[string]string | Language code to translated name |
GET /api/redemptions
Returns redemption history.
Query Parameters:
| Param | Required | Description |
|---|---|---|
user |
No | Filter by username |
Response:
[
{
"ID": 1,
"UserID": 3,
"Username": "theo",
"UsernameEN": "Theo",
"UsernameCN": "西奥",
"UsernameTW": "西奧",
"RewardName": "Ice cream outing",
"RewardNameEN": "Ice cream outing",
"RewardNameCN": "冰淇淋外出",
"RewardNameTW": "冰淇淋外出",
"Cost": 8,
"CreatedAt": "2025-01-15T14:00:00Z"
}
]| Field | Type | Description |
|---|---|---|
ID |
int | Redemption record ID |
UserID |
int | User who redeemed |
Username |
string | Username |
UsernameEN/CN/TW |
string | Translated display names |
RewardName |
string | English reward name (backward compat) |
RewardNameEN/CN/TW |
string | Translated reward names |
Cost |
int | Stars spent (snapshot at redemption time) |
CreatedAt |
datetime | When the redemption occurred (RFC3339) |
Admin Web API
These endpoints require session authentication with admin privileges. They are used by the admin panel's JavaScript and can also be called programmatically.
POST /star
Award a star from the web UI. Supports both form submission and JSON response.
Form Data:
| Field | Required | Description |
|---|---|---|
username |
Yes | Recipient username |
reason_id |
No | ID of a predefined reason |
reason |
Yes* | Reason text (*required if no reason_id) |
stars |
No | Number of stars (default based on reason) |
Set Accept: application/json header to receive JSON instead of a redirect:
{
"counts": [{"Username": "theo", "CurrentStars": 16, ...}],
"awardedBy": "dad",
"starId": 42
}POST /redeem
Redeem a reward. Set Accept: application/json for JSON response.
Form Data:
| Field | Required | Description |
|---|---|---|
reward_id |
Yes | Reward ID to redeem |
username |
Yes | Username redeeming |
JSON Response:
{
"counts": [{"Username": "theo", "CurrentStars": 6, ...}],
"rewardName": "Ice cream outing",
"cost": 8
}Errors: Returns plain text — "user not found", "reward not found", or "{name} doesn't have enough stars (has X, needs Y)".
DELETE /star/{id}
Remove a star record. Cannot delete your own stars.
Response: JSON array of updated user star counts.
DELETE /redemption/{id}
Remove a redemption record. Cannot delete your own redemptions.
Response: JSON array of updated user star counts.
PUT /admin/reason/{id}
Update a reason's translation or star count.
Form Data:
| Field | Required | Description |
|---|---|---|
lang |
No | Language code (en, zh-CN, zh-TW) |
text |
No | Translation text (required with lang) |
stars |
No | New default star count |
retroactive |
No | 1 to update existing star records (default), 0 to only change future awards |
Response: {"status": "ok"}
DELETE /admin/reason/{id}
Delete a reason and all its translations.
Response: {"status": "ok"}
PUT /admin/reward/{id}
Update a reward's translation or cost.
Form Data:
| Field | Required | Description |
|---|---|---|
lang |
No | Language code |
text |
No | Translation text |
cost |
No | New star cost |
retroactive |
No | 1 to update past redemption costs (default), 0 to snapshot current cost in existing redemptions |
Response: {"status": "ok"}
DELETE /admin/reward/{id}
Delete a reward.
Response: HTTP 200
PUT /admin/user/{id}
Update a user's display name translation.
Form Data:
| Field | Required | Description |
|---|---|---|
lang |
Yes | Language code |
text |
Yes | Translated name |
Response: {"status": "ok"}
DELETE /admin/user/{id}
Delete a user and all associated data (stars, redemptions, sessions, translations). Cannot delete your own account.
Response: HTTP 200
POST /admin/toggle-announce
Toggle Home Assistant announcements on/off.
Response:
{"ha_enabled": "1"}GET /admin/export
Download all application data as JSON.
Response: application/json file attachment (star-app-export.json).
Exported data includes: users (without password hashes), stars, reasons, rewards, redemptions, and settings.
POST /admin/import
Import previously exported JSON data. Replaces all existing stars, reasons, rewards, and redemptions.
Form Data: Multipart file upload with field name file (accepts .json).
Home Assistant Integration
Configure from the admin panel under "Home Assistant Announce":
- Enable announcements
- Set your Home Assistant TTS URL (e.g.,
https://home.example.com/api/services/tts/microsoft_say) - Set a Long-Lived Access Token (generate from HA user profile)
- Set the media player entity (e.g.,
media_player.living_room) - Choose the announcement language
When a star is awarded, the app calls Home Assistant's TTS service to announce it. Positive and negative stars get different messages:
| Language | Positive | Negative |
|---|---|---|
| English | "{name} got {n} stars for {reason}!" | "{name} lost {n} stars for {reason}!" |
| 简体中文 | "{name} 因为{reason}获得了 {n} 颗星星!" | "{name} 因为{reason}失去了 {n} 颗星星!" |
| 繁體中文 | "{name} 因為{reason}獲得了 {n} 顆星星!" | "{name} 因為{reason}失去了 {n} 顆星星!" |



