Kep is a Thai-first personal expense tracker. It helps users manually add expenses, upload Thai bank e-slip images, run client-side OCR to draft expense data, confirm the draft, and track daily, weekly, and monthly budget limits.
Tagline: Know where your money went.
- Google sign-in with Supabase Auth.
- Protected dashboard with daily, weekly, and monthly budget cards.
- Manual expense CRUD with Zod and React Hook Form validation.
- Client-side Thai + English OCR with Tesseract.js for e-slip draft extraction.
- Best-effort slip parsing for amount, date/time, receiver, bank, and reference ID.
- Expense list with search, date range, and category filters.
- Budget settings with enable/disable, limits, warning threshold, and Monday-start weeks.
- Category settings with default categories plus custom archive/restore behavior.
- General settings for base currency, theme preference, and slip storage preference placeholder.
- PWA manifest and mobile-first layout with bottom navigation.
- Next.js App Router
- TypeScript
- Tailwind CSS
- Supabase Auth, Postgres, SSR helpers
- Tesseract.js
- React Hook Form
- Zod
- lucide-react icons
- ESLint and Prettier
- pnpm
pnpm install
cp .env.example .env.local
pnpm check:env
pnpm devOpen http://localhost:3000.
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
NEXT_PUBLIC_SITE_URL=http://localhost:3000NEXT_PUBLIC_SITE_URL defaults to http://localhost:3000 in local development if it is omitted.
- Create a Supabase project.
- Copy the project URL and anon key into
.env.local. - Apply the migration in
supabase/migrations/202605220001_initial_schema.sql. - Confirm RLS is enabled for
profiles,categories,expenses, andbudgets. - In Auth settings, add the local callback URL:
http://localhost:3000/auth/callback
For production, also add:
https://YOUR_DOMAIN/auth/callback
- In Google Cloud Console, create or choose a project.
- Configure the OAuth consent screen.
- Create an OAuth client ID for a web application.
- Add Supabase's Google callback URL from Supabase Auth provider settings to Google authorized redirect URIs.
- Paste the Google client ID and secret into Supabase Auth > Providers > Google.
- Add your app callback URLs in Supabase Auth URL configuration.
Apply SQL through the Supabase dashboard SQL editor, Supabase CLI, or your migration pipeline.
supabase db pushIf you are not using the Supabase CLI yet, apply migrations in order through the Supabase SQL editor:
supabase/migrations/202605220001_initial_schema.sql— initial schemasupabase/migrations/20260603000000_add_heartbeat_table.sql— heartbeat tablesupabase/migrations/20260613000000_dedupe_categories_unique.sql— dedupe categories and add(user_id, name)unique constraint
The app creates profile, default categories, and disabled default budgets idempotently after login. There is no database auth trigger in this MVP.
pnpm dev
pnpm check:env
pnpm lint
pnpm typecheck
pnpm build
pnpm format
pnpm format:check- Import the repository in Vercel.
- Set the same environment variables.
- Set
NEXT_PUBLIC_SITE_URLto the production origin. - Add the production auth callback URL in Supabase and Google OAuth.
- Deploy with the default Next.js build command:
pnpm buildOCR is best-effort. Kep never auto-saves OCR results. The user must review and confirm the draft. Slip images are processed in the browser and are not uploaded or permanently stored by default.
The parser (src/lib/ocr/slip-parser.ts) is tested with anonymized OCR text fixtures — no real slip images.
Rules for fixtures:
- Never commit real slip images, real account numbers, real names, or real transaction references.
- Fixtures must contain only fake values that resemble realistic Thai bank slip OCR output.
- Fixtures live in
src/lib/ocr/__tests__/fixtures/.
Run parser tests:
pnpm test # all tests
pnpm test:ocr # OCR parser onlyAdding a new anonymized fixture:
- Create a
.txtfile insrc/lib/ocr/__tests__/fixtures/with fake Thai bank slip OCR text. - Use fake account numbers (e.g.
xxx-x-xx123-x), fake names, fake references. - Add a
describeblock insrc/lib/ocr/__tests__/slip-parser.test.tsasserting the fields you expect to extract. - Run
pnpm test:ocrto confirm all assertions pass.
Supabase Free projects pause after 1 week of inactivity. A GitHub Actions workflow pings the project twice a week to prevent this.
One-time setup:
-
Apply the heartbeat migration in the Supabase SQL editor:
supabase/migrations/20260603000000_add_heartbeat_table.sql -
Add two repository secrets in GitHub → Settings → Secrets and variables → Actions:
SUPABASE_URL— your Supabase project URL (e.g.https://xxxx.supabase.co)SUPABASE_ANON_KEY— your Supabase anon public key
Never use the service role key here.
-
To test manually: Actions → Supabase Heartbeat → Run workflow.
The workflow runs Monday and Thursday at 03:00 UTC. It reads a single harmless row from the heartbeat table using the public anon key. No user data is involved.
- No live exchange rates.
- No paid OCR, QR parsing, bank API, or slip verification API.
- No slip image storage by default.
- Budget periods use practical local date calculations and no rollover.
- Analytics are intentionally minimal for the MVP.