Monetization groundwork: entitlements, Pro gates, rate limiting, Polar billing (dark-launched) - #34
Open
azs06 wants to merge 14 commits into
Open
Monetization groundwork: entitlements, Pro gates, rate limiting, Polar billing (dark-launched)#34azs06 wants to merge 14 commits into
azs06 wants to merge 14 commits into
Conversation
…olar billing (dark-launched)
Everything is inert behind MONETIZATION_ENABLED="false" in wrangler.toml;
flipping the var turns on Pro gating. No user-facing behavior changes until then.
- Entitlements: subscriptions table (migration 0002), locals.tier resolved in
hooks, requirePro() + FREE_LIMITS in src/lib/server/entitlements.ts
- Gates (free tier, all grandfathered): premium templates blocked only when
newly adopted (POST + PUT save paths share enforceInvoiceSaveGates), 10
cloud invoices on creates only, 3 active share links, server-side PDF
Pro-only, shared-page attribution badge hidden for Pro owners
- Rate limiting (always on, cost protection): D1 fixed-window counters
(migration 0001) on PDF generation, share-link creation, invoice saves;
fails open if the table/binding is missing
- Polar billing: @polar-sh/sdk, /api/billing/{checkout,portal,webhook},
upsertSubscription with lifetime-downgrade guard; inert until POLAR_*
secrets are set (see docs/POLAR_SETUP.md)
- UI: pricing page, upgrade modal, template lock, billing section in
settings, PaymentDetails on the editor (pay-this-invoice v1 groundwork)
- Also sweeps in minor CSS vendor-prefix/a11y fixes (admin pages, header,
page-settings selector)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…resh - docs/MONETIZATION.md: strategy, pricing, free/pro split, roadmap - docs/POLAR_SETUP.md: operational launch checklist (products, secrets, webhook, sandbox test, flag flip, deployment commands) - PLAN.md trimmed to a pointer; full historical plan archived at docs/archive/PLAN.md (superseded) - CLAUDE.md/AGENTS.md updated to describe the real architecture Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
invoice-generator | 5c690da | Commit Preview URL | Jul 15 2026, 12:01 PM |
- Real invoice email via Cloudflare Email Sending (EMAIL binding), POST /api/invoices/[id]/email — Pro-gated, rate-limited 20/day, attaches R2 PDF when present - Recurring invoices: recurring_schedules table (migration 0003), hourly Workers Cron via wrapper worker (worker.js) re-exporting the adapter output built with wrangler.build.toml; /api/recurring CRUD, RecurringModal + History action + Settings management list - Pay-this-invoice v1: paymentDetails on the invoice, rendered on /shared/[token]; save-path gate blocks newly enabling it for free users with existing data grandfathered - All gates remain dark behind MONETIZATION_ENABLED="false" Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- clients table (migration 0004) + Drizzle schema and db helpers - /api/clients (GET list / Pro-gated POST), /api/clients/[id] (Pro-gated PUT / DELETE), ownership-checked - Editor: saved-clients picker + "save as client" under bill-to (ClientBookComponent, ClientFormModal); upgrade prompt as UX hint, server 402 authoritative - Settings: Clients management section (list/edit/delete) - i18n clients.* namespace (en + bn); docs updated Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- reminder_settings table (migration 0005), one config per invoice (unique userId+invoiceId), remindAfterDays cadence: first send at dueDate + N days, then at most every N days while overdue/unpaid; lastSentAt written only on successful send so failures retry - Cron: runDueReminders (src/lib/server/reminders.ts) wired into worker.js scheduled handler alongside recurring; links to an active share link when one exists, plain reminder otherwise; orphaned configs deactivated; no Browser Rendering from cron - /api/reminders (GET / Pro-gated POST), /api/reminders/[id] (PUT/DELETE), ownership-checked - History "Set overdue reminder" action + ReminderModal; Settings management list; sendReminderEmail helper; en+bn i18n; docs updated Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
- /dashboard/reports: monthly revenue (last 12 months, paid invoices by issue date), outstanding split into overdue vs not-yet-due, top clients by paid revenue with outstanding column - All aggregation server-side in the load; only aggregates returned; amounts bucketed per-currency (user preferredCurrency fallback), never summed across currencies - Pure CSS/Tailwind bar chart, dashboard nav link, empty states, en+bn i18n; requirePro on the load (dormant while flag is false) - Defensive JSON parsing: drafts/archived excluded, malformed rows skipped; no new deps, no schema changes Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- POST /api/ai/invoice-from-text: auth -> requirePro -> 30/day rate limit -> length validation -> env.AI.run with @cf/google/gemma-4-26b-a4b-it ($0.10/M in, $0.30/M out — cheapest text model in the Workers AI catalog); 503 without the AI binding - Gemma has no native json_schema mode, so the endpoint prompts for JSON and parses defensively (fence-strip, balanced-brace extract), then sanitizes server-side (number coercion, 20-item cap, ISO date validation); raw model output is never echoed to the client - AiFillModal: textarea -> preview -> apply; merge fills only fields the AI found (bill-to, items, dueDate, notes) and never touches sender/template/tax; 402 routes to UpgradePromptModal - [ai] binding in wrangler.toml, Env typing, en+bn i18n, docs Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- METRICS binding -> invoice_events dataset; trackEvent helper (src/lib/server/analytics.ts) fails open, no-ops without binding, synchronous writeDataPoint so requests are never delayed; no PII (event name + plan/source/gate dimensions only, no user ids) - Server events at the endpoints: signup (Better Auth user.create hook), invoice/share/recurring/reminder/client created, PDF generated/downloaded, email_sent (user vs cron), reminder_sent, ai_fill_used, checkout_started, and gate_blocked at every 402 in entitlements.ts + share limit (highest-signal conversion event) - Guest events via POST /api/track (4-name allowlist, per-isolate IP throttle, always 204) + sendBeacon client helper with trackOnce dedupe: guest_invoice_created, guest_pdf_downloaded, signup_prompt_shown, upgrade_prompt_shown - Relative imports keep the esbuild cron bundle clean; querying via the Analytics Engine SQL HTTP API (SUM(_sample_interval)) — documented with taxonomy table in CLAUDE.md; docs Phase 0 ticked Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 0/1/2 checkboxes were stale — the work landed in 8ece0b9 and this branch but the boxes were never ticked. Only truly outstanding items remain unchecked: Polar products/secrets and the launch itself. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…o flag flip Six stages: local smoke test + gate dry-run (402 paths have never executed), one-time onboarding (email DNS + remote migrations before merge, since merge auto-deploys the sending cron), merge/deploy dark, Polar activation, flag flip, post-launch guardrails. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Verified live Cloudflare pricing; four personas modeled. Typical Pro user costs <$0.05/mo vs $5.36/mo net after Polar fees (~99% margin). Sole real exposure is Browser Rendering: the 20/hr PDF cap allows ~$3-6/mo worst case (exceeds Pro net at 15s renders) -> recommends a ~200-300/mo PDF quota alongside the hourly cap. Email + Analytics Engine currently unbilled; re-verify at flag flip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 20/hr cap alone allows 14,400 renders/mo — at a 15s render that Browser Rendering cost exceeds Pro Monthly's $5.36 net revenue (docs/COST_ANALYSIS.md). A 30-day fixed window in the existing D1 counter caps worst-case exposure at ~$0.30-0.40/mo while remaining ~10x real freelancer usage. Cost analysis doc marked applied. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pricing page Pro feature line now reads "(250/mo fair use)", the 429 response states the limit and reset window, and the plan doc's feature table records the disclosure. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Product decision: Pro markets "Unlimited high-quality server-side PDFs"; the monthly cap is an anti-abuse tripwire (~30x real freelancer usage), not a plan entitlement, so it comes off the pricing card. The 429 for the rare tripper still states the number (interpolated from the constant). Worst-case exposure $0.38/user at 15s renders (~93% margin vs $5.36 net); docs updated to match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Owner
Author
|
Scope update (2026-07-15): since this PR was opened, the branch grew from the groundwork to the full code-complete roadmap (10 new commits,
Everything remains dark behind
🤖 Generated with Claude Code |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Ships the full app-side monetization layer for FreeInvoice Pro, dark-launched behind
MONETIZATION_ENABLED = "false"inwrangler.toml. Merging this changes nothing for users; flipping the var (plus setting Polar secrets) is the launch. Strategy indocs/MONETIZATION.md, operational launch checklist indocs/POLAR_SETUP.md.Entitlements & gates (no-ops until the flag flips)
subscriptionstable +locals.tierresolved per request;requirePro()helperRate limiting (active immediately — cost protection)
Polar billing (inert until
POLAR_*secrets are set)/api/billing/checkout→ hosted checkout (externalCustomerId= user id),/api/billing/portal→ hosted customer portal,/api/billing/webhook→ signature-verified upsert intosubscriptions, with a guard that never downgrades an active lifetime planUI & misc
app.html(manual snippet — auto-injection never worked for the Workers-served app)Post-merge checklist (before flipping the flag)
migrations/0001_rate_limits.sqland0002_subscriptions.sqlto remote D1docs/POLAR_SETUP.md)MONETIZATION_ENABLED = "true"Test plan
npm run check— 0 errors, 0 warningsnpm run build— passes (including prerender)🤖 Generated with Claude Code