Skip to content

Monetization groundwork: entitlements, Pro gates, rate limiting, Polar billing (dark-launched) - #34

Open
azs06 wants to merge 14 commits into
mainfrom
monetization
Open

Monetization groundwork: entitlements, Pro gates, rate limiting, Polar billing (dark-launched)#34
azs06 wants to merge 14 commits into
mainfrom
monetization

Conversation

@azs06

@azs06 azs06 commented Jul 14, 2026

Copy link
Copy Markdown
Owner

Summary

Ships the full app-side monetization layer for FreeInvoice Pro, dark-launched behind MONETIZATION_ENABLED = "false" in wrangler.toml. Merging this changes nothing for users; flipping the var (plus setting Polar secrets) is the launch. Strategy in docs/MONETIZATION.md, operational launch checklist in docs/POLAR_SETUP.md.

Entitlements & gates (no-ops until the flag flips)

  • subscriptions table + locals.tier resolved per request; requirePro() helper
  • Premium templates: blocked on both save paths (POST + PUT) only when newly adopting one — existing invoices with a premium template stay editable forever (grandfathering)
  • Free-tier quotas: 10 cloud invoices (creates only, updates never blocked), 3 active share links
  • Server-side PDF (Browser Rendering) becomes Pro-only; client silently falls back to html2pdf
  • Shared-page "Made with FreeInvoice" attribution hidden for Pro invoice owners

Rate limiting (active immediately — cost protection)

  • D1 fixed-window counters on PDF generation (20/hr), share-link creation (30/day), invoice saves (100/hr)
  • Fails open with a warning if the table/binding is missing, so dev and pre-migration deploys are safe

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 into subscriptions, with a guard that never downgrades an active lifetime plan

UI & misc

  • Pricing page, upgrade modal, premium lock in template selector, billing section in settings (replaces "coming soon"), PaymentDetails field on the editor (pay-this-invoice v1 groundwork)
  • Cloudflare Web Analytics beacon in app.html (manual snippet — auto-injection never worked for the Workers-served app)
  • Docs refresh: CLAUDE.md/AGENTS.md describe the real architecture; old PLAN.md archived

Post-merge checklist (before flipping the flag)

  • Apply migrations/0001_rate_limits.sql and 0002_subscriptions.sql to remote D1
  • Create Polar products, webhook, and secrets (see docs/POLAR_SETUP.md)
  • Sandbox end-to-end test, then set MONETIZATION_ENABLED = "true"

Test plan

  • npm run check — 0 errors, 0 warnings
  • npm run build — passes (including prerender)
  • Sandbox Polar checkout → webhook → Pro tier (needs Polar products; per POLAR_SETUP.md §6)

🤖 Generated with Claude Code

azs06 and others added 2 commits July 14, 2026 17:11
…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>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 14, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
invoice-generator 5c690da Commit Preview URL Jul 15 2026, 12:01 PM

azs06 and others added 3 commits July 15, 2026 16:21
- 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>
@cursor

cursor Bot commented Jul 15, 2026

Copy link
Copy Markdown

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.

azs06 and others added 9 commits July 15, 2026 16:54
- /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>
@azs06

azs06 commented Jul 15, 2026

Copy link
Copy Markdown
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, 7fec86d..5c690da):

  • Phase 3: real email sending (Cloudflare Email Sending), recurring invoices (hourly cron + wrapper worker + migration 0003), pay-this-invoice v1, client address book (migration 0004), overdue reminders (migration 0005)
  • Phase 4: reports dashboard (/dashboard/reports, per-currency aggregation), AI invoice-from-text (Workers AI Gemma 4, new [ai] binding)
  • Analytics: Workers Analytics Engine funnel events (METRICSinvoice_events), incl. gate_blocked at every 402
  • Cost protection: monthly PDF anti-abuse ceiling (1000/mo) on top of the hourly cap, per docs/COST_ANALYSIS.md
  • New docs: PRODUCTION_TODO.md (launch checklist), COST_ANALYSIS.md

Everything remains dark behind MONETIZATION_ENABLED="false" — merging still changes nothing for users except the cron/features that are ungated-by-design (recurring, reminders, clients, AI fill, reports become available to all signed-in users until the flag flips).

⚠️ Before merging (merge auto-deploys):

  1. Apply remote D1 migrations 0003–0005 (wrangler d1 execute invoice-db --remote --file=./migrations/000X_*.sql) — and confirm 0001/0002 are applied
  2. Email DNS: wrangler email sending enable freeinvoice.info + SPF/DKIM — otherwise the hourly cron's sends fail with E_SENDER_NOT_VERIFIED
  3. Local smoke test per docs/PRODUCTION_TODO.md Stage 1 (none of the new features or 402 paths have been runtime-exercised yet)

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant