How the AppFlowy Billing and Subscription System Works: Technical Architecture
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 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 withworkspace_id,recurring_interval, andplan, 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 toUserCloudService::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.
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, PeriodicallyCheckBillingState implements a finite state machine that repeatedly queries the cloud until the subscription change propagates.
pub struct PeriodicallyCheckBillingState {
workspace_id: Uuid,
cloud_service: Weak<dyn UserCloudServiceProvider>,
expected_plan: Option<SubscriptionPlan>,
user: Weak<AuthenticateUser>,
}
The polling algorithm operates as follows:
- Attempt limit: The poller retries up to 5 times with a fixed 4-second delay (
Duration::from_secs(4)) between attempts. - State verification: Each iteration calls
get_workspace_plan()via the cloud service provider to fetch current plans. - Early termination: If the
expected_planappears in the response, the loop returns immediately with the plan list. - Workspace consistency: The poller verifies the active workspace hasn't changed during polling to prevent race conditions.
- 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:
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:
- Payment initiation: The UI calls
subscribe_workspacewith plan details and a success URL:
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
-
Success handling: After payment, the cloud redirects to the success URL, triggering a
SuccessWorkspaceSubscriptionPBevent. -
State polling:
notify_did_switch_planinstantiatesPeriodicallyCheckBillingStateand awaits confirmation:
let success_pb = SuccessWorkspaceSubscriptionPB {
workspace_id: workspace_id_str,
plan: Some(WorkspaceSubscriptionPlan::Pro),
// ... other fields
};
user_manager.notify_did_switch_plan(success_pb).await?;
- 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-userRust crate, usingUserManagerto proxy requests to AppFlowy Cloud. - Payment flow:
subscribe_workspacegenerates payment links, whilenotify_did_switch_planhandles success callbacks by spawning a state poller. - Polling mechanism:
PeriodicallyCheckBillingStateretries 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
UserCloudServicetrait, 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 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 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →