Web-first group bill-splitting and restaurant tracker for a single shared group.
- React + React Router 7 + TypeScript + Vite + Tailwind CSS
- Fastify + TypeScript API with JWT auth and Swagger at
/api/docs - PostgreSQL + Prisma schema, migration, and seed data
- Shared TypeScript package for enums, DTO-shaped types, and bill-splitting math (Vite resolves
@ff-restaurent/sharedto source during web development, but API/root production builds require compiled output) - Docker Compose for Postgres, API, and static web frontend
Use Docker Compose for the fastest full-stack setup. It starts PostgreSQL, runs API migrations, seeds demo data when the database is empty, and serves the web app.
docker compose up --buildNo .env file is required for local Docker usage. The Compose file defaults to
development settings. If you provide overrides, keep NODE_ENV=development
locally so demo seeding remains enabled.
Then open:
- Web: http://localhost:5173
- API health: http://localhost:4000/health
- API docs: http://localhost:4000/api/docs
Demo logins, all using password123:
customer(Casey Customer)sous(Sam Sous Chef)head(Hana Head Chef)
Use this path when you want to run the API and Vite dev servers directly. The root .env.example is the canonical environment contract. No app-local .env files are required or supported.
Start a PostgreSQL 16-compatible database first, then copy .env.example to .env at the project root and ensure it has a host URL reachable from your machine:
DATABASE_URL=postgresql://ff:ff@localhost:5432/ff_restaurent?schema=public
JWT_SECRET=replace-with-a-long-random-secret
JWT_EXPIRES_IN=8h
CORS_ORIGINS=http://localhost:5173
REGISTRATION_INVITE_CODE=replace-with-a-private-group-invite
ROOT_ADMIN_USERNAME=replace-with-an-existing-username
API_PORT=4000
VITE_API_URL=http://localhost:4000npm install
npm run build -w @ff-restaurent/shared
# Reset local database
npx prisma migrate reset --schema apps/api/prisma/schema.prisma --force
# Run migrate database
npm run prisma:migrate -w @ff-restaurent/api
npm run prisma:seed -w @ff-restaurent/apiThe demo seed also loads the popular Vietnamese cuisine catalog. To add only that catalog without resetting application data, run:
npm run prisma:cuisines:seed -w @ff-restaurent/apiThis command is idempotent: it inserts missing normalized cuisine names and does not update or delete existing catalog entries. API container deployments run it automatically after Prisma migrations and before the API starts.
To test locally as ROOT_ADMIN using one of your own accounts instead of the
seeded head user, register or seed that user first, set
ROOT_ADMIN_USERNAME in .env to its username, then promote it:
npm run prisma:root:bootstrap -w @ff-restaurent/apiRun the API and web app in separate terminals:
npm run dev -w @ff-restaurent/apinpm run dev -w @ff-restaurent/webOpen the same local URLs listed above.
If the web app throws Invalid hook call / Cannot read properties of null (reading 'useContext') after pulling changes that add or update npm
dependencies, the Vite dev server's dependency pre-bundle cache is stale.
Clear it and restart (clean cache on browser, too):
rm -rf apps/web/node_modules/.vite
# PowerShell
Remove-Item -Recurse -Force apps/web/node_modules/.viteRemove-Item -Recurse -Force apps\web\node_modules\.vite -ErrorAction SilentlyContinueImage uploads are mediated by the API; the Supabase service-role key must never
be exposed through a VITE_* variable or committed to Git.
Create these buckets in the Supabase dashboard:
ff-public-images: public, allowed MIME typesimage/jpeg,image/png, andimage/webp, maximum file size 5 MiB. It stores user avatars and restaurant logos/banners.ff-payment-qr: private, the same MIME allowlist, maximum file size 2 MiB. The API serves these objects through short-lived signed URLs.
Set SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, SUPABASE_PUBLIC_BUCKET,
SUPABASE_QR_BUCKET, and optionally SUPABASE_SIGNED_URL_TTL_SECONDS (default
900). The backend validates file signatures in addition to the bucket rules.
Without these variables the rest of the app remains available, while media
endpoints return STORAGE_NOT_CONFIGURED.
The API bundles the complete 34-province, 3,321-ward directory effective July
1, 2025. Address pickers use the authenticated /address/provinces and
/address/provinces/:provinceCode/wards endpoints without contacting an
external province service. Existing numeric address snapshots are preserved
and remapped by province and ward name when edited.
npm run prettier:check
npm run typecheck
npm test
npm run buildThe highest-risk bill math lives in packages/shared/src/bill-splitting.ts and is covered by Vitest tests for even splits, explicit origin costs, percentage discounts, and validation. The API intentionally uses node:test for its test runner, while the web and shared packages use Vitest.
- i18n & Theme Support: Multi-language interface (Vietnamese default, English toggle) and customizable theme options (Light, Dark, System mode).
- Personalized Stats: Interactive visualization of spending habits via Recharts (including payment status, cuisine breakdown, monthly trends, and restaurant frequency).
- Favorites: Per-user favorites junction table allowing users to save their go-to eateries.
- CUSTOMER: View participant bills, mark their own share paid, view personal stats, and filter bills by payment status.
- SOUS_CHEF: Create bills, edit bills, send payment reminders, create/edit restaurant entries, and recommend eateries.
- HEAD_CHEF: View all bills (including archived) and archive/restore bills and restaurants.
- ROOT_ADMIN: Inherits Head Chef permissions and exclusively manages member roles and system administration.
All money values are stored and calculated as integer amounts (using VND as the default currency).