zudo-text

検索したい単語を入力

いつでも検索バーを開ける

Subscription Flow

Overview

Sync features in zudo-text require a Pro subscription. This document covers the authentication, payment, subscription lifecycle, and feature gating architecture.

Authentication

Provider

  • Better Auth mounted in sync-server for email/password identity and sessions

  • RS256 service JWTs for API authentication

  • The opaque seven-day sliding session is persisted by the client; the 15-minute service JWT is cached separately. There is no refresh-token grant. See Better Auth.

Auth Flow

User clicks "Sign In"
    → Client opens the sync-server Better Auth handoff
    → User authenticates with email and password
    → Handoff returns a single-use OTT plus the client's CSRF state
    → Client verifies the OTT and receives the sliding session token
    → Client calls GET /api/auth/token for a 15-minute service JWT
    → Frontend receives AuthState { isAuthenticated: true, user: { ... } }

Auth State

The BackendAPI.auth domain manages authentication state:

interface AuthUser {
  id: string;
  email: string;
  name: string;
  picture?: string;
}

interface AuthState {
  isAuthenticated: boolean;
  user: AuthUser | null;
}

Components subscribe to auth changes via auth.onStateChanged(). The SyncContext gates all sync operations behind isAuthenticated.

Payment

Provider

  • Stripe for subscription billing

  • Stripe Customer Portal for self-service subscription management (update card, cancel, view invoices)

Plans

PlanPriceSync AccessNotes
Free$0NoDefault for all new accounts
ProPaidFullFile Sync + Cloud Sync + real-time WebSocket

Trial

  • 30-day free trial with full Pro access

  • No credit card required to start

  • Trial starts when the user explicitly clicks "Start Trial"

  • Countdown visible in settings when 7 days or fewer remaining

Subscription States

type SubscriptionStatus =
  | "free"
  | "trial"
  | "active"
  | "past_due"
  | "cancelled"
  | "expired";

type SubscriptionPlan = "free" | "pro";
StatusDescriptionSync Access
freeDefault state, no subscriptionNo
trial30-day free trial in progressYes
activePaid subscription, billing currentYes
past_duePayment failed, retry in progressYes (temporary)
cancelledWill expire at end of current billing periodNo
expiredTrial or subscription endedNo

Subscription Info

interface SubscriptionInfo {
  plan: SubscriptionPlan;
  status: SubscriptionStatus;
  trialStartDate: string | null;
  trialEndDate: string | null;
  currentPeriodEnd: string | null;
  cancelAtPeriodEnd: boolean;
}

State Transitions

                        ┌─────────────────────┐
                        │       free           │ ← default for new accounts
                        └──────┬──────┬───────┘
                               │      │
                  Start Trial  │      │  Subscribe directly
                               ▼      ▼
                        ┌──────────┐  ┌──────────┐
                        │  trial   │  │  active   │◀─── payment succeeds
                        └────┬─────┘  └────┬──────┘
                             │             │
              ┌──────────────┤             ├──────────────┐
              │              │             │              │
    Trial expires    Subscribe    Payment fails     Cancels
              │              │             │              │
              ▼              ▼             ▼              ▼
        ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌───────────┐
        │ expired  │  │  active  │  │ past_due │  │ cancelled │
        └──────────┘  └──────────┘  └─────┬────┘  └─────┬─────┘
                                          │              │
                                   Retry succeeds   Period ends
                                          │              │
                                          ▼              ▼
                                    ┌──────────┐   ┌──────────┐
                                    │  active  │   │ expired  │
                                    └──────────┘   └──────────┘

Transition Details

FromToTrigger
freetrialUser clicks "Start Trial"
freeactiveUser subscribes directly (Stripe Checkout)
trialactiveUser subscribes during trial
trialexpired30 days elapsed without subscribing
activepast_dueStripe payment fails (auto-retry begins)
activecancelledUser cancels (remains active until period end)
past_dueactiveRetry payment succeeds
past_dueexpiredAll retry attempts exhausted
cancelledexpiredCurrent billing period ends
expiredactiveUser resubscribes

Feature Gating

Sync Access

Sync features are available only when the subscription status grants access:

function canAccessSync(info: SubscriptionInfo): boolean {
  return info.status === "trial" || info.status === "active" || info.status === "past_due";
}

UI Behavior by Status

StatusSync SettingsSync OperationsUI Indicators
freeHidden (shows benefits dialog)Blocked"Upgrade to Pro" prompt
trialVisibleEnabledTrial countdown (days remaining)
activeVisibleEnabled"Pro" badge
past_dueVisibleEnabled"Payment issue" warning
cancelledVisible (read-only)Disabled"Expires on (date)" notice
expiredVisible (read-only)Blocked"Resubscribe" prompt

Benefits Dialog

When a free user attempts to access sync settings, a benefits dialog is shown instead of the sync configuration panel. The dialog highlights:

  • Cross-device sync with E2E encryption

  • Real-time collaboration via WebSocket

  • Offline support with automatic queue

  • 30-day free trial, no credit card required

Trial Warning

When the trial has 7 or fewer days remaining, a warning banner appears in the sync settings section showing the remaining days and a link to subscribe.

Graceful Degradation

When a subscription expires:

  • All local files are preserved — nothing is deleted

  • Sync is disabled but previously synced data remains intact

  • The user can still read, edit, and manage all their local content

  • Re-subscribing immediately restores sync with the existing workspace

Server-Side Integration

Better Auth identity mapping

Better Auth mints sub: better_auth|<user.id>. Protected application routes resolve that exact subject against the existing users table, which remains authoritative for subscriptions and workspace ownership. A missing mapping fails closed rather than silently provisioning access. After the handoff establishes the Better Auth session, GET /api/auth/token returns the short-lived service JWT used by the subscription routes.

Stripe Webhook

The server listens for Stripe webhook events to update subscription status:

Stripe EventAction
customer.subscription.createdSet status to active
customer.subscription.updatedUpdate status, period dates
customer.subscription.deletedSet status to expired
invoice.payment_failedSet status to past_due
customer.subscription.trial_will_endSend trial ending notification

Customer Portal

The GET /subscription/portal-url endpoint generates a Stripe Customer Portal session URL where users can:

  • Update payment method

  • View billing history

  • Cancel or resubscribe

  • Download invoices