## What A 3-way experiment on how the paid plan is offered, plus per-variant measurement. New signups (on/after `SUBSCRIPTION_EXPERIMENT_STARTED_AT`) are split evenly into: - **control** — current 15-day trial. - **reduced_trial** — shorter trial: 3 days monthly, 7 days yearly. - **pay_now** — charged immediately (no trial), with a self-service money-back guarantee for the first 3 days. Earlier users stay **legacy** and keep the 15-day trial. **While `started_at` is null the experiment is off and everyone behaves like control — inert until activated via env.** ## How it works - **Assignment** — `App\Features\SubscriptionExperiment` (Pennant), deterministic even split by a stable hash of the user id. QA can force a variant with `feature:enable`. - **Offer policy** — `ExperimentOffer` is the single source of truth for trial days per plan, the pay-now flag, the refund window and refund eligibility; shared by checkout, paywall and billing. - **Checkout** — trial length comes from the variant (`trialDays(0)` for pay_now → immediate charge). - **Onboarding clarity** — the paywall states the exact terms above the CTA: trial length for the selected plan, or "charged €X today + 3-day money-back guarantee" for pay_now. - **Self-service refund (pay_now)** — Settings → Billing, within the window: refunds the upfront charge, `cancelNow`, revokes bank connections keeping imported data. `refunded_at` records it and blocks a second refund. Crash-safe ordering: the refund is stamped before cancel/disconnect, which run best-effort in a try/catch. ## Measurement `stats:experiment-funnel` (weekly → Discord): per-variant funnel (assigned, subscribed, status breakdown, refunds) with a **net-active rate** gated by each variant's decision window (control 15d / reduced 7d / pay_now 3d) so cohorts are read at equal age. Attribution reads the variant Pennant actually served each user, so the report can't drift from what users experienced. It also reports **MRR** (monthly run-rate of mature net-active subs, yearly normalised ÷12) and **ARPU** (MRR ÷ assigned) per variant — ARPU is the revenue metric for the winner decision. Plus a winner can be pinned org-wide with `SUBSCRIPTION_EXPERIMENT_FORCE_VARIANT` (env, no deploy). ## Config (env) - `SUBSCRIPTION_EXPERIMENT_STARTED_AT` — activates the experiment (launch date). Null = off. - `SUBSCRIPTION_EXPERIMENT_REDUCED_TRIAL_MONTHLY` (3), `..._YEARLY` (7), `..._REFUND_WINDOW_DAYS` (3) ## Tests - **Feature/unit:** assignment, offer policy, checkout wiring, refund eligibility, the refund action incl. idempotency + crash-safe ordering (Stripe mocked), and the funnel collector/command. ES + FR translations. - **Browser** (`tests/Browser/SubscriptionRefundTest.php`): the self-service refund UX end to end — card visibility + deadline, two-step confirm, back-out, the refund control disappearing after confirming, and gating (window passed / non-pay_now hidden). The `RefundSelfServe` action is doubled so it never hits Stripe but applies the same DB effect. Screenshots: `refund-card-visible`, `refund-confirm-step`, `refund-completed`. - Full non-browser suite green (the one failing `DashboardTest` is pre-existing on `main` — Inertia 409 from the unbuilt local manifest). Pint + ESLint + tsc (changed files) clean. ## Two independent reviews — acted on **Fixed:** refund atomicity/idempotency (major) · funnel attribution now reads Pennant's served value instead of recomputing, killing report-vs-runtime drift (major) · pay_now copy shows the exact amount charged · throttle + block-demo on the refund route · `resolve(?User)` nullable · French translations. **Reviewer notes (deferred, low value):** - `refunded_at` is not cast to Carbon on Cashier's `Subscription` (safe today — only null-compared; would need a custom Cashier model). - `ExperimentFunnelCollector` walks users in PHP via `chunkById`; fine at current volume, can move to grouped SQL if it grows. ## Confidence: 85 / 100 The critical money path is now **verified live against the Stripe sandbox** (see below), which removes the earlier cap. All gates are green and the acceptance criteria are met. Held at 85 (not higher) because the browser UI test runs in CI rather than locally, and the pay_now *hosted-checkout + webhook* leg reuses the standard Cashier checkout already proven by the control flow (only `trialDays(0)` differs) but wasn't re-driven through the hosted page. Given it moves money + disconnects accounts, a human glance is still warranted before enabling. ## Sandbox verification (live Stripe test mode) `php artisan stripe:verify-refund` creates a real immediately-charged subscription with a test card, runs the actual `RefundSelfServe`, and checks the Stripe API. Result: ``` PASS subscription active after immediate charge (pay_now, no trial) PASS canSelfRefund is true before refund PASS latestPayment() resolves a payment intent PASS refunded_at is stamped PASS subscription is canceled PASS canSelfRefund is false after refund PASS Stripe charge shows a full refund (refunded=true) ``` The command is committed and guarded to Stripe test keys / non-production, so it can be re-run before each launch toggle. ## Launch checklist 1. Stripe-sandbox smoke: `php artisan stripe:verify-refund` (done — passing). Optionally also drive the hosted pay_now checkout once for monthly + yearly to confirm the webhook leg. 2. Set `SUBSCRIPTION_EXPERIMENT_STARTED_AT` to the launch date (set once; don't backdate). 3. Watch `stats:experiment-funnel`; a clean cohort baseline lands once each variant's window matures. |
||
|---|---|---|
| .agents/skills | ||
| .claude | ||
| .cursor | ||
| .github | ||
| .opencode/skills | ||
| .pi | ||
| app | ||
| bootstrap | ||
| config | ||
| database | ||
| docker | ||
| docs | ||
| experiments | ||
| lang | ||
| public | ||
| resources | ||
| routes | ||
| screenshots | ||
| scripts | ||
| src/lib/crypto | ||
| storage | ||
| templates/coolify | ||
| tests | ||
| .dockerignore | ||
| .editorconfig | ||
| .env.example | ||
| .env.production.example | ||
| .gitattributes | ||
| .gitignore | ||
| .mcp.json | ||
| .php-cs-fixer.dist.php | ||
| .php-version | ||
| .prettierignore | ||
| .prettierrc | ||
| .release-it.json | ||
| AGENTS.md | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| Dockerfile | ||
| Dockerfile.production | ||
| LICENSE.md | ||
| LOCALIZATION.md | ||
| ONBOARDING.md | ||
| README.md | ||
| artisan | ||
| autoresearch-dashboard.md | ||
| autoresearch.jsonl | ||
| autoresearch.md | ||
| autoresearch.sh | ||
| boost.json | ||
| bun.lock | ||
| components.json | ||
| compose.yaml | ||
| composer.json | ||
| composer.lock | ||
| docker-compose.production.yml | ||
| eslint.config.js | ||
| falcode.json | ||
| opencode.json | ||
| package-lock.json | ||
| package.json | ||
| phpstan-baseline.neon | ||
| phpstan.neon | ||
| phpunit.xml | ||
| tsconfig.json | ||
| vite.config.ts | ||
| vitest.config.ts | ||
| vitest.setup.ts | ||
| whispermoney | ||
| worktree.sh | ||
README.md
Deutsch | Español | français | 日本語 | 한국어 | Português | Русский | 中文
Whisper Money
The most secure way to understand your finances.
Whisper Money is a privacy-first personal finance application that helps you track, categorize, and understand your spending—all while keeping your financial data encrypted and secure.
🎮 Try the Demo: Experience Whisper Money with our demo account - no registration required!
💬 Join our Community: Whether you're a user looking for help or a developer wanting to contribute, we'd love to have you in our Discord server! Share feedback, ask questions, discuss new features, or just hang out with fellow privacy enthusiasts.
Features
- 🔐 Privacy-first — Your data is never shared with third parties. You own it
- 🏦 Bank account management — Track multiple accounts in one place
- 📊 Transaction categorization — Automatic and manual categorization
- 🤖 Automation rules — Set up rules to auto-categorize transactions
- 📈 Financial insights — Understand your spending patterns
Tech Stack
- Backend: Laravel 12, PHP 8.4
- Frontend: React 19, Inertia.js v2, TypeScript
- Styling: Tailwind CSS v4
- Database: MySQL
- Cache/Queue: Redis
- Testing: Pest v4
Running Locally
Quick Start (Recommended)
The easiest way to get started is using our automated setup script:
bash <(curl -fsSL https://whisper.money/setup.sh)
After installation, just visit https://whisper.money.localhost in your browser.
Manual Setup
If you prefer to set up manually:
- Clone the repository:
git clone https://github.com/whisper-money/whisper-money.git
cd whisper-money
- Run the setup script:
whispermoney install
Available Commands
Important: You must run
whispermoney installbefore using any other command. If you skip the install step, commands likestartwill not work.
Once installed, you can use the whispermoney command for common tasks:
# Start all services
whispermoney start
# Stop all services
whispermoney stop
# Upgrade to latest version
whispermoney upgrade
# Interactive menu
whispermoney
Development Server
For active development with hot reloading:
composer run dev
This will concurrently start:
- PHP development server (via Portless HTTPS proxy)
- Queue worker
- Log viewer (Pail)
- Vite dev server
The application will be available at https://dev.whisper.money.localhost. In git worktrees, the branch name is automatically prepended (e.g. https://fix-ui.dev.whisper.money.localhost).
Running with Docker (Production Image)
For testing the production Docker image locally:
- Copy the production environment file:
cp .env.production.example .env
- Start the services:
docker compose -f docker-compose.production.yml up -d
The application will be available at http://localhost:8080.
To use a different port, set APP_PORT:
APP_PORT=3000 docker compose -f docker-compose.production.yml up -d
Deploying to Coolify
Whisper Money can be easily deployed to Coolify using our Docker Compose template.
Quick Deploy
- In Coolify, create a new resource and select Docker Compose
- Choose Empty Compose File as the source
- Paste the contents from our template: 👉 whisper-money.yaml
- Deploy!
The template includes:
- Whisper Money application container
- MySQL 8.0 database with health checks
- Persistent volumes for data and storage
- Auto-generated database credentials
Required Environment Variables
| Variable | Description |
|---|---|
RESEND_API_KEY |
Email service API key (for password resets, notifications) |
Note:
APP_KEYandAPP_URLare auto-configured. The container generates anAPP_KEYon first startup if not provided.
Optional Environment Variables
| Variable | Default | Description |
|---|---|---|
DRIP_EMAILS_ENABLED |
true |
Enable drip emails (welcome, onboarding, feedback) |
HIDE_AUTH_BUTTONS |
false |
Hide login/register buttons on landing page |
SUBSCRIPTIONS_ENABLED |
false |
Enable Stripe subscriptions |
STRIPE_KEY |
- | Stripe publishable key |
STRIPE_SECRET |
- | Stripe secret key |
STRIPE_WEBHOOK_SECRET |
- | Stripe webhook signing secret |
Star History
License
This work is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.