github usertour/usertour v0.7.4

4 hours ago

This release lands a shared plan-features matrix that finally unifies how the server enforces plan gates and how the pricing page presents
them — Subscription.overridePlan lights up across the stack so per-customer CS grants take effect everywhere instead of just one layer. The
team-invite path also gains long-missing safety nets, and the pricing settings page aligns with usertour.io's marketing copy.

What's Changed

🧱 Plan features matrix (cross-cutting infrastructure)

  • New PlanFeatures type in @usertour/types and a shared PLAN_FEATURES matrix in @usertour-packages/constants/billing that covers every
    per-tier value the product gates on today: removeBranding, sessionsLimit, teamMemberLimit, environmentLimit, dataRetentionYears,
    apiRateLimit, plus placeholders for upcoming auditLogs / ssoSaml / ssoOidc gates.
  • resolvePlanFeatures(planType, overridePlan), parseOverridePlan, and isWithinLimit helpers live in @usertour/helpers and run on both server
    and web — the same merged feature set drives runtime enforcement and UI rendering.
  • Subscription.overridePlan (the JSONB column that was wired up but unread) is now the canonical layer for per-customer grants. Override
    fields replace base fields via spread, so a CS-granted seat bump or a legacy benefit ({"removeBranding": true} for grandfathered Starter
    projects) lands in resolution without any code change.
  • @usertour-packages/constants was promoted from P1 (source-direct) to P2 (pre-built dist) since the NestJS server now requires it at
    runtime. The P1 → P2 migration steps are codified in docs/architecture/packages.md.
  • PlanType enum gains ENTERPRISE so the self-hosted license path stops using bare strings; the two existing Record<PlanType, …> maps on the
    web pick up the new key.

🛡 Server-side quota enforcement

  • ProjectsService gains resolveProjectFeatures, checkEnvironmentLimit, and checkTeamMemberLimit. The cloud / self-hosted bypass, subscription
    lookup, override merge, count query, and error throw all live in one place and accept a Prisma TransactionClient.
  • EnvironmentsService.create was previously gated only by the client — useEnvironmentLimit disabled the form button but any consumer that
    bypassed the form (stale UI, direct mutation call) could create past the cap. The mutation now delegates to
    projectsService.checkEnvironmentLimit inside its own transaction. New EnvironmentLimitError (E0030) with en/zh messages.
  • TeamService.inviteTeamMember's hand-rolled hobby/starter/growth if-chain is gone; the gate now goes through
    projectsService.checkTeamMemberLimit, which honours overridePlan the same way the client does. The roughly 230-line dup between
    projects.service and web-socket.service for cloud / self-hosted config resolution also collapses — web-socket.service.getConfig delegates to
    projects.

📧 Team invite hardening

  • The same email used to accumulate multiple Invite pending rows on the team settings page because there was no dedup before
    prisma.invite.create. Each row counted against the seat quota, letting a project artificially fill its allotment by inviting one address
    twice. Two new errors register pre-create: TeamMemberAlreadyInvitedError (E0031) and TeamMemberAlreadyInProjectError (E0032).
  • When the SMTP layer rejected the recipient (e.g. 550 / EENVELOPE for a bad mailbox), the freshly created invite row stayed in the DB while
    the client got a generic 500 and the new dedup check blocked the user from retrying. The mutation now wraps sendInviteEmail in a try / catch
    — failure soft-deletes the invite (matching the cancel / accept lifecycle) and surfaces InvitationDeliveryFailedError (E0033).

🎨 Pricing page polish

  • Comparison table aligns with usertour.io: Support & service section becomes Community / Email / Priority with priority gated to Business
    only, the ghost Concierge support row is gone, and the Growth card's redundant Live chat support line drops back to Email support.
  • Every drift-prone row (Sessions / Data Retention / Environments / API rate / Team members / No-branding) is now matrix-driven via a typed
    matrixRow helper instead of a hardcoded values: [false, true, true, true] literal.
  • Per-customer override surfaces only on the user's current plan card and current plan column. Other cards stay base so a CS-granted sessions
    bump on Growth doesn't make the Starter or Business columns claim the same number. The comparison table stays apples-to-apples for upgrade
    decisions while honouring the user's actual entitlement on their own row.
  • Pricing page icons switch from lucide-react + a custom BoxIcon to the project's standard remix icons (@usertour-packages/icons).
  • Unlimited sessions render as 123 / Unlimited instead of 123 / Infinity; the percent / threshold caption hides when the cap is unbounded.

🪝 Quota hooks on the web

  • SubscriptionContext now exposes effective features: PlanFeatures so consumers stop re-resolving themselves.
  • apps/web/src/hooks/use-plan-limits.ts adds useEnvironmentLimit, useTeamMemberLimit, and useSessionsLimit. Each hook composes the
    subscription features, the relevant resource list, and the self-hosted bypass into a single { limit, current, canUseMore } shape. Consumers
    like EnvironmentCreateForm and MemberInviteDialog shrink to one line and now honour override on the client side too — the server check the
    hook mirrors used to be the only place that saw the override.

📘 Architecture doctrine

  • docs/architecture/packages.md gets a @usertour/types vs @usertour-packages/constants split (contracts vs values, with the "delete-the-line,
    what breaks?" heuristic), a P1 / P2 package-shape section with the 6-step migration checklist that this branch's constants promotion
    follows, and a note that runtime values should share as soon as a second real consumer exists — including code that copies the same business
    contract values without importing them.

Full Changelog: v0.7.3...v0.7.4

Don't miss a new usertour release

NewReleases is sending notifications on new releases.