A full-stack budgeting app for iOS and Android, built with Expo and Supabase. Track accounts and transactions, set a monthly budget, and log expenses by typing, scanning a receipt, or speaking to it.
Poonji ("पूंजी", Hindi for capital/wealth) started from a public React Native + Expo + Supabase course project and has been substantially reworked since: rebranded end to end (naming, bundle identifiers, visual identity, email templates), audited for repo hygiene, and extended with functionality that wasn't part of the original build. The sections below cover setup, schema, and the backend jobs.
- Accounts: multiple accounts (cash, bank, credit card, savings) with a default account and running balances
- Transactions: income/expense tracking with categories, search, filters, and daily income vs. expense charts
- AI receipt scanning: snap or pick a photo of a receipt and let Gemini extract the amount, category, and description
- AI voice entry: describe a transaction out loud ("I spent 400 on groceries yesterday") and have it transcribed and parsed automatically
- Monthly budget: set a budget and track spend against it on the dashboard
- CSV export: export recent transactions to CSV and share them from the Transactions screen
- AI assistant: ask questions about your spending
- Onboarding: first-run currency and starting balance setup
- Expo (SDK 54) + Expo Router
- Clerk for authentication
- Supabase (Postgres + Row Level Security) as the backend, authenticated via Clerk's native third-party auth integration (no Supabase JWT template needed)
- Google Gemini for receipt/voice extraction
- NativeWind (Tailwind for React Native)
- Zustand
- TanStack Query
- React Hook Form + ZOD
The repository can export a credential-free web shell for portfolio review. Without Clerk or Supabase variables, protected screens remain unavailable and the data layer stays inert; no secret or fake production account is committed. Add the real public variables below in Vercel to enable authentication and live data.
Copy .env.example to .env for local development:
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=
EXPO_PUBLIC_SUPABASE_URL=
EXPO_PUBLIC_SUPABASE_KEY=
EXPO_PUBLIC_GEMINI_API_KEY=- Build command:
pnpm build:web - Output directory:
dist - Framework preset: Other
- Add the four
EXPO_PUBLIC_*variables in the Vercel project settings for production auth/data. - Attach
finance.swayam.spaceto the Vercel project, then create the DNS record Vercel shows at your DNS provider. - Expo Router static export is enabled via
expo.web.output = static; deep links are emitted as static routes.
The custom domain and DNS record require access to the Vercel project and the domain registrar, so they cannot be completed from this source archive alone.
-
Install dependencies
pnpm install
-
Add a
.envfile in the project root with:EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY= EXPO_PUBLIC_SUPABASE_URL= EXPO_PUBLIC_SUPABASE_KEY= EXPO_PUBLIC_GEMINI_API_KEY= -
In your Supabase project, add Clerk as a Third-Party Auth provider (Authentication → Sign In / Providers → Clerk) so
auth.jwt()->>'sub'resolves to the Clerk user id. This app does not use the legacy Supabase JWT template approach. -
Run the Supabase queries below to set up the schema.
-
Start the app
pnpm exec expo start -c
In the output, you'll find options to open the app in a
- development build
- Android emulator
- iOS simulator
- Expo Go, a limited sandbox for trying out app development with Expo
create table users (
clerk_id text primary key,
email text not null,
name text,
image_url text,
currency text, -- null until the user completes onboarding
created_at timestamp with time zone default now()
);alter table users enable row level security;
create policy "Users can insert own row"
on users for insert
with check (clerk_id = auth.jwt()->>'sub');
create policy "Users can read own row"
on users for select
using (clerk_id = auth.jwt()->>'sub');
create policy "Users can update own row"
on users for update
using (clerk_id = auth.jwt()->>'sub');create table accounts (
id uuid default gen_random_uuid() primary key,
user_id text not null references users(clerk_id) on delete cascade,
name text not null,
type text not null, -- 'CASH' | 'BANK' | 'CREDIT_CARD' | 'SAVINGS'
balance numeric not null default 0,
is_default boolean not null default false,
created_at timestamp with time zone default now()
);alter table accounts enable row level security;
create policy "Users can manage own accounts"
on accounts for all
using (user_id = auth.jwt()->>'sub')
with check (user_id = auth.jwt()->>'sub');create table transactions (
id uuid default gen_random_uuid() primary key,
user_id text not null references users(clerk_id) on delete cascade,
account_id uuid not null references accounts(id) on delete cascade,
type text not null, -- 'INCOME' | 'EXPENSE'
amount numeric not null,
category text not null,
description text,
date timestamp with time zone not null default now(),
status text not null default 'COMPLETED',
input_method text not null default 'MANUAL', -- 'MANUAL' | 'RECEIPT_SCAN' | 'VOICE'
voice_transcript text,
is_flagged boolean not null default false,
flag_reason text,
created_at timestamp with time zone default now(),
updated_at timestamp with time zone default now()
);alter table transactions enable row level security;
create policy "Users can manage own transactions"
on transactions for all
using (user_id = auth.jwt()->>'sub')
with check (user_id = auth.jwt()->>'sub');-- One budget per user (simple monthly budget, no per-category breakdown)
create table budgets (
id uuid default gen_random_uuid() primary key,
user_id text not null unique references users(clerk_id) on delete cascade,
amount numeric not null,
last_alert_sent timestamp with time zone,
last_alert_threshold numeric, -- last budget-usage % (80 or 100) emailed for, resets each calendar month
created_at timestamp with time zone default now(),
updated_at timestamp with time zone default now()
);alter table budgets enable row level security;
create policy "Users can manage own budget"
on budgets for all
using (user_id = auth.jwt()->>'sub')
with check (user_id = auth.jwt()->>'sub');Two Supabase Edge Functions in supabase/functions/ run on a schedule and email the user via Resend:
check-budget-alerts: runs daily, emails a user the first time they cross 80% and again the first time they cross 100% of their monthly budget (tracked vialast_alert_sent/last_alert_threshold, resets each calendar month).weekly-tips: runs every Monday, asks Gemini for a few short, personalized tips based on each user's last 7 days of transactions and emails them.
Neither is called from the app; they're standalone, timer-driven backend jobs that live entirely on Supabase.
-
Link the project (one-time; skip if using the dashboard only)
pnpm exec supabase login pnpm exec supabase init pnpm exec supabase link --project-ref <your-project-ref>
-
Apply the DB schema: Supabase dashboard → SQL Editor → run
scripts/schema.sql. -
Deploy the two edge functions: either:
pnpm exec supabase functions deploy check-budget-alerts pnpm exec supabase functions deploy weekly-tips
or from the dashboard: Edge Functions → Deploy a new function, paste in the contents of the corresponding
index.ts(and the files under_shared/), and give it the same name as the folder, no CLI required. -
Set secrets (project-wide, covers both functions): either
pnpm exec supabase secrets set KEY=valueper line below, or dashboard → Edge Functions → Secrets:RESEND_API_KEY= RESEND_FROM_EMAIL= # "Poonji <alerts@yourdomain.com>" once you verify a domain in Resend; # until then, use "Poonji <onboarding@resend.dev>", the address must # stay onboarding@resend.dev and only delivers to the email you signed # up with, but the display name (the "Poonji" part) is yours to set GEMINI_API_KEY= # same Gemini key as EXPO_PUBLIC_GEMINI_API_KEY, without the EXPO_PUBLIC_ prefix EMAIL_LOGO_URL= # any public URL to the Poonji logo, used in the email header; # omit this secret entirely to fall back to a text "Poonji" wordmarkSUPABASE_URLandSUPABASE_SERVICE_ROLE_KEYare provided automatically in the edge function runtime. Supabase reserves theSUPABASE_prefix, so you can't (and don't need to) set these yourself. -
Schedule them: SQL Editor → run
scripts/cron_jobs.sql, after filling in your real project URL and service-role key in the twovault.create_secret(...)calls at the top.
Once done: check-budget-alerts runs daily at 9am UTC, weekly-tips runs Mondays at 9am UTC.
Easiest way: dashboard → Edge Functions → select the function → use the built-in test/invoke panel to trigger it on demand and see the response and logs right there.
CLI alternative: curl the deployed URL directly (this CLI version has no functions invoke subcommand):
curl -i --location --request POST 'https://<project-ref>.supabase.co/functions/v1/check-budget-alerts' \
--header 'Authorization: Bearer <anon-or-service-role-key>'A response of {"sent": 0} just means no user currently qualifies. For check-budget-alerts you need a test user whose spend is ≥80% of their budget, for weekly-tips a test user with a transaction in the last 7 days. last_alert_sent/last_alert_threshold block repeat sends within the same month, so reset them on your test budget row between runs if you want to re-trigger the same threshold.
MIT, see LICENSE.