A few hours a week, compounding. At a goose's pace.
A reusable, self-hosted progress tracker for working through any structured goal with friends or colleagues. Drop in a CSV of items, configure your users and timeline, deploy to AWS, and get a shared dashboard with per-user checkboxes, progress tracking, and a countdown to completion.
Runs on AWS free tier.
- Certification study groups (AWS, CKA, CISSP, etc.)
- Monthly reading lists
- 30-day coding challenges
- Quarterly OKRs
- Any N-period goal with M collaborators
- Any interval — week, month, day, year, sprint, quarter
- N users — defined in config, each with their own login
- Per-user checkboxes — you can only check your own; others are read-only
- Progress bars — items completed + hours completed per user
- Countdown — days remaining to completion date
- Persistent state — stored in DynamoDB, survives page refreshes
- Auth — AWS Cognito email/password login
- Static frontend — no server, just S3 + CloudFront
- Themes — clean default theme or dark sci-fi LCARS theme
- Local dev mode — iterate on the UI without deploying
- Validate script — verify all AWS resources are healthy
| Default theme | LCARS theme |
|---|---|
![]() |
![]() |
![]() |
![]() |
┌─────────────────────────────────────────────────────────┐
│ Browser │
│ index.html + app.js + style.css + cadence.json │
└──────────────────────┬───────────────┬──────────────────┘
│ │
static assets API calls
│ (JWT in header)
▼ ▼
┌──────────────┐ ┌──────────────┐
│ CloudFront │ │ API Gateway │
│ (HTTPS CDN) │ │ (HTTP API) │
└──────┬───────┘ └──────┬───────┘
│ │
│ ┌──────┴───────┐
│ │ Cognito │
│ │ JWT Auth │
│ └──────┬───────┘
▼ ▼
┌──────────────┐ ┌──────────────┐
│ S3 Bucket │ │ Lambda │
│ (frontend) │ │ (API logic) │
└──────────────┘ └──────┬───────┘
│
▼
┌──────────────┐
│ DynamoDB │
│ (user state) │
└──────────────┘
Data flow:
- The frontend is a static SPA served from S3 via CloudFront.
- On login, Cognito issues a JWT.
- Every API call includes the JWT in the
Authorizationheader. - API Gateway validates it against the Cognito User Pool before the request reaches Lambda.
- Lambda reads/writes checkbox state in DynamoDB, keyed by the user's email extracted from the JWT claims.
Before you start, you need:
| Requirement | Notes |
|---|---|
| AWS account | Create one free |
| AWS CLI v2 | Install guide — run aws configure to set up credentials |
| Python 3.11+ | python3 --version to check |
| Linux or macOS | Windows users: use WSL2 |
Your AWS credentials need sufficient permissions to create DynamoDB tables, Lambda functions, API Gateway APIs, Cognito User Pools, IAM roles, S3 buckets, and CloudFront distributions. An admin-level IAM user works; a scoped policy is better for production.
Estimated AWS cost: negligible. This project uses services well within the free tier:
- DynamoDB: 25GB storage + 200M requests/month free
- Lambda: 1M requests/month free
- S3: 5GB storage + 20k GET requests/month free
- CloudFront: 1TB data transfer + 10M requests/month free (first 12 months)
- Cognito: 50,000 MAUs free
git clone https://github.com/rdyson/cadence.git
cd cadence
python3 -m venv .venv
source .venv/bin/activate # Windows (WSL): same command
pip install pyyaml boto3cp cadence.example.yaml cadence.yaml
cp items.example.csv items.csvOpen cadence.yaml and set:
name— your project namecompletion_date— your target end date (ISO 8601:YYYY-MM-DD)interval—week,month,day, etc.users— one entry per person, withid,name, andemailaws.region— your preferred AWS region (e.g.eu-west-2,us-east-1)
Open items.csv (or replace it with your own). The build script reads the column names from cadence.yaml → columns, so your CSV just needs a consistent header row.
bash scripts/setup.shThis runs the full setup in one go (~10 minutes):
- Preflight checks — validates AWS CLI, credentials, Python, dependencies
- AWS infrastructure — DynamoDB, Lambda, API Gateway, Cognito, S3
- CloudFront — HTTPS CDN in front of S3
- Build & deploy — builds
cadence.jsonfrom config + CSV, uploads everything
All created resource IDs are written back to cadence.yaml automatically. You can also run the steps individually — see Scripts.
Why CloudFront? S3 website URLs are HTTP only. Cognito requires HTTPS. CloudFront provides HTTPS and is free tier eligible.
Each user in cadence.yaml gets a Cognito account with a randomly generated temporary password, printed in the script output:
✓ Created user: Rob (rob@example.com) — temp password: a8Kz3xQ_mNpR!A1a
✓ Created user: Adam (adam@example.com) — temp password: bT7wYc2_hLsJ!A1a
On first sign-in, Cognito will prompt each user to set their own password. This is handled automatically by the login screen — they'll see a "Set new password" field appear after their first attempt.
Share the dashboard URL and each user's temporary password with them.
Your CSV needs at minimum a title column and a period column. Hours are optional.
Title,Hours,Week
Introduction to the topic,0.5,1
Deep dive: subtopic A,2.0,1
Deep dive: subtopic B,1.5,2Column names must match the columns settings in cadence.yaml. Defaults are Title, Hours, Week.
Rows are automatically skipped if:
- The title is blank
- The title starts with
--(e.g.-- Foo barcomment rows) - The period value is not a valid integer (e.g. section header rows with no week number)
This means you can use a spreadsheet with section headers and totals — Cadence will ignore them cleanly.
See cadence.example.yaml for a fully annotated example.
| Field | Required | Description |
|---|---|---|
name |
✅ | Project display name |
completion_date |
✅ | Target end date (YYYY-MM-DD) |
interval |
✅ | week / month / day / year / sprint / quarter |
csv |
✅ | Path to your CSV (relative to cadence.yaml) |
columns.title |
✅ | CSV column name for item titles |
columns.period |
✅ | CSV column name for period numbers |
columns.hours |
— | CSV column name for time estimates (omit to hide hours) |
users |
✅ | List of { id, name, email } |
theme |
— | default or lcars (dark sci-fi theme) |
period_labels |
— | Override period headings (e.g. 1: "Week 1 — March 2") |
aws.region |
✅ | AWS region |
aws.dynamodb_table |
✅ | DynamoDB table name (set by setup script) |
aws.cognito_user_pool_id |
— | Set automatically by setup-aws.sh |
aws.cognito_client_id |
— | Set automatically by setup-aws.sh |
aws.api_url |
— | Set automatically by setup-aws.sh |
aws.s3_bucket |
— | Set automatically by setup-aws.sh |
aws.cloudfront_url |
— | Set automatically by setup-cloudfront.sh |
CloudFront (HTTPS)
│
▼
S3 Bucket
├── index.html
├── app.js
├── style.css
└── cadence.json ← baked from cadence.yaml + items.csv at deploy time
│ (JWT in Authorization header)
▼
API Gateway (Cognito JWT authorizer)
│
▼
Lambda (lambda_function.py)
│
▼
DynamoDB
└── Table: one item per user, map of checked item titles
How auth works:
- Cognito issues a JWT on login.
- The browser includes it in every API request.
- API Gateway validates the token against your Cognito User Pool before the Lambda ever runs.
- The Lambda extracts the username from the validated claims — no auth logic in application code.
| Script | When to run | Description |
|---|---|---|
scripts/setup.sh |
Once (first time) | Full setup: infrastructure + CloudFront + deploy |
scripts/setup-aws.sh |
Once (first time) | Creates AWS infrastructure only |
scripts/setup-cloudfront.sh |
Once (first time) | Creates CloudFront distribution only |
scripts/deploy.py |
After any changes | Build + upload to S3 + update Lambda |
scripts/build.py |
After editing CSV/config | Builds frontend/cadence.json |
scripts/validate.py |
Anytime | Checks all AWS resources are healthy |
scripts/dev.py |
During development | Local dev server with mock API (no AWS needed) |
scripts/teardown-aws.sh |
To remove everything | Deletes all AWS resources |
setup.sh is the recommended way to get started. The individual setup scripts are safe to re-run — they check for existing resources and skip them.
- Add them to
usersincadence.yaml - Run
bash scripts/setup-aws.sh(skips existing resources, creates the new Cognito user) - Run
python scripts/deploy.py(rebuildscadence.jsonwith the new user column) - Share the dashboard URL + the temporary password from the setup output
Iterate on the frontend without deploying to AWS:
python scripts/dev.pyThis starts a local server at http://localhost:8000 that:
- Serves the frontend from
frontend/ - Mocks the API with a local JSON file (
.dev-state.json) - Auto-builds
cadence.jsonfrom your config - Skips auth — auto-logs in as the first user
- Checkbox state persists across refreshes (stored locally)
Options:
python scripts/dev.py --port 3000 # custom port
python scripts/dev.py --skip-build # don't rebuild cadence.jsonNo AWS credentials, no internet connection required. Edit HTML/CSS/JS, refresh the browser.
Check that all AWS resources exist and are properly configured:
python scripts/validate.pyThis checks: config file, DynamoDB table, Cognito pool + users, Lambda function, API Gateway (including a live 401 test), S3 bucket + files, CloudFront reachability, and IAM role + policies.
Useful for debugging after changes, verifying a fresh setup, or diagnosing "it was working yesterday" issues.
Login fails with "Incorrect username or password"
The user may not have been created. Check that setup-aws.sh completed successfully and that the email in cadence.yaml matches what was used to create the Cognito user.
Checkboxes don't save / API errors in console
Check that aws.api_url is set in cadence.yaml (written by setup-aws.sh). Rebuild and redeploy: python scripts/deploy.py.
Dashboard shows "Error loading cadence.json"
Run python scripts/build.py to generate frontend/cadence.json, then redeploy.
CloudFront returns stale content after deploy
deploy.py creates a CloudFront invalidation automatically. If content still appears stale, wait 1–2 minutes for the invalidation to propagate.
"Access Denied" from S3
The S3 bucket is private by design. Traffic must go through CloudFront. Check that your CloudFront distribution has an Origin Access Control (OAC) set up pointing to the bucket — setup-cloudfront.sh handles this automatically.
To remove all AWS resources created by the setup scripts:
bash scripts/teardown-aws.shThis deletes everything in the correct order — CloudFront, S3, Cognito, API Gateway, Lambda, IAM role, and DynamoDB — and cleans up the generated values in cadence.yaml. You'll be prompted to type destroy to confirm.
To set up again afterwards, re-run the setup scripts as described in Quick Start.
MIT



