A free, mobile-first Splitwise alternative. Passwordless (email-token) accounts, friends with push notifications, groups with fully versioned/audited expenses, and transitive per-currency debt simplification.
- Backend: PHP 8.5, Laravel 12 (REST API), PostgreSQL, Sanctum auth, Web Push
- Frontend: React 19, Redux Toolkit (RTK Query), Bootstrap 5, Vite PWA
- Infra: Docker Compose, Mailhog for local mail testing, docker-mailserver for the
production-shaped mail path (see
docker/mailserver/README.md)
scripts/setup.shThis copies backend/.env.example to backend/.env, builds and starts the containers, and
generates a VAPID keypair into backend/.env - required before push notifications will work
(friend requests use them).
backend/.env is gitignored on purpose (it ends up holding the VAPID private key); everything
else in it is safe local-dev defaults out of the box.
docker compose up -d
- Frontend (PWA): http://localhost:5173
- Landing page (marketing site, production: www.split-even-wiser.com): http://localhost:8080
- Backend API: http://localhost:8000/api
- Sent mail (Mailhog UI - login links land here in dev): http://localhost:8026
- Postgres: localhost:5433 (off the default 5432 to avoid clashing with a host-local Postgres)
The app container's entrypoint installs Composer dependencies, generates APP_KEY if
missing, runs migrations, and symlinks public/storage on every boot. The frontend
container runs npm install && npm run dev on boot. Both are safe to restart freely.
- Open http://localhost:5173, enter any email, and hit "Send me a login link."
- Open http://localhost:8026 (Mailhog), open the mail, click the login link.
- First time with that email, you'll be asked to pick a username - after that you're in.
- Create a group, add a friend/member, add an expense, watch the balance update.
scripts/test.shThere's no CI/CD pipeline - deploys are a manual git pull on the VPS followed by a couple of
docker compose commands using the prod overlay (docker-compose.prod.yml). Always pass both
compose files together; the base file alone starts the dev-only frontend vite server and skips
nginx's prod config/certbot:
ssh <user>@split-even-wiser.com
cd split-even-wiser
git pull
scripts/deploy-backend.shbackend/ is bind-mounted into app/queue, so PHP source changes are visible immediately -
restarting re-runs docker/php/entrypoint.sh, which applies pending migrations
(php artisan migrate --force) and re-symlinks public/storage. The restart mainly matters so
the long-lived queue:work process picks up the new code and so migrations actually run.
If composer.json/composer.lock changed, pass --install (the entrypoint only runs
composer install when vendor/ is missing, which it won't be after the first deploy):
scripts/deploy-backend.sh --installIf docker/php/Dockerfile changed (new PHP extensions/system packages), pass --build to
rebuild the image instead of just restarting:
scripts/deploy-backend.sh --buildThe SPA is a static build baked into a shared volume that nginx serves from /var/www/frontend;
it does not rebuild automatically on up:
scripts/deploy-frontend.shThis runs npm ci && npm run build against VITE_API_URL=https://split-even-wiser.com/api and
writes straight into the volume nginx reads from - no restart needed afterwards.
Landing page (www.split-even-wiser.com)
Plain static HTML/CSS/JS in landing/, no build step - nginx serves it straight from a
bind mount, so a change just needs nginx to pick up the new files:
scripts/deploy-landing.shscripts/deploy-logs.shmailserver and certbot only need touching when their own config changes - see
docker/mailserver/README.md and docker/certbot/init-letsencrypt.sh.
backend/ Laravel 12 API
frontend/ React 19 + Vite PWA
landing/ Static English marketing page, served on www.split-even-wiser.com
docker/ Dockerfiles, nginx config, mailserver config
scripts/ Setup, test, and deploy scripts referenced throughout this README