App Communication Guide

App Communication Guide

A reference for connecting a Desktop App, Website, and Admin Dashboard through a shared backend. This guide is technology-agnostic — it covers the architecture patterns and shows where different databases, auth providers, payment systems, and hosting options plug in.


Table of Contents

  1. The Big Picture
  2. Architecture Overview
  3. Choosing a Database
  4. Authentication Providers
  5. Payment Systems
  6. Webhooks (Local and Online)
  7. Environment Variables Overview
  8. User Authentication Flow
  9. AI Credits System
  10. Desktop App <-> Backend Communication
  11. Website (Download Page)
  12. Owner Dashboard
  13. Real-time Sync
  14. Offline Support
  15. Auto-Updates for Desktop
  16. Error Handling Strategy
  17. Deployment Topology
  18. Security Considerations
  19. Implementation Checklist

The Big Picture

+------------------------------------------------------------------+
|                         YOUR ECOSYSTEM                           |
+------------------------------------------------------------------+
|                                                                  |
|   Desktop App            Website              Dashboard          |
|   (The main tool)        (The store)          (Your HQ)          |
|                                                                  |
|        |                     |                     |             |
|        +---------------------+---------------------+             |
|                              |                                   |
|                   +----------+----------+                        |
|                   |   Central Backend   |                        |
|                   |    (The brain)      |                        |
|                   +----------+----------+                        |
|                              |                                   |
|                   +----------+----------+                        |
|                   |     Database(s)     |                        |
|                   |    (The memory)     |                        |
|                   +---------------------+                        |
+------------------------------------------------------------------+
Component Purpose Who Uses It
Desktop App The actual product users download and use Your customers
Website Marketing site + download page + user portal Potential and existing customers
Dashboard Admin panel for you to manage everything You (the owner)
Backend API server that connects everything All three apps
Database(s) Stores users, credits, analytics, content Backend

Architecture Overview


Choosing a Database

There is no single "best" database. Most production systems use 2-3 types together. Choose based on what your data looks like and how you query it.

Database Types

Type Best For Examples When to Use
Relational (SQL) Structured data, transactions, joins PostgreSQL, MySQL, SQLite, CockroachDB User accounts, purchases, anything with relationships
Document Flexible schemas, nested data MongoDB, Firebase Firestore, CouchDB Content, user profiles, settings, JSON-heavy data
Key-Value / Cache Fast reads, sessions, ephemeral data Redis, Memcached, DynamoDB Session tokens, rate limiting, real-time counters, caching
Graph Relationships are the data Neo4j, ArangoDB, Amazon Neptune Social networks, recommendation engines, knowledge graphs
Vector Similarity search, embeddings Pinecone, Weaviate, Qdrant, pgvector, ChromaDB AI/ML features, semantic search, RAG pipelines
Time-Series Timestamped data, metrics InfluxDB, TimescaleDB, QuestDB Analytics, monitoring, IoT data, usage tracking
Search Full-text search, faceted filtering Elasticsearch, Meilisearch, Algolia, Typesense Product search, log analysis, autocomplete
BaaS (Backend-as-a-Service) All-in-one (DB + Auth + Realtime) Supabase, Firebase, Appwrite, Nhost, PocketBase Rapid prototyping, or when you want auth + DB + realtime bundled

ORM / Client Options (for Relational)

ORM / Query Builder Language Notes
Prisma TypeScript Type-safe, schema-first, great DX, generates migrations
Drizzle TypeScript Lightweight, SQL-like syntax, no code generation
TypeORM TypeScript Decorator-based, supports many databases
Knex JavaScript/TS Query builder (not full ORM), very flexible
Sequelize JavaScript/TS Mature, feature-rich, heavier
SQLAlchemy Python The standard for Python backends
Django ORM Python Batteries-included with Django

Common Combinations

Use Case Primary DB Cache Search Vector
Simple SaaS PostgreSQL Redis
AI-powered app PostgreSQL Redis pgvector or Pinecone
Content platform MongoDB Redis Meilisearch
Analytics-heavy PostgreSQL Redis — + TimescaleDB
Rapid prototype Supabase (Postgres) built-in
Social / graph data PostgreSQL Redis Elasticsearch Neo4j

Local vs. Managed

Approach Pros Cons Examples
Self-hosted Full control, no vendor lock-in, cheaper at scale You manage backups, scaling, security PostgreSQL on a VPS, Redis on Docker
Managed cloud Zero-ops, automatic backups, scaling Cost, vendor lock-in Supabase, PlanetScale, Neon, Railway, AWS RDS, MongoDB Atlas
Embedded / Local No network, instant, great for desktop Single-machine, no cloud sync SQLite, LevelDB, electron-store

Authentication Providers

Options Landscape

Provider Type Best For Notes
Clerk Managed Polished UI, fast integration Pre-built components, expensive at scale
Auth.js (NextAuth) Self-hosted Next.js apps Free, flexible, many adapters, you manage the session store
Supabase Auth Managed Already using Supabase Bundled with Supabase, Row Level Security integration
Firebase Auth Managed Mobile + web Google ecosystem, generous free tier
Lucia Self-hosted library Full control Lightweight, bring your own DB, no vendor lock-in
Keycloak Self-hosted Enterprise, SSO/SAML Heavy but powerful, open source
Auth0 / Okta Managed Enterprise, many integrations Mature, expensive, feature-rich
Passport.js Library Custom flows Just a middleware — you build everything around it
Custom JWT DIY Full control Most work, most flexibility, highest risk of mistakes
WorkOS Managed B2B / enterprise SSO SAML/SCIM out of the box
Hanko Managed / Self-hosted Passkeys Passwordless-first, modern

Choosing

Scenario Recommended
Shipping fast, budget exists Clerk or Supabase Auth
Next.js app, cost-sensitive Auth.js (NextAuth)
Full control, no vendor lock-in Lucia or custom JWT
Enterprise / SSO requirement Keycloak, Auth0, or WorkOS
Already on Firebase Firebase Auth
Desktop + web, same auth Any provider with JWT — verify tokens on your backend

Generic Auth Middleware Pattern

Regardless of which provider you choose, the backend middleware follows the same shape:

// backend/middleware/auth.ts

// The provider-specific part: how you verify a token
// - Clerk: clerk.sessions.verifySession(token)
// - Supabase: supabase.auth.getUser(token)
// - Firebase: admin.auth().verifyIdToken(token)
// - Auth.js: decode JWT with your AUTH_SECRET
// - Custom JWT: jwt.verify(token, SECRET)
async function verifyToken(token: string): Promise<DecodedUser | null> {
  // Your provider-specific verification here
}

export async function requireAuth(req: Request, res: Response, next: NextFunction) {
  const authHeader = req.headers.authorization;

  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'No token provided' });
  }

  const token = authHeader.slice(7);

  try {
    const user = await verifyToken(token);
    if (!user) {
      return res.status(401).json({ error: 'Invalid token' });
    }
    req.user = user;
    next();
  } catch (error) {
    return res.status(401).json({ error: 'Authentication failed' });
  }
}

export function requireAdmin(req: Request, res: Response, next: NextFunction) {
  requireAuth(req, res, () => {
    if (req.user.role !== 'admin') {
      return res.status(403).json({ error: 'Admin access required' });
    }
    next();
  });
}

Payment Systems

Options Landscape

Provider Type Best For Fees (approx.)
Stripe Full platform SaaS, subscriptions, one-time payments 2.9% + $0.30/tx
LemonSqueezy Merchant of Record Indie devs, digital products, handles tax 5% + $0.50/tx
Paddle Merchant of Record SaaS, global tax compliance 5% + $0.50/tx
Gumroad Marketplace Creators, digital products 10%
PayPal Payment processor International, buyer trust 2.9% + $0.30/tx
Square Payment processor In-person + online 2.6% + $0.10/tx
Braintree Payment processor PayPal-owned, flexible 2.59% + $0.49/tx
Razorpay Payment processor India-focused 2% per tx
Mollie Payment processor Europe-focused varies by method
Coinbase Commerce Crypto Accepting cryptocurrency 1%
Open-source (Kill Bill, Lago) Self-hosted billing Full control, complex billing Free (you host)

Merchant of Record vs. Payment Processor

Aspect Payment Processor (Stripe, PayPal) Merchant of Record (LemonSqueezy, Paddle)
Tax handling You calculate and file taxes They handle all sales tax/VAT globally
Legal entity You are the seller They are the seller (reselling for you)
Refunds You handle disputes They handle disputes
Payout Direct to your bank They pay you (minus fees)
Control Full control Less control, simpler
Best for Established business, custom flows Solo devs, indie products, "just works"

Choosing

Scenario Recommended
SaaS with subscriptions Stripe
Digital product, don't want to deal with tax LemonSqueezy or Paddle
Need PayPal as an option Stripe + PayPal, or Braintree
Selling to India Razorpay
Selling to Europe Mollie or Stripe
Open-source / self-hosted billing Kill Bill or Lago

Generic Payment Flow

The pattern is the same regardless of provider:

// 1. Client requests a checkout session
// POST /api/create-checkout
router.post('/create-checkout', requireAuth, async (req, res) => {
  const { packageId } = req.body;
  const userId = req.user.id;
  const pkg = CREDIT_PACKAGES[packageId];

  // Create checkout session with your provider:
  // - Stripe: stripe.checkout.sessions.create(...)
  // - LemonSqueezy: lemonsqueezy.createCheckout(...)
  // - Paddle: paddle.transactions.create(...)
  const session = await paymentProvider.createCheckout({
    amount: pkg.price,
    currency: 'usd',
    metadata: { userId, packageId, credits: pkg.credits },
    successUrl: `${WEBSITE_URL}/purchase/success`,
    cancelUrl: `${WEBSITE_URL}/purchase/cancelled`,
  });

  // Record pending purchase in your database
  await db.purchases.create({
    userId,
    amount: pkg.price,
    credits: pkg.credits,
    providerSessionId: session.id,
    status: 'pending',
  });

  res.json({ checkoutUrl: session.url });
});

// 2. Provider sends a webhook when payment completes
// POST /webhooks/payment
router.post('/webhooks/payment', async (req, res) => {
  // Verify webhook signature (CRITICAL — never skip this)
  const event = verifyWebhookSignature(req);

  if (event.type === 'payment.completed') {
    const { userId, credits } = event.metadata;

    await db.transaction(async (tx) => {
      // Mark purchase as completed
      await tx.purchases.update({
        where: { providerSessionId: event.sessionId },
        data: { status: 'completed' },
      });

      // Add credits to user
      await tx.users.incrementCredits(userId, parseInt(credits));
    });

    // Notify connected desktop app via WebSocket
    await notifyUser(userId, { type: 'credits_added', credits: parseInt(credits) });
  }

  if (event.type === 'payment.refunded') {
    const { userId, credits } = event.metadata;

    await db.transaction(async (tx) => {
      await tx.purchases.update({
        where: { providerSessionId: event.sessionId },
        data: { status: 'refunded' },
      });

      // Remove credits (clamp to zero)
      const user = await tx.users.findById(userId);
      const newBalance = Math.max(0, user.credits - parseInt(credits));
      await tx.users.setCredits(userId, newBalance);
    });

    await notifyUser(userId, { type: 'credits_removed', reason: 'refund' });
  }

  res.json({ received: true });
});

Webhooks (Local and Online)

Webhooks are HTTP callbacks — when something happens in one system, it sends a POST request to your server.

How Webhooks Work

Common Webhook Sources

Source Events You Receive What You Do With Them
Stripe / Paddle / LemonSqueezy payment.completed, subscription.updated, charge.refunded Update credits, change plan, revoke access
Clerk / Auth0 user.created, user.deleted, session.ended Create DB record, clean up data, audit log
GitHub push, pull_request, release Trigger builds, notify team
SendGrid / Resend email.delivered, email.bounced Update delivery status, handle bounces
Twilio message.received, call.completed Process inbound messages
Your own services Custom events Decouple internal microservices

Webhook Security

Always verify webhook signatures. Every serious provider signs their payloads:

// Generic webhook handler pattern
router.post('/webhooks/:provider', express.raw({ type: 'application/json' }), async (req, res) => {
  const provider = req.params.provider;
  const signature = req.headers['x-signature'] ||
                    req.headers['stripe-signature'] ||
                    req.headers['x-webhook-signature'];

  try {
    // Each provider has its own verification method:
    // - Stripe: stripe.webhooks.constructEvent(body, sig, secret)
    // - Clerk: svix.verify(body, headers)
    // - LemonSqueezy: verify HMAC-SHA256
    // - Generic: crypto.timingSafeEqual(computedHmac, signature)
    const event = await verifyWebhook(provider, req.body, signature);

    // Process the event
    await processWebhookEvent(provider, event);

    res.json({ received: true });
  } catch (error) {
    console.error(`Webhook verification failed for ${provider}:`, error);
    res.status(400).json({ error: 'Invalid signature' });
  }
});

Local Development with Webhooks

External services can not reach localhost. Solutions:

Tool How It Works Cost
Stripe CLI stripe listen --forward-to localhost:3001/webhooks/stripe Free
ngrok Creates a public tunnel to your localhost Free tier available
Cloudflare Tunnel cloudflared tunnel to expose local services Free
localtunnel lt --port 3001 for quick testing Free
Webhook.site Inspect payloads in browser, no code needed Free
smee.io GitHub's webhook proxy for development Free

Internal Webhooks (Service-to-Service)

For your own microservices, you can use the same pattern or alternatives:

Approach Best For Notes
HTTP webhooks Simple, decoupled services Same as external webhooks, just between your own services
Message queues (RabbitMQ, SQS, BullMQ) Reliable delivery, retry logic Better for high-volume or critical events
Event streaming (Kafka, Redis Streams) Real-time, ordered events, multiple consumers More infrastructure, more power
Pub/Sub (Redis, Google Pub/Sub, SNS) Fan-out to many subscribers Simple, fire-and-forget
Database triggers React to data changes Supabase Realtime, Postgres LISTEN/NOTIFY

Environment Variables Overview

Each app needs different secrets. Never share backend secrets with frontend apps.

Variable Backend Website (Client) Desktop App Dashboard
AUTH_SECRET / AUTH_SECRET_KEY Yes No No No
AUTH_PUBLIC_KEY (e.g. NEXT_PUBLIC_CLERK_KEY) No Yes Yes Yes
AUTH_JWKS_URL Yes No No No
PAYMENT_SECRET_KEY (Stripe, Paddle, etc.) Yes No No No
PAYMENT_PUBLIC_KEY No Yes No No
PAYMENT_WEBHOOK_SECRET Yes No No No
DATABASE_URL Yes No No No
REDIS_URL Yes No No No
AI_API_KEY (OpenAI, Anthropic, etc.) Yes No No No
API_URL (your backend URL) No Yes Yes Yes
WEBSOCKET_URL No No Yes Optional
JWT_SECRET (if custom auth) Yes No No No
ADMIN_SECRET Yes No No Yes

Rules:

  • Anything prefixed NEXT_PUBLIC_ is bundled into the client — never put secrets there
  • Desktop apps ship to users' machines — never embed secrets in Electron code
  • The backend is the only place that should hold secret keys
  • Use .env files locally, environment variables in production

User Authentication Flow

The Complete Flow

Backend - Auth Endpoint

// backend/routes/auth.ts

// Works with any auth provider — just swap verifyToken()
router.post('/auth/verify', async (req, res) => {
  const { token } = req.body;

  try {
    const providerUser = await verifyToken(token);

    // Get or create user in your database
    let user = await db.users.findByProviderId(providerUser.id);

    if (!user) {
      user = await db.users.create({
        providerId: providerUser.id,
        email: providerUser.email,
        credits: 100, // Free starter credits
        plan: 'free',
      });
    }

    res.json({
      user: {
        id: user.id,
        email: user.email,
        credits: user.credits,
        plan: user.plan,
      },
    });
  } catch (error) {
    res.status(401).json({ error: 'Invalid token' });
  }
});

Desktop App - Auth Flow

// electron/auth.ts
import { BrowserWindow } from 'electron';

export async function signIn(): Promise<AuthResult> {
  return new Promise((resolve, reject) => {
    const authWindow = new BrowserWindow({
      width: 500,
      height: 700,
      webPreferences: {
        nodeIntegration: false,
        contextIsolation: true,
      },
    });

    // Load your auth provider's sign-in page
    // Redirect back to a custom protocol: yourapp://auth-callback
    const authUrl = buildAuthUrl({
      redirectUri: 'yourapp://auth-callback',
    });
    authWindow.loadURL(authUrl);

    // Listen for redirect with token
    authWindow.webContents.on('will-redirect', async (event, url) => {
      if (url.startsWith('yourapp://auth-callback')) {
        const token = new URL(url).searchParams.get('token');
        authWindow.close();

        if (token) {
          const response = await fetch(`${API_URL}/auth/verify`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ token }),
          });
          resolve(await response.json());
        } else {
          reject(new Error('No token received'));
        }
      }
    });
  });
}

Store Token Securely

// electron/secure-store.ts
import { safeStorage } from 'electron';
import Store from 'electron-store';

const store = new Store();

// NOTE: safeStorage uses the OS keychain (macOS Keychain, Windows DPAPI, Linux libsecret)
// This runs in the Electron MAIN process only

export function saveToken(token: string) {
  if (safeStorage.isEncryptionAvailable()) {
    const encrypted = safeStorage.encryptString(token);
    store.set('auth.token', encrypted.toString('base64'));
  } else {
    store.set('auth.token', token);
  }
}

export function getToken(): string | null {
  const stored = store.get('auth.token') as string;
  if (!stored) return null;

  if (safeStorage.isEncryptionAvailable()) {
    const buffer = Buffer.from(stored, 'base64');
    return safeStorage.decryptString(buffer);
  }
  return stored;
}

AI Credits System

How Credits Work

Database Schema (Generic)

Regardless of your ORM or database, you need these tables/collections:

Users
  id            : unique identifier
  providerId    : auth provider's user ID
  email         : string, unique
  credits       : integer, default 0
  plan          : string (free, pro, enterprise)
  createdAt     : timestamp
  updatedAt     : timestamp

Purchases
  id            : unique identifier
  userId        : foreign key -> Users
  amount        : integer (cents)
  credits       : integer (credits purchased)
  providerTxId  : string, unique (Stripe session ID, Paddle tx ID, etc.)
  status        : string (pending, completed, refunded)
  createdAt     : timestamp

UsageLog
  id            : unique identifier
  userId        : foreign key -> Users
  creditsUsed   : integer
  feature       : string (text-generation, image-gen, etc.)
  metadata      : JSON (optional — prompt length, model used, etc.)
  createdAt     : timestamp

Credit Packages

// backend/config/credits.ts
export const CREDIT_PACKAGES = {
  starter: {
    id: 'starter',
    name: 'Starter Pack',
    credits: 500,
    price: 999, // $9.99 in cents
  },
  popular: {
    id: 'popular',
    name: 'Popular Pack',
    credits: 2000,
    price: 2999,
    savings: '25%',
  },
  pro: {
    id: 'pro',
    name: 'Pro Pack',
    credits: 5000,
    price: 5999,
    savings: '40%',
  },
};

export const FEATURE_COSTS: Record<string, number> = {
  'text-generation': 1,
  'text-generation-long': 5,
  'image-generation': 10,
  'code-assistance': 2,
  'grammar-check': 1,
};

Using Credits in Desktop App

// desktop-app/services/ai.ts
export async function generateText(prompt: string): Promise<GenerationResult> {
  const response = await fetch(`${API_URL}/ai/generate`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${getToken()}`,
    },
    body: JSON.stringify({ prompt, feature: 'text-generation' }),
  });

  if (response.status === 402) {
    throw new InsufficientCreditsError('Not enough credits');
  }

  return response.json();
}

Backend - AI Generation with Credit Check

// backend/routes/ai.ts
router.post('/ai/generate', requireAuth, async (req, res) => {
  const { prompt, feature } = req.body;
  const userId = req.user.id;
  const cost = FEATURE_COSTS[feature] || 1;

  // Check credits
  const user = await db.users.findById(userId);

  if (!user || user.credits < cost) {
    return res.status(402).json({
      error: 'Insufficient credits',
      required: cost,
      available: user?.credits || 0,
    });
  }

  // Deduct credits BEFORE calling AI (prevents race conditions)
  await db.transaction(async (tx) => {
    await tx.users.decrementCredits(userId, cost);
    await tx.usageLogs.create({
      userId,
      creditsUsed: cost,
      feature,
      metadata: { promptLength: prompt.length },
    });
  });

  try {
    // Call AI provider (OpenAI, Anthropic, local model, etc.)
    const result = await callAIProvider(prompt);

    res.json({
      result,
      creditsUsed: cost,
      creditsRemaining: user.credits - cost,
    });
  } catch (error) {
    // Refund credits if AI call fails
    await db.users.incrementCredits(userId, cost);
    throw error;
  }
});

Desktop App <-> Backend Communication

API Client Setup

// desktop-app/services/api.ts
import axios from 'axios';
import { getToken, clearToken } from './secure-store';

const api = axios.create({
  baseURL: process.env.API_URL || 'https://api.yourapp.com',
  timeout: 30000,
});

// Add auth token to all requests
api.interceptors.request.use((config) => {
  const token = getToken();
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// Handle auth errors globally
api.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      clearToken();
      // NOTE: In Electron, this runs in the renderer process.
      // To communicate with the main process, use ipcRenderer.send('auth:logout')
      window.dispatchEvent(new CustomEvent('auth:logout'));
    }
    return Promise.reject(error);
  }
);

export default api;

Syncing User Data

// desktop-app/hooks/useUserSync.ts
import { useQuery, useEffect } from 'your-framework';

export function useUserSync() {
  const { data, error } = useQuery({
    queryKey: ['user', 'sync'],
    queryFn: async () => {
      const response = await api.get('/user/me');
      return response.data;
    },
    refetchInterval: 5 * 60 * 1000, // Every 5 minutes
    refetchOnWindowFocus: true,
  });

  // Update local store when data changes
  // NOTE: If using TanStack Query v5+, use useEffect instead of onSuccess
  useEffect(() => {
    if (data) {
      useUserStore.getState().setUser(data);
    }
  }, [data]);

  return { data, error };
}

Desktop App State

// desktop-app/stores/userStore.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface User {
  id: string;
  email: string;
  credits: number;
  plan: string;
}

interface UserState {
  user: User | null;
  isAuthenticated: boolean;
  setUser: (user: User) => void;
  updateCredits: (credits: number) => void;
  logout: () => void;
}

export const useUserStore = create<UserState>()(
  persist(
    (set) => ({
      user: null,
      isAuthenticated: false,
      setUser: (user) => set({ user, isAuthenticated: true }),
      updateCredits: (credits) =>
        set((state) => ({
          user: state.user ? { ...state.user, credits } : null,
        })),
      logout: () => set({ user: null, isAuthenticated: false }),
    }),
    { name: 'user-store' }
  )
);

Website (Download Page)

Components

Download Logic

// website/components/DownloadButton.tsx
import { useEffect, useState } from 'react';

type Platform = 'windows' | 'mac' | 'linux' | 'unknown';

function detectPlatform(): Platform {
  if (typeof window === 'undefined') return 'unknown';
  const ua = navigator.userAgent.toLowerCase();
  if (ua.includes('win')) return 'windows';
  if (ua.includes('mac')) return 'mac';
  if (ua.includes('linux')) return 'linux';
  return 'unknown';
}

const DOWNLOAD_URLS = {
  windows: 'https://releases.yourapp.com/latest/YourApp-Setup.exe',
  mac: 'https://releases.yourapp.com/latest/YourApp.dmg',
  linux: 'https://releases.yourapp.com/latest/YourApp.AppImage',
};

function DownloadButton() {
  const [platform, setPlatform] = useState<Platform>('unknown');

  useEffect(() => {
    setPlatform(detectPlatform());
  }, []);

  const handleDownload = () => {
    // Track download event
    fetch('/api/analytics/track', {
      method: 'POST',
      body: JSON.stringify({ event: 'download_started', platform }),
    });
    window.location.href = DOWNLOAD_URLS[platform] || DOWNLOAD_URLS.windows;
  };

  return (
    <div className="download-section">
      <button className="download-btn primary" onClick={handleDownload}>
        Download for {platform.charAt(0).toUpperCase() + platform.slice(1)}
      </button>

      <details className="other-platforms">
        <summary>Other platforms</summary>
        <a href={DOWNLOAD_URLS.windows}>Windows (.exe)</a>
        <a href={DOWNLOAD_URLS.mac}>macOS (.dmg)</a>
        <a href={DOWNLOAD_URLS.linux}>Linux (.AppImage)</a>
      </details>
    </div>
  );
}

User Portal

// website/pages/dashboard.tsx
function UserDashboard() {
  const user = useAuth(); // Your auth provider's hook

  const { data: userData } = useQuery({
    queryKey: ['user', 'dashboard'],
    queryFn: async () => {
      const res = await fetch('/api/user/dashboard');
      return res.json();
    },
  });

  return (
    <div className="dashboard">
      <h1>Welcome, {user?.firstName}</h1>

      <div className="credits-card">
        <h2>Your Credits</h2>
        <p className="credits-balance">{userData?.credits || 0}</p>
        <a href="/purchase" className="btn">Buy More Credits</a>
      </div>

      <div className="usage-history">
        <h2>Recent Usage</h2>
        <table>
          <thead>
            <tr>
              <th>Date</th>
              <th>Feature</th>
              <th>Credits Used</th>
            </tr>
          </thead>
          <tbody>
            {userData?.recentUsage?.map((log: any) => (
              <tr key={log.id}>
                <td>{new Date(log.createdAt).toLocaleDateString()}</td>
                <td>{log.feature}</td>
                <td>{log.creditsUsed}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </div>
  );
}

Owner Dashboard

Dashboard Architecture

Dashboard API Endpoints

// backend/routes/admin.ts

// Get dashboard overview
router.get('/admin/overview', requireAdmin, async (req, res) => {
  const [totalUsers, activeToday, revenueMTD, creditsPurchased] = await Promise.all([
    db.users.count(),
    db.users.countWhere({ lastActiveAt: { gte: oneDayAgo() } }),
    db.purchases.sumWhere({ status: 'completed', createdAt: { gte: firstOfMonth() } }, 'amount'),
    db.purchases.sumWhere({ status: 'completed', createdAt: { gte: firstOfMonth() } }, 'credits'),
  ]);

  res.json({ totalUsers, activeToday, revenueMTD, creditsPurchased });
});

// Get all users with pagination
router.get('/admin/users', requireAdmin, async (req, res) => {
  const { page = 1, limit = 20, search } = req.query;

  const where = search
    ? { or: [{ email: { contains: search } }, { id: { contains: search } }] }
    : {};

  const [users, total] = await Promise.all([
    db.users.findMany({
      where,
      skip: (Number(page) - 1) * Number(limit),
      take: Number(limit),
      orderBy: { createdAt: 'desc' },
    }),
    db.users.count({ where }),
  ]);

  res.json({
    users,
    pagination: {
      page: Number(page),
      limit: Number(limit),
      total,
      totalPages: Math.ceil(total / Number(limit)),
    },
  });
});

// Get single user details
router.get('/admin/users/:id', requireAdmin, async (req, res) => {
  const user = await db.users.findById(req.params.id, {
    include: { purchases: { limit: 10 }, usageHistory: { limit: 50 } },
  });

  if (!user) return res.status(404).json({ error: 'User not found' });
  res.json(user);
});

// Give credits to user (admin action)
router.post('/admin/users/:id/credits', requireAdmin, async (req, res) => {
  const { credits, reason } = req.body;

  const user = await db.users.incrementCredits(req.params.id, credits);

  // Audit log
  await db.adminLogs.create({
    adminId: req.user.id,
    action: 'give_credits',
    targetUserId: req.params.id,
    metadata: { credits, reason },
  });

  // Notify user's desktop app
  await notifyUser(req.params.id, {
    type: 'credits_received',
    credits,
    message: reason || 'Credits added by admin',
  });

  res.json({ success: true, newBalance: user.credits });
});

Real-time Sync

WebSocket Connection

WebSocket Server

// backend/websocket.ts
import { WebSocketServer, WebSocket } from 'ws';

const wss = new WebSocketServer({ port: 8080 });

// Map of userId -> active WebSocket connections
const userConnections = new Map<string, Set<WebSocket>>();

wss.on('connection', async (ws, req) => {
  // SECURITY NOTE: Passing tokens in query strings (wss://...?token=X) is common
  // but leaks tokens in server logs and proxy logs. Alternatives:
  // 1. Send token as the first message after connection opens
  // 2. Use a short-lived ticket: client gets a ticket via REST, passes ticket in URL
  // 3. Use the Sec-WebSocket-Protocol header
  //
  // For simplicity, this example uses query string auth:
  const url = new URL(req.url!, `http://${req.headers.host}`);
  const token = url.searchParams.get('token');

  if (!token) {
    ws.close(4001, 'No token provided');
    return;
  }

  try {
    const decoded = await verifyToken(token);
    const userId = decoded.userId;

    if (!userConnections.has(userId)) {
      userConnections.set(userId, new Set());
    }
    userConnections.get(userId)!.add(ws);

    ws.on('close', () => {
      userConnections.get(userId)?.delete(ws);
      if (userConnections.get(userId)?.size === 0) {
        userConnections.delete(userId);
      }
    });

    ws.send(JSON.stringify({ type: 'connected', userId }));
  } catch (error) {
    ws.close(4002, 'Invalid token');
  }
});

// Send a message to all of a user's connected clients
export function notifyUser(userId: string, message: object) {
  const connections = userConnections.get(userId);
  if (connections) {
    const payload = JSON.stringify(message);
    connections.forEach((ws) => {
      if (ws.readyState === WebSocket.OPEN) {
        ws.send(payload);
      }
    });
  }
}

Desktop App WebSocket Client

// desktop-app/services/websocket.ts
import { useUserStore } from '../stores/userStore';

class WebSocketClient {
  private ws: WebSocket | null = null;
  private reconnectAttempts = 0;
  private maxReconnectAttempts = 5;

  connect(token: string) {
    this.ws = new WebSocket(`wss://api.yourapp.com/ws?token=${token}`);

    this.ws.onopen = () => {
      this.reconnectAttempts = 0;
    };

    this.ws.onmessage = (event) => {
      const message = JSON.parse(event.data);
      this.handleMessage(message);
    };

    this.ws.onclose = () => {
      this.scheduleReconnect();
    };
  }

  private handleMessage(message: any) {
    switch (message.type) {
      case 'credits_added':
      case 'credits_received':
        useUserStore.getState().updateCredits(message.newBalance);
        new Notification('Credits Added!', {
          body: `You received ${message.credits} credits`,
        });
        break;

      case 'credits_removed':
        useUserStore.getState().updateCredits(message.newBalance);
        break;

      case 'plan_updated':
        useUserStore.getState().setUser({
          ...useUserStore.getState().user!,
          plan: message.plan,
        });
        break;

      case 'force_logout':
        useUserStore.getState().logout();
        break;
    }
  }

  private scheduleReconnect() {
    if (this.reconnectAttempts >= this.maxReconnectAttempts) return;

    // Exponential backoff: 1s, 2s, 4s, 8s, 16s
    const delay = Math.pow(2, this.reconnectAttempts) * 1000;
    this.reconnectAttempts++;

    setTimeout(() => {
      const token = getToken();
      if (token) this.connect(token);
    }, delay);
  }

  disconnect() {
    this.ws?.close();
    this.ws = null;
  }
}

export const wsClient = new WebSocketClient();

Real-time Alternatives to WebSockets

Technology Best For Notes
WebSockets (ws, Socket.IO) Bidirectional real-time Most flexible, most work
Server-Sent Events (SSE) Server-to-client only Simpler, works over HTTP, auto-reconnect
Supabase Realtime Already using Supabase Subscribe to database changes directly
Firebase Realtime DB Already using Firebase Automatic sync, offline support built in
Pusher / Ably Managed real-time No infrastructure to manage, paid
Long polling Fallback when WS blocked Works everywhere, higher latency

Offline Support

Handling Offline Mode in Desktop App

Offline Queue

// desktop-app/services/offlineQueue.ts

// Storage options for Electron:
// - electron-store: Simple key-value, main process (recommended for small queues)
// - IndexedDB via Dexie: Renderer process, structured data, larger capacity
// - SQLite (better-sqlite3): Main process, full SQL, best for complex queries
// - Filesystem: Main process, raw JSON files
//
// This example uses a generic interface — swap the implementation:

interface QueuedAction {
  id: string;
  action: string;
  payload: any;
  createdAt: Date;
  retries: number;
}

// Generic interface — implement with your chosen storage
interface QueueStorage {
  add(action: QueuedAction): Promise<void>;
  getAll(): Promise<QueuedAction[]>;
  remove(id: string): Promise<void>;
  updateRetries(id: string, retries: number): Promise<void>;
}

export class OfflineQueue {
  constructor(private storage: QueueStorage) {}

  async enqueue(action: string, payload: any) {
    await this.storage.add({
      id: crypto.randomUUID(),
      action,
      payload,
      createdAt: new Date(),
      retries: 0,
    });
  }

  async processQueue() {
    const actions = await this.storage.getAll();

    for (const action of actions) {
      try {
        await executeAction(action);
        await this.storage.remove(action.id);
      } catch (error) {
        if (action.retries >= 3) {
          await this.storage.remove(action.id);
        } else {
          await this.storage.updateRetries(action.id, action.retries + 1);
        }
      }
    }
  }
}

// Listen for online status (renderer process)
window.addEventListener('online', () => {
  offlineQueue.processQueue();
});

Offline-aware API Calls

// desktop-app/services/api.ts
export async function makeRequest(endpoint: string, options: RequestInit) {
  if (!navigator.onLine) {
    await offlineQueue.enqueue('api_request', { endpoint, options });
    throw new OfflineError('You are offline. Request queued.');
  }

  return fetch(`${API_URL}${endpoint}`, options);
}

Auto-Updates for Desktop

Electron apps should check for updates and prompt users to install them.

Update Flow

Implementation

// electron/updater.ts
import { autoUpdater } from 'electron-updater';
import { BrowserWindow } from 'electron';

export function setupAutoUpdater(mainWindow: BrowserWindow) {
  // Check on launch
  autoUpdater.checkForUpdatesAndNotify();

  // Check periodically (every 4 hours)
  setInterval(() => {
    autoUpdater.checkForUpdatesAndNotify();
  }, 4 * 60 * 60 * 1000);

  autoUpdater.on('update-available', (info) => {
    mainWindow.webContents.send('update:available', info.version);
  });

  autoUpdater.on('update-downloaded', (info) => {
    mainWindow.webContents.send('update:downloaded', info.version);
  });

  // Called from renderer when user clicks "Restart and Update"
  ipcMain.on('update:install', () => {
    autoUpdater.quitAndInstall();
  });
}

Where to Host Updates

Option How It Works Cost
GitHub Releases electron-updater reads from GitHub releases Free for public repos
S3 / R2 / GCS Upload builds to a bucket, point electron-updater at it Cheap storage costs
Hazel (Vercel) Tiny server that proxies GitHub releases Free on Vercel
update.electronjs.org Free update server for open-source Electron apps Free
Your own server Serve update files from your backend You manage it

In electron-builder.yml:

publish:
  provider: github  # or s3, generic, etc.
  owner: your-username
  repo: your-app

Error Handling Strategy

Error Propagation

When something fails, errors need to flow back to the user clearly.

Backend Error Responses

Use consistent error shapes:

// backend/middleware/errorHandler.ts
interface ApiError {
  error: string;       // Machine-readable code
  message: string;     // Human-readable message
  retryable: boolean;  // Should the client retry?
  retryAfter?: number; // Seconds to wait before retry
}

// Error categories:
// 400 — Bad request (client's fault, don't retry)
// 401 — Not authenticated (redirect to login)
// 402 — Insufficient credits (prompt purchase)
// 403 — Not authorized (wrong role)
// 429 — Rate limited (retry after delay)
// 500 — Server error (retry with backoff)
// 502/503 — Upstream down (retry with backoff)

export function errorHandler(err: any, req: Request, res: Response, next: NextFunction) {
  // AI provider errors — refund credits and tell user to retry
  if (err.source === 'ai_provider') {
    return res.status(502).json({
      error: 'ai_provider_error',
      message: 'AI service is temporarily unavailable. Your credits have been refunded.',
      retryable: true,
      retryAfter: 30,
    });
  }

  // Payment provider errors
  if (err.source === 'payment_provider') {
    return res.status(502).json({
      error: 'payment_error',
      message: 'Payment service is temporarily unavailable. Please try again.',
      retryable: true,
    });
  }

  // Default
  console.error('Unhandled error:', err);
  res.status(500).json({
    error: 'internal_error',
    message: 'Something went wrong. Please try again.',
    retryable: true,
  });
}

Desktop App Error Handling

// desktop-app/services/api.ts
api.interceptors.response.use(
  (response) => response,
  async (error) => {
    const status = error.response?.status;
    const data = error.response?.data;

    switch (status) {
      case 401:
        // Token expired — force re-login
        clearToken();
        showLoginScreen();
        break;

      case 402:
        // Insufficient credits — show purchase prompt
        showPurchasePrompt(data.required, data.available);
        break;

      case 429:
        // Rate limited — show countdown
        showRateLimitMessage(data.retryAfter);
        break;

      case 502:
      case 503:
        // Upstream down — show retry option
        if (data?.retryable) {
          showRetryPrompt(data.message, data.retryAfter);
        }
        break;

      default:
        showGenericError(data?.message || 'Something went wrong');
    }

    return Promise.reject(error);
  }
);

Deployment Topology

Where each piece runs in production:

Component Where to Deploy Examples
Backend API Server / container / serverless Railway, Fly.io, Render, AWS ECS, DigitalOcean App Platform, VPS
WebSocket Server Same as backend or separate Same server (different port) or dedicated instance
Primary Database Managed database service Supabase, Neon, PlanetScale, AWS RDS, Railway Postgres, MongoDB Atlas
Cache (Redis) Managed or sidecar Upstash, Railway Redis, AWS ElastiCache, self-hosted
Website Static hosting or SSR platform Vercel, Netlify, Cloudflare Pages, AWS Amplify
Admin Dashboard Same as website or separate Same platform, different subdomain (admin.yourapp.com)
Desktop App Distributed to users GitHub Releases, S3/R2, your own CDN
File Storage Object storage S3, R2, GCS, Supabase Storage
Monitoring Observability platform Sentry, Datadog, Grafana Cloud, BetterStack
Email Transactional email service Resend, SendGrid, Postmark, AWS SES

Example Production Setup (Budget-Friendly)

Backend API        -> Railway ($5/mo)
PostgreSQL         -> Neon (free tier) or Supabase (free tier)
Redis              -> Upstash (free tier)
Website            -> Vercel (free tier)
Dashboard          -> Vercel (same project, /admin route)
Desktop Releases   -> GitHub Releases (free)
Auth               -> Clerk (free tier) or Auth.js (free)
Payments           -> Stripe (pay per transaction)
Email              -> Resend (free tier)
Monitoring         -> Sentry (free tier)

Example Production Setup (Scale)

Backend API        -> AWS ECS or Fly.io (auto-scaling)
PostgreSQL         -> AWS RDS or Supabase Pro
Redis              -> AWS ElastiCache or Upstash Pro
Website            -> Vercel Pro
Dashboard          -> Separate Vercel project
Desktop Releases   -> S3 + CloudFront CDN
Auth               -> Clerk Pro or self-hosted Keycloak
Payments           -> Stripe
Email              -> SendGrid or AWS SES
Monitoring         -> Datadog or Grafana Cloud

Security Considerations

Security Checklist

Rate Limiting

// backend/middleware/rateLimit.ts
import rateLimit from 'express-rate-limit';

// General API rate limit
export const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100,
  message: { error: 'Too many requests, please try again later' },
});

// Strict limit for auth endpoints
export const authLimiter = rateLimit({
  windowMs: 60 * 60 * 1000, // 1 hour
  max: 10,
  message: { error: 'Too many login attempts' },
});

// AI endpoint limit
export const aiLimiter = rateLimit({
  windowMs: 60 * 1000, // 1 minute
  max: 20,
  message: { error: 'Rate limit exceeded' },
});

Security Best Practices

Area Do Don't
Tokens Short expiry (15min-24h), refresh rotation Long-lived tokens, tokens in localStorage on web
Secrets Backend-only, env vars, secret managers Embed in desktop app code, commit to git
Webhooks Always verify signatures Trust payload without verification
CORS Whitelist specific origins allow_origins: ["*"] in production
Input Validate and sanitize all user input Trust client-side validation alone
Admin Separate auth, IP allowlisting, audit logs Same auth as regular users
Desktop safeStorage for tokens, code signing Plain text token storage
Database Parameterized queries, least-privilege accounts String concatenation in queries
HTTPS Enforce everywhere, HSTS headers Allow HTTP in production

Implementation Checklist

Phase 1: Foundation

  • Set up backend API server (Express, Fastify, or similar)
  • Choose and configure database
  • Choose and integrate auth provider
  • Set up environment variables across all apps
  • Deploy backend to a hosting platform

Phase 2: Desktop App Integration

  • Implement auth flow in Electron (open auth window, receive token)
  • Set up secure token storage (safeStorage)
  • Create API client with auth headers and error handling
  • Add credit checking before AI calls
  • Implement user state with Zustand (persisted)

Phase 3: Website

  • Build landing page with download button (platform detection)
  • Create user portal (dashboard, usage history)
  • Add credit purchase flow with chosen payment provider
  • Handle payment webhooks

Phase 4: Admin Dashboard

  • Build admin authentication (separate or role-based)
  • Create user management pages (list, detail, search)
  • Add overview page (users, revenue, credits)
  • Implement credit gifting and plan management
  • Add audit logging for admin actions

Phase 5: Real-time Features

  • Set up WebSocket server (or SSE, or managed service)
  • Connect desktop app to WebSocket with reconnection
  • Implement real-time credit updates and notifications
  • Test: purchase on website -> desktop app updates instantly

Phase 6: Polish

  • Add offline support with action queue
  • Set up auto-updates for desktop app (electron-updater)
  • Implement rate limiting on all endpoints
  • Set up error monitoring (Sentry or similar)
  • Security audit (webhook signatures, CORS, token handling)
  • Load testing on critical paths (auth, AI, payments)

Summary

+------------------------------------------------------------------+
|                                                                  |
|   Desktop App            Website              Dashboard          |
|       |                     |                     |              |
|       +-----REST/WS---------+-------REST----------+              |
|                              |                                   |
|                       Central Backend                            |
|                              |                                   |
|              +---------------+---------------+                   |
|              |               |               |                   |
|         Auth Provider   Payment Provider   AI Provider           |
|              |               |               |                   |
|              +-------+-------+               |                   |
|                      |                       |                   |
|               Database + Cache               |                   |
+------------------------------------------------------------------+

Key Takeaways:

  1. One Backend serves all three apps (desktop, website, dashboard)
  2. Auth tokens are shared across platforms — verify on the backend, trust no client
  3. Credits are stored centrally and checked on every AI request (deduct before calling, refund on failure)
  4. Webhooks notify your system of payment and auth events — always verify signatures
  5. WebSockets (or SSE, or a managed service) keep the desktop app in sync with purchases
  6. Admin dashboard gives you full control over users, credits, and revenue
  7. Everything is swappable — the patterns stay the same regardless of which database, auth provider, or payment system you choose