Shared finances, without the noise. SplitDuo is a self-hosted expense splitting app for small groups — couples, housemates, travel companions, or anyone sharing costs. No subscription, no third-party servers, no data you don't control.
A focused alternative to Splitwise and Cospend, deployable in one docker compose up.
Expense tracking that stays out of your way Add expenses, pick a split, move on. SplitDuo handles the arithmetic — proportional splits, unequal amounts, custom breakdowns — and keeps a live balance so everyone always knows where they stand.
Settlement-aware balances Rather than a raw transaction log, SplitDuo computes net balances across the whole group and surfaces clear settlement suggestions — minimizing the number of transfers needed to settle up.
Alias mode for subgroup splitting For groups where members act as sub-units (a couple sharing one slot, a household treated as one), turn on alias mode at creation. Members are grouped into named aliases and expenses split by subgroup instead of by person. Balances settle at the alias level.
AI-powered receipt scanning Point your camera at a receipt and SplitDuo prefills the amount, date, and category. Works with any OpenAI-compatible endpoint — bring your own key, keep your data local.
Mobile-first, installable The interface is designed for a phone screen first. Add it to your home screen as a PWA and it behaves like a native app — no app store required.
Invite by email Add your partner via a secure, time-limited invitation link. No account needed on their end until they accept.
Data portability Import existing data from Cospend, Splitwise, or a SplitDuo CSV export. Alias-mode groups use a dedicated three-section CSV format. Export at any time. No lock-in.
Two-factor authentication Each user can independently enable TOTP-based 2FA on their account.
Fully self-hosted Single Docker container. Your server, your database, your rules.
Requires Docker and Docker Compose.
git clone https://gitlab.com/j1mm0/splitduo.git
cd splitduo
docker compose up -dOpen http://localhost:3000 — default login is admin@splitduo.local / changeme123.
Before going to production: set
SD_JWT_SECRET_KEYandSD_INITIAL_USER_PASSWORDto something you control.
Edit docker-compose.yml or pass environment variables directly.
SD_JWT_SECRET_KEY: your-secret-key
SD_INITIAL_USER_EMAIL: admin@splitduo.local
SD_INITIAL_USER_PASSWORD: changeme123SD_DB_HOST: postgres
SD_DB_NAME: splitduo
SD_DB_USERNAME: splitduo
SD_DB_PASSWORD: splitduoSD_BASE_URL: http://localhost:3000
ASPNETCORE_ENVIRONMENT: ProductionAny OpenAI-compatible endpoint works.
SD_AI_BASE_URL: https://api.openai.com
SD_AI_API_KEY: your-api-key
SD_AI_MODEL: gpt-4oRequired for invitation emails and password reset.
SD_EMAIL_SMTP_HOST: localhost
SD_EMAIL_SMTP_PORT: 587
SD_EMAIL_SMTP_USERNAME: ""
SD_EMAIL_SMTP_PASSWORD: ""
SD_EMAIL_SSL: "false"| Layer | Technology |
|---|---|
| Backend | .NET 10, Entity Framework Core |
| Frontend | Vue 3, Nuxt 4, Nuxt UI, TailwindCSS |
| Database | PostgreSQL 17 |
| Deployment | Single Docker container (multi-stage build) |
The frontend is compiled to static files and served directly by the .NET backend — one container, one port, no reverse proxy needed.
cd sd-backend
dotnet restore
dotnet run --project SplitDuo.Api
# → http://localhost:8080cd sd-frontend
pnpm install
pnpm dev
# → http://localhost:3000 (proxies API to :8080)./scripts/dev.sh # Start both
./scripts/dev.sh postgres # PostgreSQL only
./scripts/dev.sh mailpit # Mailpit only
./scripts/dev.sh -d # Drop and recreate volumes firstMailpit (email preview): http://localhost:8025
cd sd-backend
dotnet ef migrations add <MigrationName> --project SplitDuo.Core --startup-project SplitDuo.Api
dotnet ef database update --project SplitDuo.Core --startup-project SplitDuo.ApiReleases follow semantic versioning and are driven by scripts/bump-version.sh, which orchestrates commit-and-tag-version (bumps package.json + VERSION) and git-cliff (changelog), then commits, tags vX.Y.Z, and pushes to trigger the GitLab CI pipeline.
./scripts/bump-version.sh patch # or minor / major
./scripts/bump-version.sh --auto # derive bump from Conventional Commits since last tag
./scripts/bump-version.sh patch -d # dry-run previewThe changelog is generated from Conventional Commits — feat, fix, refactor, style, build, etc. chore and docs commits are excluded. To regenerate the full CHANGELOG.md from all past tags:
pnpm install # one-time, installs git-cliff + commit-and-tag-version at repo root
pnpm changelogBase URL: http://localhost:3000/api/v1
Full spec: OpenAPI YAML
- Project Specification
- System Architecture
- Backend Architecture
- Frontend Architecture
- OpenAPI Spec
- Frontend API Composables
- 2FA Implementation
- CSV Import
- Invitation System
- Receipt Scanning
- PWA