Cafe stamp card app: collect 9 stamps, get a free coffee
Phone loyalty app where cafe regulars collect QR-scanned stamps and redeem a free coffee after 9, with row-level security on all customer data.
exporeact-nativetypescriptsupabasepostgresjest
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 cafe-stamp-card-app-collect-9-stamps-get-a-free-coffee-428711CLAUDE.md# Cafe stamps
## Purpose
This is a loyalty stamp card app for one neighborhood cafe. Customers use it on iPhone and Android to collect stamps. Staff confirm each stamp at the counter by scanning the customer's card QR code. After 9 stamps, the customer redeems a free coffee, and staff confirm the redemption with a PIN.
## Stack
- Expo SDK (latest) with Expo Router
- TypeScript in strict mode
- Supabase for auth and Postgres
- jest-expo for tests
- ESLint for lint
## Commands
- `npm install`: install dependencies
- `npx expo start`: run the dev server and open in Expo Go
- `npm run typecheck`: TypeScript check
- `npm run lint`: lint the project
- `npm test -- --ci`: run tests once, non-interactive
- `npx expo export --platform web`: smoke build, must succeed before a task is done
## Before you start
- Read SPEC.md before coding any feature. It defines what to build and what is out of scope.
- Read PLAN.md to find the current phase. Tick finished items in PLAN.md.
- Read REVIEW.md before merging a phase.
@REVIEW.md
## Secrets
- Never commit `.env` or real keys.
- Use only placeholders in `.env.example`: `EXPO_PUBLIC_SUPABASE_URL` and `EXPO_PUBLIC_SUPABASE_ANON_KEY`.
- The staff PIN lives in Supabase, never in code or in this repo.
## Data and security rules
- Every new Supabase table must have row-level security (RLS) enabled, with policies in a migration file under `supabase/migrations`.
- Customers can read only their own card and stamps.
- Stamp counts change only through the server function `award_stamp`. Client code never writes stamp counts directly.
- Stamp rules live in one pure function in `src/lib/stamps.ts`: 9 stamps to reward, and a 2-minute cooldown between stamps on one card. Keep unit tests for it passing.
## Gotchas
- Expo Go cannot run some native modules. Check the Expo docs before adding a native library.
- Camera and notification features need permission strings in `app.json` before they work on a device.
- Test on a real phone with Expo Go for camera and QR scanning. Simulators may not have a camera.
## Code conventions
- Keep screens under 200 lines. Split into components when a screen gets larger.
- Use touch targets of at least 44 points.
- Give buttons and QR images accessibility labels.
- Do not log names or emails. Do not show personal data in error messages.
- Add a dependency only when the current phase needs it.
## Working rules
- Work in small steps.
- Run the `verify` skill before saying a task is done.
- Commit after each green step, with a short clear message.
- Use `/clear` between unrelated tasks.
- Use `/effort` to lower thinking for simple tasks and raise it for hard ones.
- Ask before any command that pushes code, submits to a store, or changes the production database.
.claude/agents/test-runner.md---
name: test-runner
description: Runs the project typecheck, lint and test commands and reports only the failures. Use for test output so the main session stays clean.
model: haiku
maxTurns: 8
tools: Bash, Read
---
You run the project's checks and report only what failed.
## Steps
1. Run these commands in order from the project root:
- `npm run typecheck`
- `npm run lint`
- `npm test -- --ci`
2. Run each command even if an earlier one failed, so the report covers all three.
3. For each failing command, identify the failing files and the first error line for each file.
## Report format
- If every command passes, reply with one line: `All checks passed: typecheck, lint, tests.`
- If anything fails, reply with a short list:
- One heading per failing command, for example `npm test -- --ci: failed`.
- Under each heading, one bullet per failing file: `path/to/file.ts: first error line`.
- If a failure has no file (for example a missing script), give the first error line only.
## Rules
- Never edit, create or delete files.
- Never print full logs. Use `tail` or `grep` to pull only the error lines you need.
- Do not suggest fixes unless the main session asks for them.
- Read a file only when needed to confirm a file path in an error message.
.claude/settings.json{
"permissions": {
"defaultMode": "default",
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Bash(rm -rf *)",
"Bash(git push --force *)"
],
"ask": [
"Bash(git push *)",
"Bash(npx eas submit *)",
"Bash(npx eas build *)",
"Bash(supabase db push *)"
],
"allow": [
"Bash(npm run typecheck)",
"Bash(npm run lint)",
"Bash(npm test *)",
"Bash(npx expo export *)",
"Bash(git status)",
"Bash(git diff *)"
]
}
}
.claude/skills/supabase-migration/SKILL.md---
name: supabase-migration
description: Use when adding or changing tables, functions or policies in supabase/migrations, so row-level security, the award_stamp function and tests stay correct.
---
# Supabase migration
Follow these steps for any schema, function or policy change.
## Steps
1. Create a new file in `supabase/migrations/` with a UTC timestamp in the name, for example `20261106120000_add_redemptions.sql`. Use `supabase migration new <name>` if the CLI is available.
2. Never edit a migration that already exists. To fix one, add a new migration.
3. In the same migration that creates a table, enable RLS:
```sql
create table public.redemptions (
id uuid primary key default gen_random_uuid(),
card_id uuid not null references public.cards(id),
redeemed_at timestamptz not null default now(),
confirmed_by uuid
);
alter table public.redemptions enable row level security;
```
4. Write one policy per operation (select, insert, update, delete). Each policy checks `auth.uid()`. Customers can read only their own card:
```sql
create policy "customers read own cards"
on public.cards for select
to authenticated
using (user_id = auth.uid());
```
5. Do not give clients direct write access to stamp data. Revoke insert and update on `stamps` and `cards.stamps_count` from `authenticated` and `anon`. Stamp counts change only through `award_stamp`.
6. `award_stamp` must be a `security definer` function with a fixed `search_path`. It must:
- check the staff PIN,
- check the 2-minute cooldown since the last stamp on that card,
- check the card has fewer than 9 stamps,
- apply the rate limit from SPEC.md.
The rules must match `src/lib/stamps.ts`. If they differ, stop and ask the user which one is correct.
7. Add a test in `supabase/tests/` (SQL test) or a documented manual script. It must prove that a second signed-in user cannot read the first user's card or stamps.
8. Run `supabase db reset` locally. Report success only if it finishes with no errors.
Never run `supabase db push` without asking the user first, because it changes the hosted project.
## Checklist before reporting
- [ ] New timestamped file; no old migration edited.
- [ ] RLS enabled on every new table, in the same migration.
- [ ] One policy per operation, each using `auth.uid()`.
- [ ] `award_stamp` is the only path that changes stamp counts.
- [ ] A test proves another user cannot read the card.
- [ ] `supabase db reset` passed locally.
- [ ] The verify skill has been run after this change.
.claude/skills/verify/SKILL.md---
name: verify
description: Use after any code change and before saying a task is done, to run typecheck, lint, tests and the export smoke build for this Expo and Supabase app.
---
# Verify
Run these four steps in order, in this session. Stop at the first failure.
1. `npm run typecheck`
2. `npm run lint`
3. `npm test -- --ci`
4. `npx expo export --platform web`
## If a step fails
- Find the root cause and fix it.
- Do not skip, disable or weaken a check to get green.
- Do not delete or skip tests. Do not add `@ts-ignore`, `eslint-disable`, `.skip` or `.only` to pass.
- Rerun from the first failing step, not from the middle.
## Report
Give one line per step: `typecheck: pass` or `typecheck: fail`.
For a failure, add the last 5 relevant lines of output. Do not paste full logs.
## Migrations
If the change touches `supabase/migrations/`:
- List the migration files that changed.
- Remind the user to run `supabase db reset` locally to rebuild the database from the migrations.
- Do not run `supabase db push`. That needs the user's approval.
## Done rule
Say a task is done only when all four steps pass in this session.
If you edit code after a passing run, run all four steps again.
INSTALL.md# Setup guide
This guide is for a first-time Claude Code and Expo user. Follow it in order.
## What each file is for
- `CLAUDE.md`: short project memory that Claude reads at the start of each session.
- `SPEC.md`: what the app does and what is out of scope. You fill in the placeholders.
- `PLAN.md`: the five build phases, each with a ready-made `/goal` prompt.
- `REVIEW.md`: the checklist used before merging each phase.
- `INSTALL.md`: this guide.
- `.claude/settings.json`: permissions that ask before risky commands and block secret files.
- `.claude/skills/`: reusable instructions for verification and database migrations.
- `.claude/agents/test-runner.md`: a small helper that runs tests and reports only failures.
## 1. Install the tools
1. Install Node.js LTS from nodejs.org.
2. Install Git from git-scm.com.
3. Install Claude Code with the official installer in the Claude Code docs.
4. Open a terminal and run `claude --version`. You should see a version number.
## 2. Set up the project folder
1. Create a folder named `cafe-stamps` and copy in the files listed above.
2. Open `SPEC.md` and fill in the cafe name, the brand color, and the open questions.
3. Run `git init` in the folder, then make a first commit with the docs:
```
git add .
git commit -m "Add project docs"
```
## 3. Create free accounts and keys
1. Create a free Expo account at expo.dev.
2. Create a free Supabase project at supabase.com.
3. Copy the project URL and the anon key from the Supabase API settings.
4. In the project folder, create a file named `.env`:
```
EXPO_PUBLIC_SUPABASE_URL=your-project-url
EXPO_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
```
5. Check that `.env` is listed in `.gitignore` before you commit anything else.
6. Never paste these values into a committed file or a chat.
## 4. Get the app on your phone
1. Install Expo Go from the App Store or Google Play.
2. Later, when Phase 1 is done, run `npx expo start` in the terminal.
3. Scan the QR code it shows with your phone camera (iPhone) or with Expo Go (Android).
## 5. Start Claude Code
1. Open a terminal in the project folder and run `claude`.
2. When asked, trust the folder.
3. Run `/plugin` and install these from claude-plugins-official:
- code-review
- commit-commands
- security-guidance
4. Claude asks before changing files or running risky commands. Read each request before you approve it.
## 6. Start Phase 1
1. Copy the Phase 1 `/goal` prompt from PLAN.md, or use the kickoff prompt Claude was given.
2. Paste it into Claude Code.
3. Watch the progress. Claude commits after each green step.
About `/goal`:
- `/goal` keeps Claude working until a check passes. A small fast model judges the check using only what Claude has shown in the conversation.
- Every `/goal` prompt in PLAN.md includes a turn limit. If the check does not pass by then, Claude stops and writes a summary.
- `/goal clear` stops it at any time.
When Phase 1 finishes, read the summary. Then run `/clear` and start Phase 2 with its `/goal` prompt from PLAN.md. Repeat for each phase.
## 7. Cost and token habits
- `/clear` between unrelated tasks.
- `/compact` at natural breaks in a long task.
- `/usage` to see where your tokens went.
- `/effort` to lower thinking for simple tasks, or raise it for hard bugs. Change it between tasks when you can.
## 8. Troubleshooting
- **`/goal` stopped early.** Read the last check output. Fix the first failing step, then run the same `/goal` again. Use `/goal clear` first if it is still active.
- **`claude` command not found.** Close and reopen the terminal, then run `claude --version` again. If it still fails, reinstall Claude Code.
- **QR code does not load in Expo Go.** Check that your phone and computer are on the same network, then restart `npx expo start`.
- **Supabase errors about keys.** Check that `.env` has the URL and anon key with no extra spaces, then restart `npx expo start`.
- **A native library does not work in Expo Go.** Check the Expo docs for that library before using it. Some native modules need a development build.
- **Camera or notification features do nothing on your phone.** Check that the permission text is in `app.json`.
## 9. Before any store submission
- The owner must review the app before any submission to the App Store or Google Play.
- Phase 5 prepares a TestFlight and Play internal test build. Publishing to the stores is a separate, manual step.
- Claude asks before running `eas build` or `eas submit`. Approve those only when you are ready.
PLAN.md# Build plan
Five phases. Finish one phase, merge it, run `/clear`, then start the next phase with its `/goal` prompt.
Tick items as you finish them: `- [ ]` becomes `- [x]`.
---
## Phase 1: Scaffold
Tasks:
- [ ] Create the Expo Router app with the TypeScript strict template.
- [ ] Add npm scripts: `typecheck`, `lint`, `test`.
- [ ] Set up eslint.
- [ ] Set up jest-expo and add one empty smoke test.
- [ ] Add `.env.example` with placeholder values only.
- [ ] Confirm `.env` is listed in `.gitignore`.
Definition of done:
- `npm run typecheck`, `npm run lint`, `npm test -- --ci` and `npx expo export --platform web` all exit 0 in the same session.
- Exit codes and last output lines are shown in the conversation.
Helpers: use the `verify` skill before saying the phase is done. Use the `test-runner` subagent when test output is long.
Goal prompt:
```
/goal Phase 1 of PLAN.md is done: an Expo Router TypeScript strict app exists in this folder with npm scripts typecheck, lint and test, one passing jest-expo smoke test, a .env.example with placeholders only, and the web export builds. Check: run `npm run typecheck && npm run lint && npm test -- --ci && npx expo export --platform web`, then print each exit code and the last 5 lines of output in the conversation. Constraints: read CLAUDE.md, SPEC.md, PLAN.md and REVIEW.md first. Scaffold with `npx create-expo-app@latest app-tmp --template default`, move its contents into this folder, and delete app-tmp. If the folder already has app code, stop and ask. Install only what the template, strict TypeScript, eslint and jest-expo need. Never create a .env with real keys. Do not edit SPEC.md. Tick Phase 1 items in PLAN.md only. Commit after each green step with a short message. Stop and ask before any command that needs a password, a paid service, or a push. Turn bound: stop after 25 turns and write a summary of what passed and what still fails.
```
---
## Phase 2: Customer auth and card screen
Tasks:
- [ ] Add the Supabase client, reading the URL and anon key from `EXPO_PUBLIC_` variables.
- [ ] Sign-in screen with Supabase email magic link.
- [ ] Create a profile row on first login.
- [ ] Card screen shows a QR code of the card id.
- [ ] Card screen shows 9 empty stamp slots.
- [ ] Add a sign-in test (mocked Supabase client).
- [ ] Add a card screen render test.
Definition of done:
- Sign-in test and card screen render test pass.
- Typecheck, lint and the verify steps pass.
- Touch targets are at least 44 points. QR image and buttons have accessibility labels.
Helpers: use the `verify` skill before saying the phase is done. Use the `test-runner` subagent for test output. Screens stay under 200 lines.
Goal prompt:
```
/goal Phase 2 of PLAN.md is done: a customer can sign in with an email magic link, a profile row is created on first login, and the card screen shows a QR code of the card id plus 9 empty stamp slots. Check: run `npm run typecheck && npm run lint && npm test -- --ci`, and show the output of the sign-in test and the card screen render test in the conversation, with all exit codes 0. Constraints: read SPEC.md and PLAN.md first. Mock the Supabase client in tests; never call the live project. Do not add native libraries without checking the Expo docs first. Keep each screen under 200 lines. Do not edit SPEC.md; tick Phase 2 items in PLAN.md only. Commit after each green step. Stop and ask before any push or paid service. Turn bound: stop after 25 turns and write a summary of what passed and what still fails.
```
---
## Phase 3: Database and stamping
Tasks:
- [ ] Migration for profiles, cards and stamps, with RLS enabled in the same file.
- [ ] One policy per operation on each table, using `auth.uid()`.
- [ ] `award_stamp` security definer function: checks staff PIN, 2-minute cooldown and stamp count.
- [ ] Rate limit on award calls.
- [ ] Pure stamp rules in `src/lib/stamps.ts`.
- [ ] Unit tests for `stamps.ts`.
- [ ] Staff Stamp screen with PIN entry and camera QR scan.
- [ ] SQL test, or a documented manual test script, proving another user cannot read a card.
Definition of done:
- Unit tests for `stamps.ts` pass.
- SQL tests pass, or the manual test script is written in `supabase/tests/` and marked as pending.
- `supabase db reset` runs locally with no errors.
- Verify steps pass.
Helpers: use the `supabase-migration` skill for all schema work. Use the `verify` skill before saying the phase is done. Use the `test-runner` subagent for test output. Camera and notification features need permission strings in `app.json` before they work on a device.
Goal prompt:
```
/goal Phase 3 of PLAN.md is done: migrations for profiles, cards and stamps exist with RLS enabled and one policy per operation; the award_stamp function checks the staff PIN, the 2-minute cooldown and the stamp count; the staff Stamp screen scans a customer QR code; and unit tests for src/lib/stamps.ts pass. Check: run `npm run typecheck && npm run lint && npm test -- --ci`, run `supabase db reset` locally, and show all exit codes and the last 10 lines of each in the conversation. Also show either the SQL test output or the path of the manual test script, which must prove another user cannot read a card. Constraints: read SPEC.md and PLAN.md first. Use the supabase-migration skill. Never edit an old migration. Never run `supabase db push`; ask first. Stamp counts change only through award_stamp, never from client code. Do not edit SPEC.md; tick Phase 3 items in PLAN.md only. Commit after each green step. Turn bound: stop after 25 turns and write a summary of what passed and what still fails.
```
---
## Phase 4: Redemption and history
Tasks:
- [ ] Reward-ready state when stamps reach 9.
- [ ] Redeem button on the card screen.
- [ ] Staff PIN confirmation for redemption, checked on the server.
- [ ] Insert a row in `redemptions` on successful confirmation.
- [ ] Block a second redemption for the same reward.
- [ ] History screen listing stamps with date and time.
- [ ] Tests for the reward threshold (8 stamps no reward, 9 stamps reward ready).
- [ ] Tests for double-redeem prevention.
Definition of done:
- Reward threshold and double-redeem tests pass.
- Verify steps pass.
- A customer cannot read another customer's history.
Helpers: use the `verify` skill before saying the phase is done. Use the `test-runner` subagent for test output. Keep redemption logic on the server, not in client code.
Goal prompt:
```
/goal Phase 4 of PLAN.md is done: a card with 9 stamps shows Reward ready, Redeem asks for the staff PIN and is confirmed on the server, a second redemption for the same reward is blocked, and the history screen lists stamps with date and time. Check: run `npm run typecheck && npm run lint && npm test -- --ci`, and show in the conversation the output of the reward threshold test (8 and 9 stamps) and the double-redeem test, with exit codes 0. Constraints: read SPEC.md and PLAN.md first. Redemption rules live on the server, not only in client code. Do not edit SPEC.md; tick Phase 4 items in PLAN.md only. Commit after each green step. Stop and ask before any push or any change to an existing migration. Turn bound: stop after 25 turns and write a summary of what passed and what still fails.
```
---
## Phase 5: Release prep
Tasks:
- [ ] App icon and splash screen using the brand color from SPEC.md.
- [ ] Privacy text in the app and in a `PRIVACY.md` draft.
- [ ] Store listing checklist in `docs/store-checklist.md`.
- [ ] `eas.json` with a preview profile for internal testing.
- [ ] Set permission strings in `app.json` for camera.
- [ ] Owner review: the owner reads the app and the checklist before any store submission.
Definition of done:
- `npx expo export --platform web` exits 0.
- `npx eas build --platform all --profile preview` is started, or the manual steps left are listed clearly.
- The owner has reviewed the app. Store submission is not done by Claude.
Helpers: use the `verify` skill before saying the phase is done. EAS build and submit commands ask for approval by project settings; do not run `eas submit`.
Goal prompt:
```
/goal Phase 5 of PLAN.md is done: the app has an icon and splash in the brand color, privacy text and a store checklist exist, eas.json has a preview profile, and the camera permission string is set in app.json. Check: run `npm run typecheck && npm run lint && npm test -- --ci && npx expo export --platform web`, show all exit codes in the conversation, and then run `npx eas build --platform all --profile preview`. If the EAS build cannot run without a login or paid step, stop and write a clear list of the manual steps left instead. Constraints: read SPEC.md and PLAN.md first. Never run `npx eas submit`. Do not edit SPEC.md; tick Phase 5 items in PLAN.md only. Commit after each green step. Stop and ask before any command that needs a password or a paid service. Turn bound: stop after 25 turns and write a summary of what passed and what still fails.
```
REVIEW.md# Review checklist
Run this list before merging each phase. Start with `/code-review` for a first pass, then check every item yourself.
## Secrets and personal data
- [ ] No secrets or real keys in any committed file.
- [ ] `.env` is listed in `.gitignore` and is not tracked by git.
- [ ] No names or emails in `console.log` or in error messages shown to users.
## Database and security
- [ ] Every new table has RLS enabled and one policy per operation.
- [ ] A test proves another user's card cannot be read.
- [ ] Stamp counts and the cooldown are enforced in `award_stamp` on the server, not in client code.
- [ ] Client code never writes `stamps_count` directly.
## Stamp rules
- [ ] Unit tests cover 8 to 9 stamps, the cooldown boundary, and the reward state.
- [ ] Rules live in `src/lib/stamps.ts` and are not repeated in screens.
## Checks
- [ ] typecheck, lint, tests and the export smoke build all pass in the same session.
- [ ] The verify skill ran and reported pass for all four steps.
## Scope and code size
- [ ] The diff matches the current phase in PLAN.md.
- [ ] No unrelated refactors.
- [ ] Screens are under 200 lines.
- [ ] No unused dependencies were added.
- [ ] Any new native library was checked against the Expo docs.
## Accessibility
- [ ] Touch targets are at least 44 points.
- [ ] Buttons and QR images have accessibility labels.
SPEC.md# Cafe stamp card: product spec
Fill in the placeholders and open questions before Phase 2. Claude reads this file before building any feature.
## Problem
Regulars lose paper stamp cards. Staff want a quick way to add stamps that is hard to fake.
## Owner placeholders
- Cafe name: [CAFE NAME]
- Brand color: [#RRGGBB]
- Staff PIN: set in Supabase only. Never put it in code, in this file, or in git.
## Users
- Customers: regulars with a phone (iPhone or Android).
- Staff: one shared staff device, or a staff PIN entered at the counter.
## Core flows
1. Sign up. The customer enters an email, gets a magic link, and signs in. A card is created and its QR code is shown on screen.
2. Stamp. Staff opens the Stamp screen, enters the staff PIN, and scans the customer's QR code. One stamp is added per purchase.
3. Reward. When stamps reach 9, the card shows "Reward ready". The customer taps Redeem, and staff confirm with the PIN.
4. History. The customer sees a list of stamps with date and time.
## Rules
- 9 stamps earn one free coffee.
- Cooldown: at least 2 minutes between stamps on one card.
- Stamps are added only by the server function `award_stamp`.
- Award calls are rate limited.
- Customers can read only their own card and stamps.
## Data model
- profiles(id, display_name)
- cards(id, user_id, stamps_count, created_at)
- stamps(id, card_id, created_at, added_by)
- redemptions(id, card_id, redeemed_at, confirmed_by)
## Anti-fraud
- Cooldown is enforced on the server.
- The staff PIN is checked inside `award_stamp`.
- Award calls are rate limited.
- Row-level security is enabled on every table.
## Out of scope for v1
- Payments
- Push notifications
- Multiple cafes
- Customer chat
- Web admin
## Open questions for the owner
- Does the card reset after redemption?
- Can a card hold more than 9 stamps?
- What happens to unused stamps after 6 months?