制作者的介绍仅有上面的一句话。 更多详情请阅读仓库 README。 仓库 README ↗
正在加载完整 README。
查看完整 README ↗A secure multi-user web application for tracking recurring expenses and income with complete data separation. Built with React + Mantine frontend and Flask + PostgreSQL backend.
Account Boundaries and Session Recovery — Consistent owner permissions, safe bill-group switching, and recovery from removed saved groups.
This project is licensed under the O'Saasy License - a modified MIT license that permits broad use while restricting SaaS commercialization by third parties.
Learn more at osaasy.dev
For local development in WSL or Linux, the repo now includes a small task runner:
make bootstrap
export BILLMANAGER_DEV_DB_PASSWORD="$(openssl rand -base64 32)"
make dev-up
make test
What these commands do:
make bootstrap creates .venv, installs backend Python dependencies, and runs npm ci in apps/web and apps/mobilemake dev-up builds and starts the local Docker stack from docker-compose.dev.yml. It keeps BILLMANAGER_DEV_DB_PASSWORD raw for PostgreSQL and derives the percent-encoded URL component without putting the password on a command line.make test runs backend, web, and mobile tests. Its disposable local PostgreSQL container uses a fresh generated password unless BACKEND_TEST_DB_PASSWORD is set; external test databases still require an explicitly allowlisted BACKEND_TEST_DB_URL.make verify runs the full test suite plus backend security checksMobile development uses an Expo development client rather than Expo Go. See the mobile build and release-readiness guide.
Continuous integration in build.yml is deliberately non-publishing: backend, web, mobile, and security checks run on pushes and pull requests to main, while Docker image validation is pull-request-only. It does not publish images or create a release.
Web and server containers are released only by release-web.yml. Create an exact v<apps/web package version> tag on a commit contained in main to release that version, or use its protected manual dispatch from main. Before publishing, the workflow requires that commit's backend, frontend, mobile, and secrets checks to have completed successfully. The web-production environment must be protected before either path is used. The workflow publishes the GHCR container outputs, including the ARM64 image; publishing an image neither deploys it to a running environment nor promotes it publicly.
Product event tracking is disabled by default. Release automation publishes two distinct artifacts from the same commit and Dockerfile:
ghcr.io/brdweb/billmanager:<version> (plus the documented public
<major>.<minor>, latest, and sha-* tags) is the public self-hosted image.
This build never receives the BillManager SaaS analytics build arguments and
is always tracker-free.ghcr.io/brdweb/billmanager-saas:<version> is the BillManager SaaS image. It
is published only when both protected PRODUCT_UMAMI_* values are configured
and contains the corresponding tracker configuration. If both values are
empty, release automation skips this artifact; if only one is set, it fails.Self-hosters should pull ghcr.io/brdweb/billmanager:latest or another public
tag, never the billmanager-saas repository. To opt in from source, build a
deployment-specific image with both
VITE_UMAMI_SCRIPT_URL (an absolute HTTPS script URL) and
VITE_UMAMI_WEBSITE_ID (your Umami website UUID). The source Compose file forwards
these values from your private .env to Docker build arguments. Rebuild to change
or remove tracking; container runtime variables alone cannot enable the tracker.
Never retag or publish a SaaS-configured image as the public self-hosted image.
For a direct web build, set both variables in apps/web/.env.local or the build
environment before npm run build. Set UMAMI_SCRIPT_URL to the same script URL
when starting Flask so production CSP permits that origin for scripts and event
requests. Docker sets this server default from the build argument automatically.
Only that HTTPS origin is added; the remaining CSP restrictions stay in place.
Missing one build value or an invalid value fails the build.
For BillManager SaaS, the intended destination is the separate app.billmanager.app
website in the existing Umami dashboard at https://analytics.billmanager.app,
using https://analytics.billmanager.app/script.js. The marketing website UUID
must not be reused. Enablement awaits the operator's website setup, review of
consent/opt-out policy, and deployment approval; this configuration does not
establish a legal basis for tracking. Self-hosted operators may use their own
Umami instance or leave both values empty. This is separate from server telemetry.
The tracker is deferred, requests Do Not Track support, and excludes URL query
strings and fragments (requires Umami 2.17+). Existing events include
checkout_started (tier, interval) and bill_created (expense/deposit type).
No new event properties are added. An unavailable or blocked tracker leaves these
optional event calls inactive. There is no in-app consent or opt-out control in
this change; policy and any required UI must be settled before SaaS enablement.
After an approved rollout, verify the production CSP allows the configured
origin, window.umami.track exists, and a permitted test-account checkout and
bill creation appear under the product website in Umami. Check actual event
properties and that no query/fragment values are sent. Use an approved test
account and payment test mode; do not run automated suites against production.
For rollback, clear both build values and rebuild/redeploy the previous
untracked configuration. No database migration or existing-row changes occur.
Add one complete <code>.json catalog to apps/web/src/i18n/locales, using a lowercase two- or three-letter language code. Copy en.json, translate every value, keep the same keys, and set a non-empty _meta.languageName for the language picker. The web app discovers the new file automatically. For the existing mobile sync and code-generation steps, see Add a shared locale.
Useful day-to-day commands:
make dev-ps
make dev-logs
make dev-down
make test-db-up
make test-db-down
Create a private .env file containing three independently generated
secrets (openssl rand -hex 32) and your public HTTPS URL, then create a
docker-compose.yml file with the following content:
FLASK_SECRET_KEY=<random value>
JWT_SECRET_KEY=<different random value>
POSTGRES_PASSWORD=<random database password>
APP_URL=https://bills.example.com
services:
bills-app:
image: ghcr.io/brdweb/billmanager:latest
container_name: billmanager
ports:
- "5000:5000"
restart: unless-stopped
environment:
- DATABASE_URL=postgresql://billsuser:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}@db:5432/billsdb
- FLASK_SECRET_KEY=${FLASK_SECRET_KEY:?set FLASK_SECRET_KEY}
- JWT_SECRET_KEY=${JWT_SECRET_KEY:?set JWT_SECRET_KEY}
- APP_URL=${APP_URL:?set APP_URL to the public HTTPS URL}
- DEPLOYMENT_MODE=self-hosted
depends_on:
- db
db:
image: postgres:17-alpine
container_name: bills-db
restart: unless-stopped
environment:
- POSTGRES_USER=billsuser
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
- POSTGRES_DB=billsdb
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
New installs use PostgreSQL 17 by default. If you already have data in a
PostgreSQL 16 Docker volume, do not point the existing volume at the 17 image
directly; take a backup and restore into a fresh PostgreSQL 17 volume or run a
proper pg_upgrade flow.
Run the application:
docker compose up -d
Open the HTTPS URL from APP_URL. Production/self-hosted mode enables
HTTPS redirects, secure cookies, HSTS, and browser security headers, so put
port 5000 behind a TLS-terminating reverse proxy.
If you already have a PostgreSQL server or prefer to use a managed database service (AWS RDS, DigitalOcean, Supabase, etc.), you can run just the application container:
Create your database on your PostgreSQL server:
CREATE DATABASE billsdb;
CREATE USER billsuser WITH ENCRYPTED PASSWORD 'your-secure-password';
GRANT ALL PRIVILEGES ON DATABASE billsdb TO billsuser;
Create a simplified docker-compose.yml:
services:
bills-app:
image: ghcr.io/brdweb/billmanager:latest
container_name: billmanager
ports:
- "5000:5000"
restart: unless-stopped
environment:
- DATABASE_URL=postgresql://billsuser:$POSTGRES_PASSWORD@your-db-host:5432/billsdb
- FLASK_SECRET_KEY=${FLASK_SECRET_KEY:?set FLASK_SECRET_KEY}
- JWT_SECRET_KEY=${JWT_SECRET_KEY:?set JWT_SECRET_KEY}
- APP_URL=${APP_URL:?set APP_URL to the public HTTPS URL}
- DEPLOYMENT_MODE=self-hosted
Or run with Docker directly:
docker run -d \
--name billmanager \
-p 5000:5000 \
-e DATABASE_URL=postgresql://billsuser:$POSTGRES_PASSWORD@your-db-host:5432/billsdb \
-e FLASK_SECRET_KEY="$FLASK_SECRET_KEY" \
-e JWT_SECRET_KEY="$JWT_SECRET_KEY" \
-e APP_URL="$APP_URL" \
-e DEPLOYMENT_MODE=self-hosted \
ghcr.io/brdweb/billmanager:latest
Database URL Format:
postgresql://USERNAME:$PASSWORD@HOST:PORT/DATABASE
| Component | Example | Description |
|---|---|---|
| USERNAME | billsuser | PostgreSQL username |
| PASSWORD | secretpass | PostgreSQL password (URL-encode special characters) |
| HOST | db.example.com | Database server hostname or IP |
| PORT | 5432 | PostgreSQL port (default: 5432) |
| DATABASE | billsdb | Database name |
Examples:
postgresql://billsuser:$POSTGRES_PASSWORD@localhost:5432/billsdbpostgresql://billsuser:[email protected]:5432/billsdbpostgresql://billsuser:[email protected]:5432/billsdbpostgresql://postgres:[email protected]:5432/postgres| Variable | Description | Default |
|---|---|---|
DATABASE_URL | PostgreSQL connection string | postgresql://billsuser:$POSTGRES_PASSWORD@db:5432/billsdb |
FLASK_SECRET_KEY | Backward-compatible fallback for JWT_SECRET_KEY | Required in production when JWT_SECRET_KEY is unset |
JWT_SECRET_KEY | Secret key for API access, refresh, and OAuth state tokens | Falls back to FLASK_SECRET_KEY |
EMAIL_PROVIDER | Outbound email provider: smtp, resend, or none | Auto-detects Resend/SMTP config |
FROM_EMAIL | Sender email address | None |
APP_URL | Application URL for email links | http://localhost:5000 |
SMTP_HOST | SMTP server hostname for self-hosted email | None |
SMTP_PORT | SMTP server port | 587 or 465 when SSL is enabled |
SMTP_USERNAME | SMTP username, optional for unauthenticated relays | None |
SMTP_PASSWORD | SMTP password, optional for unauthenticated relays | None |
SMTP_USE_TLS | Use STARTTLS for SMTP connections | true unless SSL is enabled |
SMTP_USE_SSL | Use implicit SSL/TLS for SMTP connections | false |
SMTP_TIMEOUT | SMTP connection timeout in seconds | 10 |
RESEND_API_KEY | Resend API key for hosted/existing installs | None |
ALLOWED_ORIGINS | Comma-separated list of allowed CORS origins | Uses APP_URL or localhost |
DEPLOYMENT_MODE | self-hosted or |
Each user selects their currency from Settings. The preference is stored on their account and follows them across devices. DEFAULT_LOCALE still controls regional formatting conventions for the deployment, while the interface language controls translated text.
The supported currency order follows the final 2025 BIS foreign-exchange turnover table: USD, EUR, JPY, GBP, CNY, CHF, AUD, CAD, HKD, SGD, INR, KRW, SEK, NZD, MXN. JPY and KRW use zero minor units, so amounts must be whole numbers. Every other supported currency uses two minor units.
Changing a user's currency changes validation and display but does not convert stored amounts. SaaS subscription prices remain in USD regardless of this preference. During upgrade, existing users inherit the former DEFAULT_CURRENCY value once; the environment variable is no longer used afterward.
Security Note: In production, JWT_SECRET_KEY or FLASK_SECRET_KEY must be explicitly set. The application will refuse to start without it. Generate secure keys with: openssl rand -hex 32
BillManager uses a three-tier priority system for CORS origins:
ALLOWED_ORIGINS - Explicit comma-separated list (e.g., https://app1.com,https://app2.com)APP_URL - Single origin for typical deploymentsThis ensures secure self-hosted deployments while remaining flexible for development.
ENABLE_2FA=true, ENABLE_PASSKEYS=true, WEBAUTHN_RP_ID, and WEBAUTHN_ORIGIN. Android development/release builds also require their exact Credential Manager origins in WEBAUTHN_ANDROID_ORIGINS and a matching /.well-known/assetlinks.json statement.OAUTH_APPLE_PRIVATE_KEY may be provided as a single line with \n escapes.https://<your-domain>/auth/callback.https://<your-domain>/auth/callbackOAUTH_OIDC_DISCOVERY_URL to your provider's .well-known/openid-configuration URLOAUTH_OIDC_TOKEN_AUTH_METHOD=client_secret_basic for OIDC clients configured to authenticate to the token endpoint with HTTP Basic auth, or none only for public clients that do not use a client secretOAUTH_OIDC_SKIP_EMAIL_VERIFICATION=true if they don't include email_verified in tokensOAUTH_OIDC_EMAIL_CLAIM, etc.) is available when your OIDC provider uses non-standard claim namesFor complete configuration options, see the Self-Hosted Installation Guide.
On first startup, BillManager creates a default admin account with a randomly generated secure password. This password is printed to the container logs:
docker-compose logs billmanager | grep -A 5 "INITIAL ADMIN CREDENTIALS"
You will see:
============================================================
INITIAL ADMIN CREDENTIALS (save these now!)
Username: admin
Password: xK9mP2vL7nQr3wYz
You will be required to change this password on first login.
============================================================
Save this password immediately! It is only shown once during initial startup.
For detailed setup and usage instructions, see the documentation.
BillManager allows you to share bills with other users and split costs in flexible ways:
Username-based shares (self-hosted):
Email-based shares (SaaS):
As the Bill Owner:
As a Share Recipient:
When a bill is shared:
For more details, see the Sharing Bills Guide in the documentation.
Complete documentation is available at docs.billmanager.app:
docker compose up -d
docker compose logs -f bills-app
docker compose down
docker compose pull
docker compose down
docker compose up -d
# Create backup
docker exec bills-db pg_dump -U billsuser billsdb > backup.sql
# Restore backup
docker exec -i bills-db psql -U billsuser billsdb < backup.sql
Docker Compose uses a named volume for PostgreSQL data:
postgres_data - All application data (users, databases, bills, payments)/api/v2/*/api/v2/docs/api/v2/*BillManager can send password reset, verification, invitation, shared bill, and email OTP messages through either SMTP or Resend. SMTP is the recommended self-hosted path because it works with common mail providers and existing homelab relays:
EMAIL_PROVIDER=smtp
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USERNAME=your-smtp-username
SMTP_PASSWORD=your-smtp-password
SMTP_USE_TLS=true
SMTP_USE_SSL=false
[email protected]
APP_URL=https://bills.yourdomain.com
Resend remains supported for hosted or existing installs:
EMAIL_PROVIDER=resend
RESEND_API_KEY=re_your_api_key
[email protected]
BillManager does not bundle a production SMTP server. Operating one safely requires DNS, SPF/DKIM/DMARC, abuse prevention, deliverability monitoring, and network port considerations that are separate from the application stack. Use a mail provider, transactional email service, or a relay you already operate.
Ready to organize your finances securely? Get started with BillManager!
Licensed under O'Saasy | Documentation | View on GitHub
saasself-hosted |
DEFAULT_LOCALE | BCP 47 locale used to format bill amounts | en-US |
ENABLE_2FA | Enable two-factor authentication flows | false |
ENABLE_PASSKEYS | Enable passkey (WebAuthn) support | false |
WEBAUTHN_RP_ID | WebAuthn relying party ID (domain only) | Derived from APP_URL |
WEBAUTHN_RP_NAME | WebAuthn relying party display name | BillManager |
WEBAUTHN_ORIGIN | WebAuthn origin (must match app origin) | APP_URL |
WEBAUTHN_ANDROID_ORIGINS | Comma-separated android:apk-key-hash: origins for trusted Android signing certificates | unset |
OAUTH_AUTO_REGISTER | Auto-create users during social sign-in | true in self-hosted, false in SaaS |
OAUTH_GOOGLE_ENABLED | Enable Google sign-in | false |
OAUTH_GOOGLE_CLIENT_ID | Google OAuth client ID | None |
OAUTH_GOOGLE_CLIENT_SECRET | Google OAuth client secret | None |
OAUTH_APPLE_ENABLED | Enable Apple sign-in | false |
OAUTH_APPLE_CLIENT_ID | Apple Services ID (client ID) | None |
OAUTH_APPLE_TEAM_ID | Apple Developer Team ID | None |
OAUTH_APPLE_KEY_ID | Apple Sign in with Apple key ID | None |
OAUTH_APPLE_PRIVATE_KEY | Apple private key (.p8, multiline or \n escaped) | None |
OAUTH_MICROSOFT_ENABLED | Enable Microsoft sign-in | false |
OAUTH_MICROSOFT_CLIENT_ID | Microsoft OAuth client ID (Azure AD app registration) | None |
OAUTH_MICROSOFT_CLIENT_SECRET | Microsoft OAuth client secret | None |
OAUTH_MICROSOFT_TENANT_ID | Azure AD tenant ID (common for multi-tenant, or specific tenant GUID) | common |
OAUTH_OIDC_ENABLED | Enable generic OIDC sign-in (for Authentik, Authelia, Keycloak, etc.) | false |
OAUTH_OIDC_CLIENT_ID | OIDC provider client ID | None |
OAUTH_OIDC_CLIENT_SECRET | OIDC provider client secret | None |
OAUTH_OIDC_DISCOVERY_URL | OIDC provider discovery URL (.well-known/openid-configuration) | None |
OAUTH_OIDC_DISPLAY_NAME | Display name shown on login button | SSO |
OAUTH_OIDC_ICON | Icon name for login button (Tabler icon without Icon prefix) | lock |
OAUTH_OIDC_SCOPES | OAuth scopes to request | openid email profile |
OAUTH_OIDC_TOKEN_AUTH_METHOD | Token endpoint client auth method: client_secret_post, client_secret_basic, none, or auto | client_secret_post |
OAUTH_OIDC_EMAIL_CLAIM | Claim name for user's email address | email |
OAUTH_OIDC_USERNAME_CLAIM | Claim name for username | preferred_username |
OAUTH_OIDC_NAME_CLAIM | Claim name for display name | name |
OAUTH_OIDC_SKIP_EMAIL_VERIFICATION | Skip email verification check (for providers that don't include email_verified) | false |
MAX_REQUEST_BYTES | Maximum HTTP request body size | 1048576 |
MAX_SYNC_ITEMS_PER_COLLECTION | Maximum items accepted in each sync collection | 500 |
MAX_PUSH_DEVICES_PER_USER | Maximum registered push devices per user | 10 |
INSTANCE_OPERATOR_USER_IDS | SaaS user IDs allowed to run instance-wide operations | unset |