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. providerPaymentRefstarting withsub_orpricingType=recurring→monthly; elselifetime.- Plans config:
config/paywall.js(guardedRoutes,plans.oneTime,plans.monthly) fed byconfig/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.