Habit tracker with weekly digest and group cheers
React and Supabase habit tracker with a weekly email digest and cheer groups of up to 5 members.
reactvitetypescriptsupabaseresend
Built 1 time, downloaded 0 times.
Install from your terminal
Run this in your project folder. It signs you in, shows what changes and asks before it overwrites a file.
npx claude-lab add habit-tracker-with-weekly-digest-and-group-cheers-12525cCLAUDE.md# the app
the app is a personal habit tracker. Users check in on daily habits, see their streaks, and get a weekly email digest on Sunday evening in their own timezone. Users can also join small cheer groups of up to 5 friends, where members see each other's check-in counts and send short cheers. Habit names stay private unless the user opts in.
The spec lives in SPEC.md and the build plan in PLAN.md. Read them before large changes.
## Commands
- `npm run dev`: start the Vite dev server
- `npm run typecheck`: TypeScript check, no emit
- `npm run lint`: ESLint over the project
- `npm test -- --run`: Vitest unit tests, single run
- `npx playwright test`: end-to-end tests (run when UI files change)
- `npm run build`: production build
- `npm run digest:preview`: render the weekly digest to an HTML file for review
## Architecture
- `src/features/habits`: habit CRUD, daily check-ins, streak calculation
- `src/features/groups`: groups, invite codes, membership, cheers
- `src/features/digest`: pure digest generator (no network calls)
- `src/lib/supabase.ts`: the single Supabase client setup
- `supabase/migrations`: schema, RLS policies, triggers
- `supabase/functions/weekly-digest`: scheduled function that sends email through Resend
Keep feature code inside its feature folder. Shared helpers go in `src/lib` only when two features need them.
## Conventions
- TypeScript strict mode is on. Do not use `any`. Use `unknown` and narrow it.
- Function components only. No class components.
- Server state goes through TanStack Query. Do not fetch in components directly.
- Validate every input boundary with Zod: form input, API responses, webhook or function payloads.
- Tests sit next to the code they cover (`*.test.ts`, `*.test.tsx`). Playwright tests live in `e2e/`.
- Use Intl for date and time work. Never hard-code offsets.
## Gotchas
- The 5-member group cap is enforced in two places: a database trigger on `group_members` (the real guard) and the UI (for clear feedback). A UI-only check is a bug.
- Every table needs row level security enabled, with policies written in the same migration that creates the table.
- Store each user's timezone on `profiles`. Compute the digest send time (Sunday 18:00) and the week boundaries in that timezone, including DST weeks.
- Cheers are visible to group members. The digest and the UI must say so.
- The local `/code-review` reads CLAUDE.md but not REVIEW.md. The import below makes it read the review checklist too.
- `DRY_RUN` must be respected by the digest sender. Tests never send email.
@REVIEW.md
## Secrets
- Never commit `.env` or `.env.local`. Both must be listed in `.gitignore`.
- Use only the placeholder names `SUPABASE_URL`, `SUPABASE_ANON_KEY`, and `RESEND_API_KEY`. Real values go in `.env.local` only.
- The Supabase service role key never goes into client code or the Vite bundle.
- Do not read `.env` or `.env.local` into the conversation. Refer to variable names only.
## Ask before
- Changing migrations on tables that already exist
- Changing the digest schedule or its timing rules
- Adding a new dependency (state the reason in the PR or commit message)
## Definition of done
Before saying a task is done, run the `verify` skill. It runs typecheck, lint, unit tests, and build, and runs Playwright when UI files changed. Report only the failures. Do not claim done while a step fails.
## Working style
- Explore, plan, code, then verify. Give yourself a way to check each change.
- Keep changes scoped. No unrelated refactors in the same change.
- Use the `test-runner` subagent for long test output so logs stay out of the main context.
- Use `/clear` between unrelated tasks and `/compact` at natural breaks.
.claude/agents/test-runner.md---
name: test-runner
description: Runs the requested npm or npx check (tests, typecheck, lint, build, Playwright) and reports only PASS or FAIL with the failing names and first error lines. Use it to keep long test output out of the main context.
model: haiku
tools: Bash, Read, Grep, Glob
maxTurns: 15
---
You run one check command for the the app project and report the result.
## Steps
1. Run the exact command you were given, for example `npm test -- --run` or `npx playwright test`. If no command is given, ask for one and stop.
2. Note the exit code.
3. If the exit code is 0, reply PASS with the command and a count of tests passed, if the output shows one.
4. If the exit code is not 0, reply FAIL and list each failing test or step by name.
5. For each failure, include the first 20 lines of its error output and nothing after them.
## Rules
- Never edit, create or delete files. Your job is to report, not to fix.
- Never print full logs unless the caller explicitly asks for them.
- Never read `.env`, `.env.local` or any other env file, even to debug a failure.
- Do not re-run a failing command more than once unless the caller asks.
- Keep the reply under 60 lines.
## Reply format
```
PASS or FAIL: <command>
Exit code: <n>
Failing: <name>, <name>
<first 20 lines of error for each failure>
```
.claude/settings.json{
"model": "sonnet",
"permissions": {
"allow": [
"Bash(npm run *)",
"Bash(npm test *)",
"Bash(npx vitest *)",
"Bash(npx playwright test *)",
"Bash(npx tsc *)",
"Bash(npx eslint *)",
"Bash(git status*)",
"Bash(git diff*)",
"Bash(git log*)"
],
"ask": [
"Bash(git push *)",
"Bash(supabase db push *)",
"Bash(supabase migration repair *)"
],
"deny": [
"Bash(rm -rf *)",
"Bash(git push --force *)",
"Bash(git push -f *)",
"Bash(git reset --hard *)",
"Read(./.env)",
"Read(./.env.local)"
]
},
"env": {}
}
.claude/skills/email-digest/SKILL.md---
name: email-digest
description: Rules for editing weekly digest generation or email templates. Use when changing digest content, the HTML or plain-text email, timezone logic, or the Resend send path.
---
# Email digest
The weekly digest is sent Sunday 18:00 in each user's timezone. It summarises the past 7 days of habit completion, current streaks and cheers received. It is skipped when the user has no active habits.
Do not change the Sunday 18:00 schedule without asking first (see CLAUDE.md).
## Pure function rule
- Digest content is built by a pure function in `src/features/digest`.
- Input is plain data: profile, habits, logs, cheers and a reference time. Output is a content object with subject, HTML and text.
- No network calls. Do not create a Supabase or Resend client inside this function. Do not read env vars there.
- Sending happens only in `supabase/functions/weekly-digest`. That function calls the pure function, then Resend.
## Template rules
- Use inline CSS. Do not rely on `<style>` blocks or external stylesheets.
- Table layout is not required. The email must still render correctly in common clients (Gmail, Outlook, Apple Mail). Keep widths simple and avoid flexbox and grid.
- Always generate a plain-text alternative from the same data. Both versions must contain the same facts.
- Keep the subject short and plain.
## Required content
- An unsubscribe link in both the HTML and text versions.
- A note that cheers are visible to group members. Suggested wording: "Cheers you receive are visible to members of your group."
- Habit names appear only in the recipient's own section. Never show them in group sections (SPEC visibility rule).
## Timezone
- Compute week boundaries in the user's stored timezone with `Intl.DateTimeFormat` (or `formatToParts`). Do not use server local time or fixed UTC offsets.
- Send time is Sunday 18:00 in the user's timezone.
- Add a unit test for a DST week, such as the week the clocks change. Check the 7-day window and the send time.
## Testing and preview
- Preview with `npm run digest:preview`. It writes HTML to disk for review.
- Never send email in tests. Tests call the pure function only.
- Read `RESEND_API_KEY` from the environment only, inside the send function. Never hardcode it, log it or print it in preview output.
- Respect `DRY_RUN`. When it is set, the send function logs what it would send and does not call Resend.
## Before finishing
- The pure function has no side effects.
- HTML and plain-text versions match.
- Unsubscribe link and cheer-visibility note are present.
- The DST test is added or updated.
- Run the verify skill.
.claude/skills/verify/SKILL.md---
name: verify
description: Run the real typecheck, lint, test and build checks for this project before declaring any task done. Use it before saying work is finished. Report only failures.
allowed-tools: Bash(npm run *), Bash(npx *), Read, Grep
---
# Verify
Use this skill before telling the user a task is done. Run the checks in order and stop at the first failure.
## Steps
1. `npm run typecheck`
2. `npm run lint`
3. `npm test -- --run`
4. `npm run build`
5. `npx playwright test`, only if UI files changed. Check with `git diff --name-only`. UI files are components, pages, styles and routes under `src/`.
If digest code changed (`src/features/digest` or `supabase/functions/weekly-digest`):
- Run `npm run digest:preview`.
- Confirm the HTML file was written. Read the output path the script prints and check that the file exists and is not empty.
## Long output
Test and build output can be long. Delegate steps 3 to 5 to the `test-runner` subagent so logs stay out of the main context. Ask it for PASS or FAIL per command, with failing test names and the first 20 lines of each error.
## Report format
Report each step on one line:
```
typecheck: PASS
lint: PASS
test: FAIL
<first error lines>
build: not run (stopped at first failure)
```
Show error lines for failing steps only. Do not paste output from passing steps.
## Rules
- Never say a task is done while any step fails.
- If a step is skipped, say why (for example, "playwright: skipped, no UI files changed").
- If a check fails, fix the cause and rerun from the first failing step.
- Do not read `.env` or `.env.local`.
INSTALL.md# Setup guide
This guide takes you from an empty folder to your first Claude Code session on the app.
## Prerequisites
- Node 20 or newer. Check with `node --version`. Install from nodejs.org if needed.
- A free Supabase account at supabase.com.
- A Resend account at resend.com. Use a placeholder key for now and keep the real key out of git.
- Git installed. Check with `git --version`.
## Install Claude Code
1. Install Claude Code with the official installer from Anthropic's Claude Code docs.
2. Run `claude` once and log in with your account.
3. The first session starts in auto mode when no permission mode is set. To choose a different default, set `permissions.defaultMode` in `.claude/settings.json` (shared) or `~/.claude/settings.json` (all projects). For example, `"defaultMode": "default"` asks before risky actions.
## Create the project
1. Create the app:
```
npm create vite@latest the-app -- --template react-ts
cd the-app
npm install
```
2. Run `git init` and make a first commit. Claude Code needs a clean baseline to diff against.
## Add the project files
Copy these from the plan into the project root:
- `CLAUDE.md`
- `SPEC.md`
- `PLAN.md`
- `REVIEW.md`
- the `.claude` folder (settings, the verify skill, the email-digest skill and the test-runner agent)
Commit them after copying.
## Install plugins
First add the skills marketplace:
```
claude plugin marketplace add anthropics/skills
```
Then install each plugin:
```
claude plugin install code-review@claude-plugins-official
claude plugin install feature-dev@claude-plugins-official
claude plugin install commit-commands@claude-plugins-official
claude plugin install security-guidance@claude-plugins-official
claude plugin install example-skills@anthropic-agent-skills
```
What each one adds:
- **code-review** reviews diffs against REVIEW.md before you merge or push.
- **feature-dev** guides each phase through explore, plan and build.
- **commit-commands** makes clean, consistent commits so each phase is easy to review and roll back.
- **security-guidance** flags secrets in diffs and risky database or auth code.
- **example-skills** adds frontend-design for UI work and webapp-testing for the Phase 5 Playwright tests.
## Set up environment files
1. Create `.env.example` with these placeholder lines and nothing else:
```
SUPABASE_URL=YOUR_SUPABASE_URL
SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
RESEND_API_KEY=YOUR_RESEND_API_KEY
```
2. Copy it to `.env.local` and fill in your real values there only.
3. Confirm git ignores the file:
```
git check-ignore .env.local
```
The command should print `.env.local`. If it prints nothing, add `.env.local` to `.gitignore` before going further.
## Start the first session
1. Open a terminal in the project folder and run `claude`.
2. Check that CLAUDE.md loaded. Ask Claude what commands the project uses.
3. You only need `/init` if CLAUDE.md is missing. Your copy already has one.
4. Paste the kickoff prompt below as your first message:
```
/goal Phase 1 of PLAN.md is complete: a Vite + React + TypeScript app (working name the app) exists at the repo root, and all four commands exit with code 0: npm run typecheck, npm run lint, npm test -- --run, npm run build. Also npm run dev serves a page whose heading reads the app. Check by running each command yourself and showing the exit codes in the conversation, because the evaluator only sees what you show. Constraints: TypeScript strict mode on; no Supabase, email or auth code yet; do not create or commit .env files or any secret; do not edit SPEC.md or REVIEW.md; you may edit CLAUDE.md only to correct a command that does not work; you may tick Phase 1 boxes in PLAN.md; keep dependencies to react, react-dom, vite, typescript, vitest, @testing-library/react, jsdom, eslint and its React plugins; add one passing smoke test in src/App.test.tsx. Use the test-runner subagent for test output so logs stay out of the main context. Turn bound: stop after 25 turns even if unfinished, then report exactly which checks pass, which fail, and what remains.
```
Claude keeps working until the Phase 1 checks pass or it reaches 25 turns. Check its exit-code report yourself before you move on.
## Between phases
- Run `/clear` before each new phase so it starts with a fresh context. Claude reads PLAN.md and CLAUDE.md again, so nothing important is lost.
- Run `/compact` at a natural break inside a long phase.
- Run `/usage` now and then to see where tokens are going.
- Before you say a phase is done, run the verify skill. It runs the real checks and reports only failures.
PLAN.md# the app build plan
Five phases. Each phase ends with a `/goal` prompt that Claude can check, then a checkbox update and a short summary of what remains. Run `/clear` between phases.
Before starting: follow INSTALL.md, then paste the Phase 1 prompt.
## Phase 1: scaffold
- [ ] Vite React TypeScript app at the repo root
- [ ] Vitest and Testing Library configured
- [ ] ESLint configured with React plugins
- [ ] Smoke test in `src/App.test.tsx`
- [ ] Heading reads the app
```
/goal Phase 1 of PLAN.md is complete: npm run typecheck, npm run lint, npm test -- --run and npm run build each exit 0, and npm run dev serves a page with the heading the app. Run each command yourself and show the exit codes. Constraints: strict TypeScript; no Supabase, email or auth code; no .env files or secrets; do not edit SPEC.md or REVIEW.md; you may edit CLAUDE.md only to fix a command that does not work; you may tick Phase 1 boxes in PLAN.md. Dependencies limited to react, react-dom, vite, typescript, vitest, @testing-library/react, jsdom, eslint and its React plugins. Use the test-runner subagent for test output. Stop after 25 turns and report which checks pass, which fail, and what remains.
```
When done: tick the boxes above and write a short summary of what remains.
## Phase 2: database, auth and habits
- [ ] Supabase migrations for profiles, habits and habit_logs
- [ ] Row level security on every table
- [ ] Magic link sign-in with timezone saved to profiles
- [ ] Habit create, list, and tick for today
- [ ] Streak calculation in `src/features/habits/streak.ts` with unit tests
- [ ] Integration tests for RLS on habits
```
/goal Phase 2 of PLAN.md is complete: npm run typecheck, npm run lint and npm test -- --run exit 0; supabase db reset applies all migrations to the local Supabase instance without error; the RLS tests in supabase/tests pass against local Supabase. Run each command yourself and show the exit codes. Constraints: do not run supabase db push or any command that writes to a remote database; do not edit existing migrations, only add new ones; no secrets in code; the streak function must have unit tests for today ticked, today not ticked, and a gap day; use the test-runner subagent for test output. Stop after 25 turns and report which checks pass, which fail, and what remains.
```
When done: tick the boxes above and write a short summary of what remains.
## Phase 3: groups and cheers
- [ ] Groups with random invite codes
- [ ] Join by invite link
- [ ] Database trigger that rejects a sixth member, with an integration test
- [ ] UI check that shows the cap error, marked as feedback only
- [ ] Cheers with a 140-character limit and one per sender per log
- [ ] Visibility rules tested for cheers and check-in counts
```
/goal Phase 3 of PLAN.md is complete: npm run typecheck, npm run lint and npm test -- --run exit 0; the group cap and cheer uniqueness integration tests pass against local Supabase; a sixth member insert is rejected by the trigger in the database. Run each command yourself and show the exit codes. Constraints: the trigger is the source of truth, and the UI check is only feedback; do not edit existing migrations; do not run supabase db push; cheers must reject messages over 140 characters in both the Zod schema and the database; use the test-runner subagent for test output. Stop after 25 turns and report which checks pass, which fail, and what remains.
```
When done: tick the boxes above and write a short summary of what remains.
## Phase 4: weekly digest
- [ ] Digest generator as a pure function in `src/features/digest`
- [ ] Unit tests for zero active habits, DST week boundaries and cheers
- [ ] `npm run digest:preview` writes an HTML file
- [ ] Plain-text alternative and unsubscribe link in the template
- [ ] Supabase scheduled function that sends via Resend, guarded by `DRY_RUN`
```
/goal Phase 4 of PLAN.md is complete: npm run typecheck, npm run lint and npm test -- --run exit 0; npm run digest:preview exits 0 and writes an HTML file that contains an unsubscribe link; the scheduled function reads DRY_RUN and defaults to dry run when it is unset. Run each command yourself and show the exit codes. Constraints: the digest builder makes no network calls; tests never call Resend and never need a real RESEND_API_KEY; do not add real keys to any file; do not change the digest schedule without asking; use the test-runner subagent for test output. Stop after 25 turns and report which checks pass, which fail, and what remains.
```
When done: tick the boxes above and write a short summary of what remains.
## Phase 5: end-to-end tests and deployment
- [ ] Playwright tests for signup, check-in, group join and cheer
- [ ] Accessibility pass: labels, keyboard use, contrast
- [ ] Deployment notes in `docs/deployment.md`
- [ ] All verify skill steps pass
```
/goal Phase 5 of PLAN.md is complete: npx playwright test exits 0 with the signup, check-in, group join, cheer and accessibility tests all passing; npm run build exits 0; npm run typecheck, npm run lint and npm test -- --run exit 0; docs/deployment.md lists the Supabase project setup, the environment variable names, and the DRY_RUN setting. Run each command yourself and show the exit codes. Constraints: no real keys in the repo; do not change the digest schedule; do not add new dependencies without a stated reason; run the verify skill before saying done; use the test-runner subagent for test output. Stop after 25 turns and report which checks pass, which fail, and what remains.
```
When done: tick the boxes above and write a short summary of what remains.
REVIEW.md# Review checklist
Use this list with `/code-review` and when reading changes yourself. Report each finding as blocker, should-fix, or nit, with file and line.
## Security
- Row level security is enabled on every new table, with policies for select, insert, update and delete.
- The Supabase service role key never appears in client code, in files the browser loads, or in the Vite bundle.
- No secrets in diffs: no `.env`, `.env.local`, real keys or tokens. Only placeholders belong in `.env.example`.
- Auth changes keep magic link sign-in tied to the signed-in user's own rows.
## Group cap
- Any code path that adds a group member goes through the database trigger that enforces the 5-member limit.
- A UI-only check is at least a should-fix, and a blocker if it is the only check.
- The trigger has a test that tries to add a sixth member and expects a failure.
## Digest
- Week boundaries and the Sunday 18:00 send time use the stored user timezone.
- Every digest includes an unsubscribe link and a note that cheers are visible to group members.
- `DRY_RUN` is respected: when it is set, nothing calls Resend.
- No email is sent when the user has zero active habits.
- Digest content comes from a pure function in `src/features/digest` with no network calls.
## Accessibility
- Form controls have a visible label, or an `aria-label` when a visible label is not possible.
- Check-in buttons are reachable and operable with the keyboard, and show a visible focus state.
- Text and controls meet sufficient colour contrast (at least 4.5:1 for body text).
## Tests
- New logic has unit tests in Vitest, including streak and digest code.
- Bug fixes include a regression test that would have failed before the fix.
- Changes to signup, check-in, group join or cheer flows are covered by Playwright tests.
## Scope
- No unrelated refactors mixed into a feature or a fix.
- No new dependencies without a stated reason in the commit message or PR description.
- Changes to migrations on existing tables, or to the digest schedule, are flagged for a human decision.
## Output format
- Group findings as blocker, should-fix, or nit.
- Give each finding as file and line, then one sentence on the problem and one on the fix.
- End with a verdict: approve, or request changes with the number of blockers.
SPEC.md# the app spec
## Purpose
the app is a web app for personal habit check-ins. Users tick habits each day, see their streaks, and join small cheer groups of up to 5 friends. Each week, every user gets an email digest with their progress and any cheers they received.
## Scope
In scope for v1:
- Email magic link sign-in
- Habits with check-ins and streaks
- Cheer groups with invite links, capped at 5 members
- Cheers on check-ins
- Weekly email digest via Resend
Out of scope for v1:
- Payments
- Native mobile app
- Push notifications
- Public feeds
- Group chat
## User stories
- As a new visitor, I sign up with an email magic link.
- As a user, I create habits with a name and an optional daily target note, such as "10 minutes".
- As a user, I tick a habit for today and see it marked done.
- As a user, I see my current streak for each habit.
- As a user, I create a group or join one with an invite link.
- As a group member, I cheer a friend's check-in with a short message.
- As a user, I receive a weekly email with my progress and cheers.
## Data model
- `profiles`: `id` (matches the auth user), `timezone` (IANA name such as `Europe/London`)
- `habits`: `id`, `user_id`, `name`, `daily_target` (optional text), `active` (boolean)
- `habit_logs`: `id`, `habit_id`, `log_date` (date in the user's timezone); unique on (`habit_id`, `log_date`)
- `groups`: `id`, `name`, `invite_code` (unique)
- `group_members`: `group_id`, `user_id`; primary key (`group_id`, `user_id`); a trigger rejects any insert that would make a sixth member
- `cheers`: `id`, `log_id`, `from_user_id`, `message` (max 140 characters); unique on (`log_id`, `from_user_id`)
Every table has row level security enabled.
## Rules
Check-ins:
- A check-in means the habit is done for that day.
- Ticking a habit twice on the same day stores one log.
- Streak counts consecutive days with a log, ending today. If today has no log yet, the streak ends yesterday and does not break until the day ends in the user's timezone.
Groups:
- A group holds at most 5 members. The database trigger is the source of truth; the UI check is only for feedback.
- Invite codes are random and unique.
Cheers:
- One cheer per sender per check-in.
- Messages are 1 to 140 characters.
Visibility:
- Group members see each other's check-in counts and cheers only.
- Habit names are never shown to other members unless the user opts in.
## Weekly digest
- Sent Sunday 18:00 in the user's timezone.
- Covers the past 7 days.
- Includes the completion rate per habit, current streaks, and cheers received.
- Skipped if the user has no active habits.
- Each email has an unsubscribe link and a note that cheers are visible to group members.
- The digest content is built by a pure function with no network calls.
## Acceptance criteria
Each criterion maps to a test. Unit tests use Vitest; end-to-end tests use Playwright.
- AC-01: Magic link sign-in creates a profile with the browser's timezone. Test: `e2e/auth.spec.ts`
- AC-02: A user can create a habit, and it appears in today's list. Test: `e2e/habits.spec.ts`
- AC-03: Ticking a habit twice on one day stores one log. Test: `src/features/habits/logs.test.ts`
- AC-04: Streak counts consecutive days ending today, or yesterday if today is not yet ticked. Test: `src/features/habits/streak.test.ts`
- AC-05: A valid invite code adds the user to the group. Test: `e2e/groups.spec.ts`
- AC-06: A sixth member insert is rejected by the database trigger, and the UI shows an error. Test: `supabase/tests/group_cap.test.ts`
- AC-07: A cheer over 140 characters is rejected. Test: `src/features/groups/cheers.test.ts`
- AC-08: A second cheer from the same sender on the same log is rejected. Test: `supabase/tests/cheers_unique.test.ts`
- AC-09: A non-member cannot read another group's cheers or logs. Test: `supabase/tests/rls_groups.test.ts`
- AC-10: A group member cannot see another member's habit names. Test: `supabase/tests/rls_habits.test.ts`
- AC-11: A user with no active habits gets no digest. Test: `src/features/digest/digest.test.ts`
- AC-12: Week boundaries use the user's timezone, including a week with a DST change. Test: `src/features/digest/digest.test.ts`
- AC-13: `npm run digest:preview` writes an HTML file. Test: `src/features/digest/preview.test.ts`
- AC-14: A keyboard-only user can tick a habit and submit a cheer. Test: `e2e/accessibility.spec.ts`
- AC-15: Tests never send email. Test: `src/features/digest/send.test.ts` checks that the send path is blocked when `DRY_RUN` is set.
## Open questions
- Should the digest include groups the user left during the week?
- Should a user be able to remove a cheer after sending it?