Free and Open Source Alternative to Splitwise. Share expenses with your friends and family.
See the codeSpliit is a free and open source alternative to Splitwise. You can either use the official instance at Spliit.app, or deploy your own instance:
The project is open to contributions. Feel free to open an issue or even a pull-request! Join the discussion in the Spliit Discord server.
Spliit is free, open source, and has no ads. Hosting, database and API costs are paid for by donations. If you want to help keep it that way, you can:
Contributions of any size are appreciated, and so is simply telling people about the project.
The project's translations are managed using our Weblate project. You can easily add missing translations to the project or even add a new language! Here is the current state of translation:
./scripts/start-local-db.sh if you don’t have a server already..env.example as .envnpm install to install dependencies. This will also apply database migrations and update Prisma Client.npm run dev to start the development serverThe Playwright suite in e2e/ drives a real browser against the app running in
Docker, so it exercises the same image users deploy. It needs Docker and a free
port 3000, and nothing else — the stack builds itself from your checkout and
throws its database away afterwards.
npm run e2e
That builds the image, starts app + PostgreSQL from compose.e2e.yaml, waits
for /api/health/readiness, runs the suite and tears everything down. It never
touches your development stack or ./postgres-data.
While writing tests it is quicker to keep the stack up:
npm run e2e:up # build and start, then leave it running
npm run e2e:test -- --ui # iterate (also --headed, --grep, --debug)
npm run e2e:report # open the HTML report of the last run
npm run e2e:down # stop and delete the test database
--ui opens Playwright's UI mode, where you can pick tests, watch them run and
step through a trace. It does not start the stack itself, so run npm run e2e:up
first.
If port 3000 is already taken — by npm run dev, for instance — set
E2E_HOST_PORT on every command of the session, including the test run:
E2E_HOST_PORT=3100 npm run e2e # one-shot
E2E_HOST_PORT=3100 npm run e2e:up # or, for the iteration loop
E2E_HOST_PORT=3100 npm run e2e:test -- --ui
E2E_HOST_PORT=3100 npm run e2e:down
The same suite runs in GitHub Actions from the E2E workflow, which can be triggered manually and runs automatically on release tags.
npm run build-image to build the docker image from the Dockerfilecontainer.env.example as container.envnpm run start-container to start the postgres and the spliit2 containersThis is a sample docker-compose.yml file that you can use to deploy this web app.
name: spliit
services:
app:
image: ghcr.io/spliit-app/spliit:latest
user: "1000:1000" # change to your user id or remove if you want root
ports:
- "8080:3000/tcp"
environment:
POSTGRES_PRISMA_URL: postgresql://spliit:spliit@database:5432/spliit
POSTGRES_URL_NON_POOLING: postgresql://spliit:spliit@database:5432/spliit
volumes:
- ./app/cache:/usr/app/.next/cache
depends_on:
- database
networks:
- spliit
database:
image: postgres:17.3
user: "1000:1000" # same as above
environment:
POSTGRES_USER: spliit
POSTGRES_PASSWORD: spliit
POSTGRES_DB: spliit
volumes:
- ./database/data:/var/lib/postgresql/data
networks:
- spliit
networks:
spliit:
The web app will then be available on your host at http://localhost:8080/.
You can use named volumes in place of bind mounts if you prefer not having data stored inside local directories.
The application has a health check endpoint that can be used to check if the application is running and if the database is accessible.
GET /api/health/readiness or GET /api/health - Check if the application is ready to serve requests, including database connectivity.GET /api/health/liveness - Check if the application is running, but not necessarily ready to serve requests.Every variable below is read at runtime. For a container deployment, set them in
container.env or pass them with docker run -e; no rebuild is required, which
means the published image can be configured by whoever runs it.
Set BASE_URL to the public URL your instance is reachable at. It is used for
metadata, the sitemap, robots.txt, and to accept server actions sent to that
host.
BASE_URL=https://spliit.example.com
Defaults to http://localhost:3000.
Set DEFAULT_CURRENCY_CODE to pre-select a currency on the new-group form.
DEFAULT_CURRENCY_CODE=EUR
Defaults to USD.
NEXT_PUBLIC_* variablesEarlier versions used NEXT_PUBLIC_-prefixed variables for the settings above
and for the opt-in feature flags below. Next.js inlines those into the app at
build time, so in a prebuilt image — like the published one — they are frozen
at whatever the release build used and setting them at runtime does nothing. The
runtime variables replace them:
| Old (build-time) | New (runtime) |
|---|---|
NEXT_PUBLIC_BASE_URL | BASE_URL |
NEXT_PUBLIC_DEFAULT_CURRENCY_CODE | DEFAULT_CURRENCY_CODE |
NEXT_PUBLIC_ENABLE_EXPENSE_DOCUMENTS | ENABLE_EXPENSE_DOCUMENTS |
NEXT_PUBLIC_ENABLE_RECEIPT_EXTRACT | ENABLE_RECEIPT_EXTRACT |
NEXT_PUBLIC_ENABLE_CATEGORY_EXTRACT | ENABLE_CATEGORY_EXTRACT |
The old variables still work — the runtime variant simply takes precedence
when both are set, so there is nothing you have to change immediately. They
remain the right choice if you build your own image and want a setting baked in.
To migrate, drop the NEXT_PUBLIC_ prefix and set the variable wherever your
container gets its environment.
Spliit offers users to upload images (to an AWS S3 bucket) and attach them to expenses. To enable this feature:
ENABLE_EXPENSE_DOCUMENTS=true
S3_UPLOAD_KEY=AAAAAAAAAAAAAAAAAAAA
S3_UPLOAD_SECRET=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
S3_UPLOAD_BUCKET=name-of-s3-bucket
S3_UPLOAD_REGION=us-east-1
You can also use other S3 providers by providing a custom endpoint:
S3_UPLOAD_ENDPOINT=http://localhost:9000
You can offer users to create expense by uploading a receipt. This feature relies on a vision-capable OpenAI model and a public S3 storage endpoint.
To enable the feature:
ENABLE_RECEIPT_EXTRACT=true
OPENAI_API_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXX
The model defaults to gpt-5-nano and can be changed with the optional OPENAI_MODEL_RECEIPT_EXTRACT variable — a larger model reads poor-quality photos more reliably, at a higher price per scan.
You can offer users to automatically deduce the expense category from the title. Since this feature relies on a OpenAI subscription, follow the signup instructions above and configure the following environment variables:
ENABLE_CATEGORY_EXTRACT=true
OPENAI_API_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXX
The model defaults to gpt-5-nano and can be changed with the optional OPENAI_MODEL_CATEGORY_EXTRACT variable.
Both AI features above talk to the official OpenAI API by default. Set the optional OPENAI_BASE_URL variable to point them at a self-hosted or alternative provider instead:
OPENAI_BASE_URL=http://localhost:11434/v1
OPENAI_MODEL_RECEIPT_EXTRACT=name-of-a-vision-model
OPENAI_MODEL_CATEGORY_EXTRACT=name-of-a-text-model
Whichever provider you choose has to support the json_schema response format (structured outputs), and the receipt feature additionally needs image input. If a response does not match the expected schema, the app reports that nothing could be extracted rather than filling the form with guesses.
If your environment file was created on Windows, make sure it uses LF line endings. A trailing carriage return makes OPENAI_API_KEY fail authentication and silently switches feature flags off.
Spliit can report anonymous usage events to an analytics service. It is disabled by default: nothing is loaded and nothing is sent unless you select a provider.
Select one with ANALYTICS_PROVIDER. The variables are read on the server, so a single Docker image can be configured when the container starts.
Several providers can be listed, comma-separated, and every event is reported to each of them. That is how to try a new service next to the one you already use, or to watch what is sent while keeping the real one:
ANALYTICS_PROVIDER=plausible,umami
console — see what would be reportedLogs every event to the browser console and sends nothing anywhere. Useful while developing, and the shortest example of what a provider looks like.
ANALYTICS_PROVIDER=console
plausibleReports to Plausible, a privacy-friendly, cookie-free analytics service. No extra dependency is installed: the provider is a script tag and a function call.
ANALYTICS_PROVIDER=plausible
PLAUSIBLE_DOMAIN=your-domain.com
For a self-hosted Plausible instance, point at it with PLAUSIBLE_HOST:
PLAUSIBLE_HOST=https://plausible.your-domain.com
Ad blockers drop requests to known analytics hosts. To avoid that, serve the script and the event endpoint from your own origin by adding rewrites in next.config.mjs and pointing the provider at them:
PLAUSIBLE_SCRIPT_URL=/js/script.manual.js
PLAUSIBLE_API_URL=/proxy/api/event
umamiReports to Umami, a privacy-friendly, cookie-free analytics service that is open source and runs on PostgreSQL if you self-host it. Like the Plausible provider, it is a script tag and a function call: no dependency is installed. Umami's tracker would also send the page title and same-origin referrers, which on group pages carry the group name and possibly a group ID; the provider drops both.
ANALYTICS_PROVIDER=umami
UMAMI_WEBSITE_ID=your-website-id
The script is loaded from Umami Cloud by default. For a self-hosted instance, point at its script:
UMAMI_SCRIPT_URL=https://umami.your-domain.com/script.js
To serve the script and the event endpoint from your own origin (see the Plausible section for why), add the rewrites and tell the tracker where events go — UMAMI_HOST_URL is the base the tracker appends /api/send to:
UMAMI_SCRIPT_URL=/js/umami.js
UMAMI_HOST_URL=/proxy/umami
// next.config.mjs — with your own instance in place of cloud.umami.is if you self-host
async rewrites() {
return [
{ source: '/js/umami.js', destination: 'https://cloud.umami.is/script.js' },
{ source: '/proxy/umami/api/send', destination: 'https://cloud.umami.is/api/send' },
]
},
Pageviews for a handful of pages, and one event per significant action: creating and updating a group, creating, updating and deleting an expense, attaching a document, scanning a receipt, and exporting expenses.
Group and expense IDs are never sent. They are the capability to read someone's group, so /groups/<id>/expenses is reported as /groups/[groupId]/expenses. Anonymization happens in one place, anonymizePath in src/lib/analytics/, between the call sites and every provider, and the event types forbid properties that are not explicitly declared — so leaking an ID is a compile error rather than a review question.
Pages are tracked explicitly, with <TrackPage path="…" />. A new route reports nothing until someone adds it, which keeps that a deliberate decision.
This is unrelated to the group activity log (the Activity tab), which is stored in your own database and is a product feature rather than analytics.
Providers live in src/lib/analytics/providers/. Copy console.tsx, then register the new one in three places: provider-ids.ts, registry.ts, and config.ts (to map its environment variables to options). The last two are type-checked against the first, so npm run check-types tells you exactly what is missing.
A provider supplies a transport — where events go — and optionally a Script component if it needs to load an SDK.
MIT, see LICENSE.
(top 24 of 43)
2,808 followers · starred Jun 2024
2,179 followers · starred May 2026
8,313 followers · starred Mar 2024
122 followers · starred Nov 2024
Free and Open Source Alternative to Splitwise. Share expenses with your friends and family.
See the codeSpliit is a free and open source alternative to Splitwise. You can either use the official instance at Spliit.app, or deploy your own instance:
The project is open to contributions. Feel free to open an issue or even a pull-request! Join the discussion in the Spliit Discord server.
Spliit is free, open source, and has no ads. Hosting, database and API costs are paid for by donations. If you want to help keep it that way, you can:
Contributions of any size are appreciated, and so is simply telling people about the project.
The project's translations are managed using our Weblate project. You can easily add missing translations to the project or even add a new language! Here is the current state of translation:
./scripts/start-local-db.sh if you don’t have a server already..env.example as .envnpm install to install dependencies. This will also apply database migrations and update Prisma Client.npm run dev to start the development serverThe Playwright suite in e2e/ drives a real browser against the app running in
Docker, so it exercises the same image users deploy. It needs Docker and a free
port 3000, and nothing else — the stack builds itself from your checkout and
throws its database away afterwards.
npm run e2e
That builds the image, starts app + PostgreSQL from compose.e2e.yaml, waits
for /api/health/readiness, runs the suite and tears everything down. It never
touches your development stack or ./postgres-data.
While writing tests it is quicker to keep the stack up:
npm run e2e:up # build and start, then leave it running
npm run e2e:test -- --ui # iterate (also --headed, --grep, --debug)
npm run e2e:report # open the HTML report of the last run
npm run e2e:down # stop and delete the test database
--ui opens Playwright's UI mode, where you can pick tests, watch them run and
step through a trace. It does not start the stack itself, so run npm run e2e:up
first.
If port 3000 is already taken — by npm run dev, for instance — set
E2E_HOST_PORT on every command of the session, including the test run:
E2E_HOST_PORT=3100 npm run e2e # one-shot
E2E_HOST_PORT=3100 npm run e2e:up # or, for the iteration loop
E2E_HOST_PORT=3100 npm run e2e:test -- --ui
E2E_HOST_PORT=3100 npm run e2e:down
The same suite runs in GitHub Actions from the E2E workflow, which can be triggered manually and runs automatically on release tags.
npm run build-image to build the docker image from the Dockerfilecontainer.env.example as container.envnpm run start-container to start the postgres and the spliit2 containersThis is a sample docker-compose.yml file that you can use to deploy this web app.
name: spliit
services:
app:
image: ghcr.io/spliit-app/spliit:latest
user: "1000:1000" # change to your user id or remove if you want root
ports:
- "8080:3000/tcp"
environment:
POSTGRES_PRISMA_URL: postgresql://spliit:spliit@database:5432/spliit
POSTGRES_URL_NON_POOLING: postgresql://spliit:spliit@database:5432/spliit
volumes:
- ./app/cache:/usr/app/.next/cache
depends_on:
- database
networks:
- spliit
database:
image: postgres:17.3
user: "1000:1000" # same as above
environment:
POSTGRES_USER: spliit
POSTGRES_PASSWORD: spliit
POSTGRES_DB: spliit
volumes:
- ./database/data:/var/lib/postgresql/data
networks:
- spliit
networks:
spliit:
The web app will then be available on your host at http://localhost:8080/.
You can use named volumes in place of bind mounts if you prefer not having data stored inside local directories.
The application has a health check endpoint that can be used to check if the application is running and if the database is accessible.
GET /api/health/readiness or GET /api/health - Check if the application is ready to serve requests, including database connectivity.GET /api/health/liveness - Check if the application is running, but not necessarily ready to serve requests.Every variable below is read at runtime. For a container deployment, set them in
container.env or pass them with docker run -e; no rebuild is required, which
means the published image can be configured by whoever runs it.
Set BASE_URL to the public URL your instance is reachable at. It is used for
metadata, the sitemap, robots.txt, and to accept server actions sent to that
host.
BASE_URL=https://spliit.example.com
Defaults to http://localhost:3000.
Set DEFAULT_CURRENCY_CODE to pre-select a currency on the new-group form.
DEFAULT_CURRENCY_CODE=EUR
Defaults to USD.
NEXT_PUBLIC_* variablesEarlier versions used NEXT_PUBLIC_-prefixed variables for the settings above
and for the opt-in feature flags below. Next.js inlines those into the app at
build time, so in a prebuilt image — like the published one — they are frozen
at whatever the release build used and setting them at runtime does nothing. The
runtime variables replace them:
| Old (build-time) | New (runtime) |
|---|---|
NEXT_PUBLIC_BASE_URL | BASE_URL |
NEXT_PUBLIC_DEFAULT_CURRENCY_CODE | DEFAULT_CURRENCY_CODE |
NEXT_PUBLIC_ENABLE_EXPENSE_DOCUMENTS | ENABLE_EXPENSE_DOCUMENTS |
NEXT_PUBLIC_ENABLE_RECEIPT_EXTRACT | ENABLE_RECEIPT_EXTRACT |
NEXT_PUBLIC_ENABLE_CATEGORY_EXTRACT | ENABLE_CATEGORY_EXTRACT |
The old variables still work — the runtime variant simply takes precedence
when both are set, so there is nothing you have to change immediately. They
remain the right choice if you build your own image and want a setting baked in.
To migrate, drop the NEXT_PUBLIC_ prefix and set the variable wherever your
container gets its environment.
Spliit offers users to upload images (to an AWS S3 bucket) and attach them to expenses. To enable this feature:
ENABLE_EXPENSE_DOCUMENTS=true
S3_UPLOAD_KEY=AAAAAAAAAAAAAAAAAAAA
S3_UPLOAD_SECRET=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
S3_UPLOAD_BUCKET=name-of-s3-bucket
S3_UPLOAD_REGION=us-east-1
You can also use other S3 providers by providing a custom endpoint:
S3_UPLOAD_ENDPOINT=http://localhost:9000
You can offer users to create expense by uploading a receipt. This feature relies on a vision-capable OpenAI model and a public S3 storage endpoint.
To enable the feature:
ENABLE_RECEIPT_EXTRACT=true
OPENAI_API_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXX
The model defaults to gpt-5-nano and can be changed with the optional OPENAI_MODEL_RECEIPT_EXTRACT variable — a larger model reads poor-quality photos more reliably, at a higher price per scan.
You can offer users to automatically deduce the expense category from the title. Since this feature relies on a OpenAI subscription, follow the signup instructions above and configure the following environment variables:
ENABLE_CATEGORY_EXTRACT=true
OPENAI_API_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXX
The model defaults to gpt-5-nano and can be changed with the optional OPENAI_MODEL_CATEGORY_EXTRACT variable.
Both AI features above talk to the official OpenAI API by default. Set the optional OPENAI_BASE_URL variable to point them at a self-hosted or alternative provider instead:
OPENAI_BASE_URL=http://localhost:11434/v1
OPENAI_MODEL_RECEIPT_EXTRACT=name-of-a-vision-model
OPENAI_MODEL_CATEGORY_EXTRACT=name-of-a-text-model
Whichever provider you choose has to support the json_schema response format (structured outputs), and the receipt feature additionally needs image input. If a response does not match the expected schema, the app reports that nothing could be extracted rather than filling the form with guesses.
If your environment file was created on Windows, make sure it uses LF line endings. A trailing carriage return makes OPENAI_API_KEY fail authentication and silently switches feature flags off.
Spliit can report anonymous usage events to an analytics service. It is disabled by default: nothing is loaded and nothing is sent unless you select a provider.
Select one with ANALYTICS_PROVIDER. The variables are read on the server, so a single Docker image can be configured when the container starts.
Several providers can be listed, comma-separated, and every event is reported to each of them. That is how to try a new service next to the one you already use, or to watch what is sent while keeping the real one:
ANALYTICS_PROVIDER=plausible,umami
console — see what would be reportedLogs every event to the browser console and sends nothing anywhere. Useful while developing, and the shortest example of what a provider looks like.
ANALYTICS_PROVIDER=console
plausibleReports to Plausible, a privacy-friendly, cookie-free analytics service. No extra dependency is installed: the provider is a script tag and a function call.
ANALYTICS_PROVIDER=plausible
PLAUSIBLE_DOMAIN=your-domain.com
For a self-hosted Plausible instance, point at it with PLAUSIBLE_HOST:
PLAUSIBLE_HOST=https://plausible.your-domain.com
Ad blockers drop requests to known analytics hosts. To avoid that, serve the script and the event endpoint from your own origin by adding rewrites in next.config.mjs and pointing the provider at them:
PLAUSIBLE_SCRIPT_URL=/js/script.manual.js
PLAUSIBLE_API_URL=/proxy/api/event
umamiReports to Umami, a privacy-friendly, cookie-free analytics service that is open source and runs on PostgreSQL if you self-host it. Like the Plausible provider, it is a script tag and a function call: no dependency is installed. Umami's tracker would also send the page title and same-origin referrers, which on group pages carry the group name and possibly a group ID; the provider drops both.
ANALYTICS_PROVIDER=umami
UMAMI_WEBSITE_ID=your-website-id
The script is loaded from Umami Cloud by default. For a self-hosted instance, point at its script:
UMAMI_SCRIPT_URL=https://umami.your-domain.com/script.js
To serve the script and the event endpoint from your own origin (see the Plausible section for why), add the rewrites and tell the tracker where events go — UMAMI_HOST_URL is the base the tracker appends /api/send to:
UMAMI_SCRIPT_URL=/js/umami.js
UMAMI_HOST_URL=/proxy/umami
// next.config.mjs — with your own instance in place of cloud.umami.is if you self-host
async rewrites() {
return [
{ source: '/js/umami.js', destination: 'https://cloud.umami.is/script.js' },
{ source: '/proxy/umami/api/send', destination: 'https://cloud.umami.is/api/send' },
]
},
Pageviews for a handful of pages, and one event per significant action: creating and updating a group, creating, updating and deleting an expense, attaching a document, scanning a receipt, and exporting expenses.
Group and expense IDs are never sent. They are the capability to read someone's group, so /groups/<id>/expenses is reported as /groups/[groupId]/expenses. Anonymization happens in one place, anonymizePath in src/lib/analytics/, between the call sites and every provider, and the event types forbid properties that are not explicitly declared — so leaking an ID is a compile error rather than a review question.
Pages are tracked explicitly, with <TrackPage path="…" />. A new route reports nothing until someone adds it, which keeps that a deliberate decision.
This is unrelated to the group activity log (the Activity tab), which is stored in your own database and is a product feature rather than analytics.
Providers live in src/lib/analytics/providers/. Copy console.tsx, then register the new one in three places: provider-ids.ts, registry.ts, and config.ts (to map its environment variables to options). The last two are type-checked against the first, so npm run check-types tells you exactly what is missing.
A provider supplies a transport — where events go — and optionally a Script component if it needs to load an SDK.
MIT, see LICENSE.
(top 24 of 43)
2,808 followers · starred Jun 2024
2,179 followers · starred May 2026
8,313 followers · starred Mar 2024
122 followers · starred Nov 2024