Boring Docs

Paywall — One Boolean, Two Plans

requirePaidUser, PaywallGate, guarded routes, vault, claim-repo.

In this guide

Paywall

Single source of truth: libs/paywall.js:requirePaidUser(){ session, user, hasAccess, planType, isLifetime, isMonthly }.

Semantics

  • No/stale session → hasAccess=false, planType=free.
  • Any completed order OR user.hasAccess → access true.
  • providerPaymentRef starting with sub_ or pricingType=recurringmonthly; else lifetime.
  • Plans config: config/paywall.js (guardedRoutes, plans.oneTime, plans.monthly) fed by config/billing.js (sale, dodo.plans).

Guarding

Pages (branch):

const { hasAccess, planType } = await requirePaidUser();
// + isAdmin(user) bypass where appropriate (/vault)
<PaywallGate hasAccess={hasAccess} planType={planType} preview={…}>

APIs (map): 401 no/stale session → 403 free user. Example: app/api/board/route.js:paidOrError().

Guarded by default: /vault, /pro, /dashboard/boards, /dashboard/analytics (see paywall.guardedRoutes).

UI

components/PaywallGate.js — unlocked banner + children when hasAccess, else blurred preview + Pay Once vs Monthly cards via ButtonSubscribe (posts to /api/billing/create-checkout, falls back to checkoutUrl).

Claim repo

POST /api/claim-repo — session required, githubUsername regex ^[a-zA-Z0-9-]{1,39}$, access bound to the authenticated user (session id/email → orders), persists to user.githubUsername on all DBs, then GitHub collaborator invite via GITHUB_ACCESS_TOKEN/GITHUB_REPO or pendingManual fallback.