A self-hosted, open-source alternative to Splitwise with AI-powered receipt scanning.
Quick Start • Features • Screenshots • Configuration • Development
ShareTab is a free, self-hosted alternative to Splitwise for tracking shared expenses with roommates, friends, and travel groups. Snap a photo of a receipt, let AI extract the line items, and assign them to group members. Taxes and tips split proportionally. Deploy on your own server with Docker Compose.
- Group expense tracking with multiple split modes (equal, percentage, shares, exact, item-level)
- AI receipt scanning -- photograph a receipt, AI extracts line items, assign items to group members with proportional tax/tip; zoomable/pannable receipt viewer; rescan with correction prompts
- Guest bill splitting -- no account needed, shareable summary links
- Pluggable AI providers -- OpenAI (GPT-4o), OpenAI-Codex (ChatGPT OAuth), Claude (API key), Meridian (Claude Max subscription), local Ollama
- Group archiving -- archive inactive groups to declutter your dashboard; toggle archived view on groups page
- Friends -- track IOUs with people outside any group: add by email or as a placeholder, log non-group expenses, see one balance per person rolled up across every shared group, and settle up outside a group
- Cross-group dashboard -- see all your balances at a glance, with per-person debt breakdown converted to your own currency
- Debt simplification -- minimize the number of payments needed
- Settle up -- record payments between any two group members with explicit From/To fields
- Placeholder members -- add people without accounts; rename or remove them from group settings
- Dark mode -- system-aware with manual toggle
- Invite links -- share a link to add friends to your groups
- Magic link auth -- passwordless email sign-in
- OIDC / SSO -- sign in through any OIDC-compliant identity provider (Pocket ID, Authentik, Keycloak, Authelia, Zitadel, ...), with optional OIDC-only mode, auto-redirect, and RP-initiated logout
- PWA -- installable on mobile with app-like experience
- Admin dashboard -- user management, group overview, storage stats, AI usage, audit log, registration control, announcements, server logs, user impersonation, data export, expired guest split cleanup
- Self-hosted -- Docker Compose deployment, designed for Unraid
ShareTab ships as an all-in-one Docker container with PostgreSQL bundled inside. No external database needed.
cd docker
cp ../.env.example .envEdit .env with your settings -- at minimum, generate real values for NEXTAUTH_SECRET and AUTH_SECRET:
# Generate a secret
openssl rand -base64 32Then start the container:
docker compose up -dThe app will be available at http://localhost:3000.
If you want to run ShareTab on Unraid, this repo includes a ready-made template at unraid/sharetab.xml.
To use it:
# On your Unraid server
mkdir -p /boot/config/plugins/dockerMan/templates-user
cp /path/to/sharetab/unraid/sharetab.xml /boot/config/plugins/dockerMan/templates-user/sharetab.xmlThen in the Unraid web UI:
- Open
Docker. - Click
Add Container. - Select the
ShareTabtemplate from the template dropdown. - Fill in the required variables like
AUTH_SECRET,NEXTAUTH_SECRET, and any optional AI settings. - Click
Applyto create and start the container.
You can also skip the manual copy and paste the raw template URL into Unraid's template install flow:
https://raw.githubusercontent.com/sw-carlos-cristobal/sharetab/main/unraid/sharetab.xml
Backups:
docker compose exec sharetab su-exec postgres pg_dump -U sharetab sharetab > backup.sqlWhen upgrading to a new ShareTab version, pull the latest image and recreate the container:
cd docker
docker compose pull
docker compose up -dThe entrypoint automatically runs any SQL migration files in prisma/migrations/ before applying the Prisma schema. Most upgrades are fully automatic.
Version 0.8.0 added an updatedAt column and converted the status column from text to an enum on the GuestSplit table. This migration now runs automatically on container startup. If you need to run it manually:
docker compose exec sharetab su-exec postgres psql -U sharetab -d sharetab \
-f /app/prisma/migrations/guest_split_status_enum.sqlThis is idempotent — safe to run more than once.
All configuration is done through environment variables. Copy .env.example to .env and adjust as needed.
| Variable | Description |
|---|---|
NEXTAUTH_SECRET |
Session encryption key. Generate with openssl rand -base64 32. |
AUTH_SECRET |
Auth.js secret. Generate the same way. |
| Variable | Description |
|---|---|
AI_PROVIDER_PRIORITY |
Comma-separated provider priority list (for example openai-codex,meridian,openai). ShareTab checks providers in order, uses the first available one, and falls through to the next provider if extraction fails. |
OPENAI_API_KEY |
Required when openai is included in AI_PROVIDER_PRIORITY. |
OPENAI_MODEL |
OpenAI model for receipt scanning. Defaults to gpt-4o. |
OPENAI_CODEX_MODEL |
Model for ChatGPT OAuth / Codex backend receipt scanning. Defaults to gpt-5.4. |
ANTHROPIC_API_KEY |
Required when claude is included in AI_PROVIDER_PRIORITY. |
ANTHROPIC_MODEL |
Claude model for receipt scanning. Defaults to claude-sonnet-4-6 (claude provider) or claude-opus-4-6 (meridian provider). |
ANTHROPIC_HEALTH_MODEL |
Model for health-check probes (auth verification). Defaults to claude-haiku-4-5-20251001. |
MERIDIAN_PORT |
Port for the embedded Meridian proxy. Defaults to 3457. |
OLLAMA_BASE_URL |
Ollama server URL. Defaults to http://localhost:11434. |
OLLAMA_MODEL |
Ollama model name. Defaults to llava. |
The openai-codex provider uses ChatGPT OAuth via the Codex backend instead of an API key. Auth data lives in /app/chatgpt, so if that path is on a persistent volume the login survives restarts and image updates.
After the container is running, open the ShareTab admin dashboard and complete the ChatGPT OAuth flow there:
- Sign in as the admin user and open
/admin. - In the ChatGPT OAuth section, start the login flow.
- Authorize with ChatGPT in your browser.
- When the flow redirects to
http://localhost:1455/auth/callback, copy the full URL from the browser address bar and paste it back into ShareTab.
If you use your own Docker or Unraid template, mount a persistent path to /app/chatgpt when openai-codex is in AI_PROVIDER_PRIORITY.
The meridian provider uses a Claude Max/Pro subscription via an embedded proxy -- no API key needed. Claude login data lives in /app/claude, so if that path is on a persistent volume the login survives restarts and image updates.
After the container is running, open the ShareTab admin dashboard and complete the Meridian login flow there:
- Sign in as the admin user and open
/admin. - In the Meridian auth section, start the login flow.
- Authorize with Claude in your browser.
- Copy the full callback URL from the browser address bar and paste it back into ShareTab.
The bundled Docker Compose setup persists /app/claude automatically. If you use your own Docker or Unraid template, mount a persistent path to /app/claude.
ocr provider (Tesseract.js) was originally included as a free fallback for users without AI API access, but after extensive testing across hundreds of real-world receipts, the accuracy was too unreliable for production use. Common failures included extracting modifiers as line items, failing to exclude delivery fees, and poor handling of non-standard receipt layouts. The OCR provider has been removed from the codebase. Existing configs that include ocr in AI_PROVIDER_PRIORITY will silently ignore it. If you need reliable receipt scanning, configure one of the AI providers above (openai-codex or meridian are recommended). Community contributions to reintroduce OCR with improved accuracy are welcome.
Benchmarked on a set of receipt photos (grocery, coffee shop, restaurant). Results represent typical single-receipt extraction.
| Provider | Speed | Item Accuracy | Cost | Notes |
|---|---|---|---|---|
| OpenAI Codex (ChatGPT OAuth) | ~6 s | 5/5 items | Free (uses ChatGPT subscription) | Recommended. Best balance of speed and accuracy. |
| Meridian (Claude OAuth) | ~16 s | 5/5 items | Free (uses Claude Max subscription) | Same accuracy, but 2–3x slower. |
| OpenAI (API key) | ~4 s | 5/5 items | Pay-per-token | Fastest, but requires an API key and costs money. |
| Ollama (local LLM) | Varies | Varies | Free, fully local | Depends on model and hardware. Requires a running Ollama server. |
Recommendation: Use openai-codex as your primary provider. It delivers the same accuracy as API-key providers at no additional cost (it piggybacks on your existing ChatGPT Plus/Pro subscription). Set your priority to:
AI_PROVIDER_PRIORITY="openai-codex"
If you also have a Claude Max subscription, you can add meridian as a fallback:
AI_PROVIDER_PRIORITY="openai-codex,meridian"
| Variable | Description |
|---|---|
GOOGLE_CLIENT_ID |
Google OAuth client ID for "Sign in with Google". |
GOOGLE_CLIENT_SECRET |
Corresponding client secret. |
One generic OIDC provider, configured via issuer discovery rather than a per-vendor preset. Works with any OIDC-compliant identity provider -- Pocket ID, Authentik, Keycloak, Authelia, Zitadel, and others. Setting OIDC_ISSUER, OIDC_CLIENT_ID, and OIDC_CLIENT_SECRET enables a "Sign in with SSO" button on /login and /register; the rest are optional tuning knobs.
| Variable | Default | Description |
|---|---|---|
OIDC_ISSUER |
Issuer URL, e.g. https://auth.example.com. ShareTab reads ${OIDC_ISSUER}/.well-known/openid-configuration for the rest of the endpoints. |
|
OIDC_CLIENT_ID |
Client ID registered with the IdP. | |
OIDC_CLIENT_SECRET |
Client secret. Leave the client's "public"/PKCE-only toggle off -- ShareTab is a confidential (server-side) client and needs a real secret. | |
OIDC_NAME |
Display name for the sign-in button, e.g. Pocket ID. Unset renders a generic "Sign in with SSO" label. |
|
OIDC_ALLOW_LINKING |
true |
Link an OIDC sign-in onto an existing password/Google account with the same email, but only when the IdP marks that email email_verified: true. |
OIDC_TRUST_EMAIL |
false |
Treat a missing email_verified claim as verified, for IdPs that omit the claim rather than sending false. Governs both linking and provisioning -- an unverified address is never turned into a new account either. Leave off unless you trust your IdP. |
OIDC_AUTO_PROVISION |
false |
Provision new users on first OIDC sign-in even when registration is invite-only or closed, bypassing the normal registration mode. It does not bypass the email_verified requirement. |
OIDC_ONLY |
false |
Hide the password/magic-link form on /login and /register. /login?password=1 is a permanent break-glass back to the password form. |
OIDC_AUTO_REDIRECT |
false |
Skip /login entirely and redirect straight to the IdP. Also changes the post-logout redirect URI to ${NEXTAUTH_URL}/login?password=1, which has to be registered too. Only enable once OIDC sign-in is confirmed working for every account on the instance. |
OIDC_RP_LOGOUT |
true |
Also end the IdP's own session on sign-out (RP-initiated logout) instead of just ShareTab's local session. Requires the IdP to advertise end_session_endpoint and to have the post-logout redirect URI below registered. |
Two URLs need registering with the IdP, not one:
| Register as | Value |
|---|---|
| Callback / redirect URI | ${NEXTAUTH_URL}/api/auth/callback/oidc |
| Post-logout redirect URI | ${NEXTAUTH_URL}/login |
For https://sharetab.example.com those are https://sharetab.example.com/api/auth/callback/oidc and https://sharetab.example.com/login.
The post-logout URI is only used when OIDC_RP_LOGOUT is on, which it is by default. Skip registering it and the first sign-out lands on the IdP’s "invalid post_logout_redirect_uri" error page -- after ShareTab’s own session has already been cleared. If you also enable OIDC_AUTO_REDIRECT, register ${NEXTAUTH_URL}/login?password=1 instead: auto-redirect changes the post-logout destination so signing out cannot bounce straight back into a still-open IdP session. Setting OIDC_RP_LOGOUT=false drops the requirement entirely, at the cost of leaving the IdP session open on sign-out.
Some IdPs (Pocket ID among them) match both URLs as a literal path rather than a wildcard, so register the exact paths.
Roll out in two steps: ship OIDC_ISSUER/OIDC_CLIENT_ID/OIDC_CLIENT_SECRET first, alongside the existing password form, and confirm SSO sign-in works for every account. Only then set OIDC_ONLY=true and OIDC_AUTO_REDIRECT=true -- /login?password=1 stays available as a fallback either way.
| Variable | Description |
|---|---|
EMAIL_SERVER_HOST |
SMTP host (e.g. smtp.gmail.com). Used for magic link sign-in and OAuth auth expiry alerts (Meridian / ChatGPT OAuth). |
EMAIL_SERVER_PORT |
SMTP port. Use 465 for implicit TLS, 587 for STARTTLS. |
EMAIL_SERVER_USER |
SMTP username / email address. |
EMAIL_SERVER_PASSWORD |
SMTP password or app password. |
EMAIL_FROM |
From address for sent emails. |
| Variable | Description |
|---|---|
ADMIN_EMAIL |
Email of the admin user. Grants access to /admin dashboard for managing users, groups, storage, and system settings, and receives OAuth auth expiry alerts when email is configured. |
| Variable | Default | Description |
|---|---|---|
NEXTAUTH_URL |
http://localhost:3000 |
Public URL of your instance. |
AUTH_TRUST_HOST |
false |
Set to true when running on a local network or behind a reverse proxy. |
DB_USER |
sharetab |
PostgreSQL username (Docker bundled DB). |
DB_PASSWORD |
sharetab |
PostgreSQL password (Docker bundled DB). |
DB_NAME |
sharetab |
PostgreSQL database name (Docker bundled DB). |
UPLOAD_DIR |
./uploads |
Directory for receipt image uploads. |
MAX_UPLOAD_SIZE_MB |
10 |
Maximum upload file size. |
AUTH_RATE_LIMIT_MAX |
5 |
Max login attempts per IP per hour. |
REGISTER_RATE_LIMIT_MAX |
10 |
Max registration attempts per IP per hour. |
GUEST_RATE_LIMIT_MAX |
10 |
Max guest split creations per IP per hour. |
FRIEND_INVITE_RATE_LIMIT_MAX |
20 |
Max friend invites sent per user per hour. |
LOG_LEVEL |
info |
Logging verbosity: debug, info, warn, or error. |
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router) + TypeScript |
| API | tRPC v11 (end-to-end type-safe) |
| Database | Prisma 7 + PostgreSQL 16 |
| Auth | NextAuth v5 (credentials + OAuth + magic link) |
| UI | TailwindCSS 4 + shadcn/ui + next-themes |
| AI | Pluggable providers: OpenAI, OpenAI-Codex, Claude, Meridian, Ollama |
| Testing | Vitest (unit) + Playwright (e2e) |
These commands are intended to be explicit enough for a human or an LLM to use without inferring repo-specific workflow details.
# Bump version files on main and update CHANGELOG.md
npm run version:bump -- patch
# Create a release branch + PR from main
npm run release:create -- patch
# Create a PR from the current branch
npm run pr:create -- --title "feat: example change"
# Push the current HEAD to origin/main
npm run push:main
# Publish a merged release by pushing the version tag
npm run release:publish -- v1.2.3Intent mapping:
- "bump the version" ->
npm run version:bump -- <patch|minor|major> - "create a release" ->
npm run release:create -- <patch|minor|major> - "create a PR" ->
npm run pr:create -- [--base main] [--title \"...\"] - "push to main" ->
npm run push:main - "publish the release" ->
npm run release:publish -- [vX.Y.Z]
Release flow:
- Run
npm run release:create -- patchfrommain. - Merge the generated
release/vX.Y.ZPR. - Run
npm run release:publish -- vX.Y.Zfrommain.
release:publish only creates and pushes the git tag. The actual GitHub release page and semver Docker images are still published by publish-release.yml.
# Install dependencies
npm install
# Generate Prisma client
npx prisma generate
# Copy and configure environment
cp .env.example .env # Then edit .env as needed
# Option A: All-in-one (embedded PostgreSQL + schema push + seed + dev server)
npm run dev:full
# Option B: Manual setup (bring your own PostgreSQL)
# Set DATABASE_URL in .env pointing to your PostgreSQL instance
npx prisma db push
npm run db:seed # optional -- creates demo data
npm run devDemo accounts after seeding: alice@example.com, bob@example.com, charlie@example.com (password: password123).
A flake.nix provides a pinned toolchain (Node 22, PostgreSQL 16, Prisma schema engine, Playwright browsers) matching what CI uses. Nix is entirely optional -- it changes nothing for contributors who do not use it.
# Enter the dev shell
nix develop
# Install dependencies (the flake pins the toolchain, not node_modules)
npm install
# Copy and configure environment
cp .env.example .env # Then edit .env as needed
# All-in-one: PostgreSQL + schema push + seed + dev server
nix run .#dev
# Or, from inside `nix develop`, the same thing:
sharetab-devOther outputs:
nix run .#build # npm run build
nix run .#test # npm test (unit tests)
nix fmt # format .nix files (nixpkgs-fmt)
nix flake check # evaluate all outputsFor e2e, wire up the browsers once, then run the suite against a server started by
nix run .#dev in another terminal. Quote the whole command for bash -c -- passing
Playwright's flags directly to nix develop -c lets nix consume --project and
--grep before Playwright sees them:
nix develop -c link-playwright-browsers
nix develop -c bash -c 'BASE_URL=http://localhost:3000 npx playwright test'With direnv, direnv allow activates the shell automatically on cd.
npm run dev:fulldoes not work on NixOS. It drives theembedded-postgresnpm package, whose prebuilt PostgreSQL binaries are dynamically linked against an FHS layout and will not execute. Usenix run .#devinstead -- it startspostgresql_16from nixpkgs against the same./test-pg-datadirectory, on the same port (51214), soDATABASE_URLfrom.env.examplekeeps working.npx prisma db pushneeds the flake's schema engine. The@prisma/enginesnpm package downloads a prebuilt binary that cannot run on NixOS -- without an override Prisma tries to fetch alinux-nixosengine and fails with a 404. Every flake output therefore exportsPRISMA_SCHEMA_ENGINE_BINARYfrom nixpkgs'prisma-engines_7. nixpkgs andpackage.jsonversion this independently (7.9.1 vs 7.6.0 at time of writing); that combination is tested and works, but a wider gap could eventually be rejected. The app runtime is unaffected either way:prisma/schema.prismauses the Rust-freeprisma-clientgenerator with@prisma/adapter-pg, so no query engine binary is involved.- Playwright browsers need one extra step. nixpkgs and npm roll Playwright on different schedules, so the browser revisions rarely match. Run
link-playwright-browsersonce afternpm install-- it builds a symlink farm in.playwright-browsers/mapping the revisions this checkout expects onto the ones nixpkgs built. If a future version gap is too wide to alias, fall back toprograms.nix-ldplusnpx playwright install. - Prisma may warn about OpenSSL.
prisma:warn Prisma failed to detect the libssl/openssl versionis cosmetic -- it comes from Prisma probing for engine binaries this setup does not use.
# Unit tests (Vitest)
npm test
# E2E tests (requires dev server running)
BASE_URL=http://localhost:3000 npx playwright test
# E2E with visible browser
BASE_URL=http://localhost:3000 npx playwright test --headed
# Include AI-dependent tests (requires configured AI provider)
BASE_URL=http://localhost:3000 RUN_AI_TESTS=1 npx playwright testSet AUTH_RATE_LIMIT_MAX=9999 and GUEST_RATE_LIMIT_MAX=9999 in .env to avoid rate limiting during repeated test runs.
Contributions are welcome! See CONTRIBUTING.md for development setup, PR guidelines, and code style.
If you find a bug or have a feature request, please open an issue.
To report a vulnerability, see SECURITY.md.
MIT













