How to Add Subscriptions to a Desktop Application
Desktop apps have unique subscription challenges: offline access, license validation, and cross-platform auth. Learn how to handle them with StackBE.
In This Guide
Comparisons
Desktop Subscriptions Are Different
Desktop applications present unique challenges for subscription billing that web apps don't face:
Whether you're building with Electron, Tauri, or native frameworks, the billing patterns are similar.
Architecture
Desktop App (Electron/Tauri/Native)
↕ API calls (when online)
StackBE (auth, billing, entitlements)
↕ Payment processing
Stripe (payment gateway)
Local cache for offline access
Key Design Decisions
1. How often to validate: On launch? Daily? Every time?
2. Offline behavior: Full access? Limited? None?
3. License storage: Where to cache subscription data locally?
Authentication Flow
OAuth-Style Browser Auth
Desktop apps typically authenticate via the system browser:
1. App opens system browser to login URL
2. User authenticates (magic link)
3. Browser redirects to custom protocol handler
4. App captures the auth token
// Electron exampleconst { shell } = require('electron');function startLogin() {// Register protocol handlerapp.setAsDefaultProtocolClient('myapp');// Open browser to StackBE loginshell.openExternal(
'https://yourapp.com/auth/desktop?redirect=myapp://auth/callback'
);
}// Handle the callbackapp.on('open-url', (event, url) => {const token = new URL(url).searchParams.get('token');if (token) {storeSession(token);
checkSubscription(token);
}});Custom Protocol Registration
Register your app's protocol in the OS:
Electron:
app.setAsDefaultProtocolClient('myapp');macOS Info.plist:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>
Subscription Validation
Online Validation
When the app has internet, verify subscription with StackBE:
async function validateSubscription(token) {try {
const response = await fetch('https://api.stackbe.io/v1/subscriptions/current', {headers: {
'Authorization': `Bearer ${token}`,
'X-Api-Key': API_KEY,
'X-App-Id': APP_ID
}});if (response.ok) {const subscription = await response.json();// Cache locally for offline usecacheSubscription({
plan: subscription.plan.slug,
status: subscription.status,
expiresAt: subscription.currentPeriodEnd,
validatedAt: new Date().toISOString()
});return subscription;}} catch (error) {// Network error - use cached datareturn getCachedSubscription();}}Offline Validation
When offline, use cached subscription data:
function getCachedSubscription() {const cached = store.get('subscription');if (!cached) {return { plan: 'free', status: 'no_cache' };}// Check if cache is still validconst validatedAt = new Date(cached.validatedAt);const now = new Date();const hoursSinceValidation = (now - validatedAt) / (1000 * 60 * 60);// Allow offline access for up to 7 daysif (hoursSinceValidation > 168) {return { plan: 'free', status: 'cache_expired' };}// Check if subscription period hasn't endedif (new Date(cached.expiresAt) < now) {return { plan: 'free', status: 'subscription_expired' };}return cached;}Validation Strategy
async function getSubscriptionStatus(token) {// Try online validation firstif (navigator.onLine) {const online = await validateSubscription(token);if (online) return online;}// Fall back to cached datareturn getCachedSubscription();}Feature Gating
Based on Subscription
function initializeFeatures(subscription) {const isPro = subscription.plan === 'pro' &&['active', 'trialing'].includes(subscription.status);
// Enable/disable menu itemssetMenuEnabled('Export PDF', isPro);
setMenuEnabled('Batch Processing', isPro);
setMenuEnabled('Cloud Sync', isPro);
// Show upgrade prompt for locked featuresif (!isPro) {showUpgradeBar('Upgrade to Pro for unlimited access');
}}Entitlement Checks (Online)
async function canUseFeature(feature) {// Check cache firstconst cached = getCachedEntitlements();if (cached && cached[feature] !== undefined) {return cached[feature];}// Check online if availableif (navigator.onLine) {const { hasAccess } = await stackbe.entitlements.check(customerId,
feature
);
// Cache the resultcacheEntitlement(feature, hasAccess);
return hasAccess;}// Default to cached or free tierreturn false;}Upgrade Flow
In-App Upgrade
Open Stripe checkout in the system browser:
async function handleUpgrade(planId) {const { url } = await stackbe.checkout.createSession({customer: customerId,
planId,
successUrl: 'https://yourapp.com/upgrade-success',
cancelUrl: 'https://yourapp.com/pricing',
});// Open in system browsershell.openExternal(url);
// Poll for subscription changestartPollingForUpgrade();
}function startPollingForUpgrade() {const interval = setInterval(async () => {const sub = await validateSubscription(sessionToken);if (sub.plan !== 'free') {clearInterval(interval);
initializeFeatures(sub);
showNotification('Upgrade successful! Pro features enabled.');
}}, 5000);// Stop polling after 10 minutessetTimeout(() => clearInterval(interval), 600000);
}Local Storage Security
Secure Token Storage
Don't store tokens in plain text files:
Electron (keychain):
const keytar = require('keytar');// Storeawait keytar.setPassword('myapp', 'session', token);// Retrieveconst token = await keytar.getPassword('myapp', 'session');// Delete on logoutawait keytar.deletePassword('myapp', 'session');Alternative (encrypted store):
const Store = require('electron-store');const store = new Store({encryptionKey: getOrCreateMachineKey(),
});store.set('subscription', subscriptionData);
Platform-Specific Considerations
Electron
Tauri
Native (Swift/Kotlin/C#)
Offline Grace Period
Define a reasonable offline policy:
Online validation: Check on every launch (if connected)
Offline grace: 7 days of full access without validation
Expired cache: Revert to free tier features
Cache renewal: Any successful online validation resets the timer
This balances:
Anti-Piracy Considerations
Desktop apps face piracy challenges:
Accept: Some piracy is inevitable. Don't punish paying customers with aggressive DRM.
Mitigate:
Don't:
The best anti-piracy strategy is providing enough value that paying is the natural choice.
Testing
Test these scenarios:
Next Steps
1. Choose your framework: Electron, Tauri, or native
2. Implement auth: Browser-based login with custom protocol
3. Add subscription validation: Online check with offline cache
4. Gate features: Enable/disable based on plan
5. Build upgrade flow: In-app prompt → browser checkout
6. Test offline scenarios: Ensure graceful degradation
Related Resources
SaaS Billing Guide
Complete billing fundamentals
Entitlements Guide
Feature access patterns
Chrome Extension Guide
Browser extension billing
StackBE vs Gumroad
Subscriptions vs one-time licenses
StackBE vs LemonSqueezy
License keys vs entitlements
Auth API Docs
Authentication integration
Ready to simplify your SaaS backend?
StackBE combines auth, billing, and entitlements in one API. Get started in minutes, not weeks.
Get Started FreeFrequently Asked Questions
Other Guides
The Complete Guide to SaaS Billing
Master SaaS billing from subscription models to payment recovery. Learn how to implement billing that scales with your business.
The Complete Guide to SaaS Authentication
Master SaaS authentication from passwordless to enterprise SSO. Learn how to implement secure, user-friendly auth for your application.
The Complete Guide to SaaS Entitlements
Master SaaS entitlements—the system that controls what features users can access based on their subscription. Learn why it's different from feature flags.
How to Add Subscriptions to a Chrome Extension
Chrome extensions have unique billing challenges. Learn how to implement subscriptions, handle authentication in a browser context, and gate features by plan.
How to Add Subscriptions to a Next.js App
Next.js is the most popular framework for SaaS. Learn how to add authentication, subscription billing, and feature gating with StackBE.
How to Monetize Your API Product
Turning your API into a business requires more than just code. Learn pricing strategies, usage-based billing, and access control for API products.