# How the AppFlowy Billing and Subscription System Works: Technical Architecture

> Discover the AppFlowy billing and subscription system. Learn how its cloud-backed Rust backend polls AppFlowy Cloud for status changes with retries and confirmation for seamless plan management.

- Repository: [AppFlowy-IO/AppFlowy](https://github.com/AppFlowy-IO/AppFlowy)
- Tags: architecture
- Published: 2026-03-03

---

**AppFlowy uses a cloud-backed polling mechanism where the Rust backend periodically queries AppFlowy Cloud to verify subscription status changes, attempting up to 5 retries with 4-second intervals until the expected plan is confirmed.**

The **AppFlowy billing and subscription system** is implemented in the `flowy-user` Rust crate, which proxies workspace plan management requests to AppFlowy Cloud and handles asynchronous state verification. This architecture keeps the Flutter UI thin while ensuring accurate subscription state synchronization through persistent polling.

## Managing Workspace Subscription Plans

The `UserManager` struct in [`frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs`](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs) exposes methods that forward subscription operations to the cloud service. These methods act as thin wrappers around the `UserCloudService` trait, handling request serialization and response parsing.

**Subscription management methods include:**

- **`subscribe_workspace`** (lines 510‑525): Builds a payment link request with `workspace_id`, `recurring_interval`, and `plan`, returning a URL that the UI opens in a browser.
- **`get_workspace_subscription_info`** (lines 527‑540): Retrieves current subscription details by parsing the workspace UUID and forwarding the call to the cloud.
- **`cancel_workspace_subscription`** (lines 541‑554): Proxies cancellation requests to `UserCloudService::cancel_workspace_subscription`.
- **`update_workspace_subscription_payment_period`** (lines 556‑569): Handles billing interval changes (monthly/yearly).
- **`get_subscription_plan_details`** (lines 571‑579): Fetches available plan definitions from the cloud.
- **`get_workspace_usage`** (lines 581‑608): Checks storage quotas and informs the app lifecycle whether write operations are permitted.
- **`get_billing_portal_url`** (lines 610‑618): Retrieves the customer portal URL for payment method management.

All these operations rely on the `UserCloudServiceProvider` trait to abstract cloud communication, ensuring the client remains agnostic of specific billing provider implementations.

## Processing Payment Success Callbacks

When payment succeeds, AppFlowy Cloud sends a `SuccessWorkspaceSubscriptionPB` event to the client. The UI layer triggers `UserManager::notify_did_switch_plan` (starting at line 751) to initiate state verification.

```rust
pub async fn notify_did_switch_plan(&self, success: SuccessWorkspaceSubscriptionPB) -> FlowyResult<()> {
    let workspace_id = Uuid::from_str(&success.workspace_id)?;
    let plans = PeriodicallyCheckBillingState::new(
        workspace_id,
        success.plan.map(SubscriptionPlan::from),
        self.cloud_service.clone(),
        Arc::downgrade(&self.authenticate_user),
    )
    .start()
    .await?;
    
    self.app_life_cycle.read().await.on_subscription_plans_updated(plans);
    Ok(())
}

```

This method extracts the **expected plan** from the protobuf message and spawns a billing state poller. Upon completion, it pushes the verified `SubscriptionPlan` list to the application lifecycle via `on_subscription_plans_updated`, triggering UI refreshes.

## The PeriodicallyCheckBillingState Poller

Located in [`frontend/rust-lib/flowy-user/src/services/billing_check.rs`](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/rust-lib/flowy-user/src/services/billing_check.rs), `PeriodicallyCheckBillingState` implements a finite state machine that repeatedly queries the cloud until the subscription change propagates.

```rust
pub struct PeriodicallyCheckBillingState {
    workspace_id: Uuid,
    cloud_service: Weak<dyn UserCloudServiceProvider>,
    expected_plan: Option<SubscriptionPlan>,
    user: Weak<AuthenticateUser>,
}

```

The polling algorithm operates as follows:

1. **Attempt limit**: The poller retries up to **5 times** with a fixed **4-second delay** (`Duration::from_secs(4)`) between attempts.
2. **State verification**: Each iteration calls `get_workspace_plan()` via the cloud service provider to fetch current plans.
3. **Early termination**: If the `expected_plan` appears in the response, the loop returns immediately with the plan list.
4. **Workspace consistency**: The poller verifies the active workspace hasn't changed during polling to prevent race conditions.
5. **Fallback behavior**: If max attempts are exhausted without finding the expected plan, the poller returns the latest plan snapshot or a timeout error.

This polling mechanism bridges the eventual consistency gap between payment provider webhooks and the cloud's internal state.

## UI Integration and Lifecycle Hooks

The Flutter frontend receives updates through the `AppLifecycle` interface. When `PeriodicallyCheckBillingState` completes, `UserManager` calls:

```rust
self.app_life_cycle.read().await.on_subscription_plans_updated(plans);

```

This lifecycle hook updates the Billing page UI (implemented in `frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_billing_view.dart`), refreshes plan badges, and toggles premium feature availability. The UI can also proactively query current state using `get_workspace_subscription_info` when users open Settings → Billing.

## Complete Subscription Flow Example

A typical upgrade workflow demonstrates how these components interact:

1. **Payment initiation**: The UI calls `subscribe_workspace` with plan details and a success URL:

```rust
let subscription_pb = SubscribeWorkspacePB {
    workspace_id: workspace_id.to_string(),
    recurring_interval: RecurringInterval::Monthly,
    workspace_subscription_plan: WorkspaceSubscriptionPlan::Pro,
    success_url: "appflowy://subscription_success".into(),
};

let payment_link = user_manager.subscribe_workspace(subscription_pb).await?;
// UI opens payment_link in browser

```

2. **Success handling**: After payment, the cloud redirects to the success URL, triggering a `SuccessWorkspaceSubscriptionPB` event.

3. **State polling**: `notify_did_switch_plan` instantiates `PeriodicallyCheckBillingState` and awaits confirmation:

```rust
let success_pb = SuccessWorkspaceSubscriptionPB {
    workspace_id: workspace_id_str,
    plan: Some(WorkspaceSubscriptionPlan::Pro),
    // ... other fields
};

user_manager.notify_did_switch_plan(success_pb).await?;

```

4. **UI update**: Once the poller detects the Pro plan (or exhausts retries), the lifecycle hook refreshes the billing interface and enables premium features.

## Summary

- The **AppFlowy billing and subscription system** centralizes logic in the `flowy-user` Rust crate, using `UserManager` to proxy requests to AppFlowy Cloud.
- **Payment flow**: `subscribe_workspace` generates payment links, while `notify_did_switch_plan` handles success callbacks by spawning a state poller.
- **Polling mechanism**: `PeriodicallyCheckBillingState` retries cloud queries up to 5 times with 4-second intervals until the expected subscription plan appears.
- **UI synchronization**: Verified plan changes propagate through `AppLifecycle::on_subscription_plans_updated`, updating the Flutter billing interface and feature gates.
- **All subscription operations** are abstracted behind the `UserCloudService` trait, maintaining clean separation between client logic and cloud implementation.

## Frequently Asked Questions

### How does AppFlowy verify that a payment actually succeeded?

AppFlowy relies on the `PeriodicallyCheckBillingState` poller to verify payments rather than trusting the success URL callback alone. After receiving the `SuccessWorkspaceSubscriptionPB` event, the system polls AppFlowy Cloud up to 5 times with 4-second delays, querying `get_workspace_plan()` until the expected subscription tier appears in the response. This prevents race conditions where the webhook might fire before the database updates.

### What happens if the billing state poller never finds the expected plan?

If `PeriodicallyCheckBillingState` exhausts its 5 retry attempts without detecting the expected plan, it returns either the current plan snapshot or a timeout error to `UserManager`. The UI layer receives this result through `on_subscription_plans_updated` and typically surfaces a message indicating the upgrade status is pending or failed, prompting users to refresh or contact support.

### Where is the subscription polling logic implemented in the codebase?

The polling logic resides in [`frontend/rust-lib/flowy-user/src/services/billing_check.rs`](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/rust-lib/flowy-user/src/services/billing_check.rs) within the `PeriodicallyCheckBillingState` struct. The orchestration method `notify_did_switch_plan` that creates this poller is located in [`frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs`](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs) starting at line 751.

### How does the Flutter UI receive updates when a subscription changes?

The Rust backend pushes updates through the `AppLifecycle` trait method `on_subscription_plans_updated`, which the Flutter layer implements to refresh the billing settings page. Additionally, the UI can proactively call `get_workspace_subscription_info` when loading the Settings → Billing screen to fetch current state without waiting for polling completion.