Font Manager: Cross-Platform Desktop App for Font Subscription

Software Engineer
React, TypeScript, Python, PyWebView, SQLite, Vite, Tailwindcss
Available only to partner companies

(opens in new window)Behind The Project

justfont’s business model evolved to serve two distinct markets. For individual consumers (B2C), the traditional purchase model remains: buy a font file outright, own it permanently. But for enterprise clients (B2B), justfont shifted to a rental subscription model.

This shift wasn’t arbitrary. Enterprise font licensing follows predictable patterns: large companies purchase fonts for entire design teams, procurement cycles are stable and recurring, and subscription-based pricing has become the industry standard for business software. More importantly, rental subscriptions create predictable recurring revenue, making the business sustainable and scalable. Instead of one-time purchases with unpredictable cash flow, justfont can rely on steady monthly income from enterprise clients.

But rental fundamentally changes the technical requirements. Fonts need to be installed when a subscription is active and removed seamlessly when it expires, across macOS and Windows. justfont needed a desktop application that handles this lifecycle automatically: one-click installation, background license verification, cross-device sync, and offline tolerance while running silently and working reliably for non-technical users.

Font Manager Main

(opens in new window)Solution & Approach

I designed and built justfont Manager, a cross-platform desktop application that manages font installation, licensing, and device synchronization for justfont’s font rental subscribers. The app runs as a lightweight background process with a system tray icon (Windows) or menu bar item (macOS), providing one-click font installation and automatic license verification.

The architecture uses a React frontend for the UI and a Python backend for all system-level operations. The backend handles font registration, file management, credential storage, and platform-specific API calls. PyWebView bridges the two layers, exposing Python functions as a JavaScript API that the React frontend calls directly. This hybrid approach gives us the rich UI capabilities of a modern web framework with the deep OS (Operating System) integration that font management requires.

The frontend is a single-page application built with TypeScript and Tailwind CSS. It uses TanStack Query for server state management and supports both Traditional Chinese and English. The backend handles font installation through platform-specific system APIs, stores licensing data in a local SQLite database, and secures authentication tokens using the OS credential store (Keychain on macOS, Credential Vault on Windows).

I also built the complete build and distribution pipeline: Cython compilation for code protection, platform-specific packaging (py2app for macOS, PyInstaller for Windows), code signing, macOS notarization, and installer generation.

Font Manager Arch

(opens in new window)Technical Challenges

(opens in new window)Cross-Platform Font Registration

Installing a font on a user’s system is not as simple as copying a file. Each operating system has its own font registration API, and they behave very differently. On macOS, fonts are registered through CoreText CTFontManagerRegisterFontsForURL with session-scoped activation. The font becomes available to all applications immediately but does not persist across reboots unless re-registered.

from CoreText import CTFontManagerRegisterFontsForURL
 
class MacOSFontSystem(FontSystemBase):
    def register_font(self, file_uri: str, font_id: str) -> None:
        CTFontManagerRegisterFontsForURL(font_url, kCTFontManagerScopeSession, None)

On Windows, the same operation requires GDI32’s AddFontResourceW followed by broadcasting a WM_FONTCHANGE message to notify all running applications that a new font is available.

import win32gui
 
class WindowsFontSystem(FontSystemBase):
    def register_font(self, file_uri: str, font_id: str) -> None:
        win32gui.SendMessage(HWND_BROADCAST, WM_FONTCHANGE, 0, 0)

I designed an abstract base class defining the font management interface. This includes methods to install, uninstall, register, unregister, and list installed fonts, with platform-specific implementations for macOS and Windows. This separation means the application logic never knows which platform it is running on; it calls the same methods regardless.

The trickiest platform difference was file locking. On Windows, a registered font file is locked by the OS; you cannot delete or move it while applications are using it. Uninstallation requires unregistering the font first, waiting for the lock to release, then cleaning up the file. On macOS, no such lock exists. I built a delayed cleanup mechanism for Windows that retries file deletion with backoff, handling the cases where applications hold references longer than expected.

class FontSystemBase(ABC, LoggerMixin):
    @abstractmethod
    def register_font(self, file_uri: str, font_id: str) -> None:
        pass

(opens in new window)Offline Tolerance and License Verification

Font rental subscribers do not always have internet access. A designer working on a train, a studio with unreliable WiFi, a presentation in an offline conference room if the app immediately disables fonts when the network drops, users lose access to their work in progress. But if the app never checks licensing, the business model breaks.

I implemented an offline tolerance period of several days. When the app goes online, it verifies the subscription status against justfont’s API and caches the result locally with a timestamp. If the network becomes unavailable, the app continues to function normally using the cached authorization. After the tolerance period expires without a successful verification, the app begins a graceful uninstall. It warns the user first, then gradually uninstalls fonts that cannot be verified.

On the verification side, I added a one-minute in-memory cache to prevent redundant API calls during rapid UI interactions (like scrolling through a font list where each item triggers a license check). This reduced verification API calls by roughly 80% during typical browsing sessions while maintaining security guarantees.

const [isWithinTolerance, setIsWithinTolerance] = useState<boolean | null>(null);
const [hasHandledOffline, setHasHandledOffline] = useState(false);

(opens in new window)PyWebView Bridge Reliability

The communication layer between React and Python is the heart of the application, but PyWebView’s API bridge has a critical timing problem: the JavaScript side can try to call Python functions before the bridge is fully initialized, especially during fast application startup. A race condition here means the entire app shows a blank screen.

I solved this with a dual-detection system. The frontend waits for PyWebView’s window.pywebview.api to become available using both an event listener (for the normal case) and a polling fallback (for edge cases where the event fires before the listener is registered).

export async function ensurePyWebViewAPI(): Promise<void> {
  await waitForPywebviewApi();
}

Once the API is confirmed ready, the app initializes. This sounds simple, but getting it reliable across both platforms where PyWebView uses different rendering engines (WebKit on macOS, Edge WebView2 on Windows) required careful testing of every startup scenario: cold start, background launch, system wake from sleep, and auto-start on login.

The bridge also needed to handle long-running operations gracefully. Font installation can take several seconds for large Chinese font files. I designed the API calls to return immediately with a status, then update the UI through a callback pattern, keeping the interface responsive during heavy operations.

(opens in new window)Automated Build, Sign, and Distribute

Shipping a desktop application on two platforms is vastly more complex than deploying a website. Each platform has its own packaging format, code signing requirements, and distribution rules. macOS requires notarization (Apple must scan and approve the binary before it can run without security warnings), and Windows needs Authenticode signing.

I built a unified build pipeline controlled by a single entry point that orchestrates the entire process: compile the React frontend with Vite, compile core Python modules to native code with Cython (protecting business logic from decompilation), package everything into a standalone application (py2app for macOS, PyInstaller for Windows), sign the binary with platform-specific certificates, run macOS notarization, and generate the final installer (DMG or NSIS).

Cython compilation was particularly important for an application that handles licensing logic locally. Without it, anyone could unpack the application bundle and read the Python source code to understand (and circumvent) the license verification. Compiling to .so (macOS) and .pyd (Windows) native extensions makes reverse engineering significantly harder while also improving execution performance.

Font Manager Build Package

(opens in new window)Background Mode and System Integration

A font manager needs to run silently at all times. Users shouldn’t have to remember to launch it. On macOS, the app registers as a login item through SMAppService and detects whether it was launched at login (within 5 minutes of boot) or manually by the user. Background launches skip the main window and show only a menu bar icon. On Windows, the app uses the registry Run key for auto-start and shows a system tray icon.

Single-instance control prevents users from accidentally launching multiple copies (which would conflict on font file operations). On Windows, I used a named mutex. On macOS, the dock handler detects reactivation and brings the existing window forward instead of creating a new one.

class SingleInstanceLock:
    def __init__(self, app_name: str, on_second_instance: Optional[Callable] = None):
        """
        Initialize the single instance lock.
 
        Args:
            app_name: Application name used for lock identification
            on_second_instance: Optional callback to execute when second instance is detected
        """
        self.app_name = app_name
        self.on_second_instance = on_second_instance
        self.lock_acquired = False
        self._lock_handle = None
        self._lock_file_path: Optional[str] = None
 
    def acquire(self) -> bool:
        """
        Try to acquire the single instance lock.
 
        Returns:
            bool: True if lock acquired (first instance), False if another instance is running
        """
 
    def _acquire_windows(self) -> bool:
        """
        Acquire lock on Windows using named mutex via pywin32.
 
        Returns:
            bool: True if lock acquired, False if already locked
        """
 
    def _activate_existing_window(self):
        """
        Try to activate the existing application window on Windows.
 
        Uses pywin32 to enumerate windows and bring the matching window to foreground.
        Handles both visible and hidden windows (e.g., when app is in system tray).
        """
        
    def _acquire_unix(self) -> bool:
        """
        Acquire lock on Unix-like systems using file lock.
 
        Uses fcntl for proper file locking on Unix/Linux/macOS.
 
        Returns:
            bool: True if lock acquired, False if already locked
        """
        
    def release(self):
        """
        Release the single instance lock.
 
        Cleans up mutex handle on Windows or file lock on Unix.
        """
 
    def __enter__(self):
        """Context manager entry."""
        return self
 
    def __exit__(self, exc_type, exc_val, exc_tb):
        """Context manager exit."""
        self.release()
        return False
 

(opens in new window)Impact & Results

justfont Manager shipped as v1.0.0 in December 2025 and became the primary distribution channel for justfont’s font rental service. The application handles the complete font lifecycle from subscription verification through font installation to automatic cleanup on expiration without requiring any manual steps from users.

The system manages fonts across both macOS and Windows with a single codebase and a unified user experience. The offline tolerance period eliminated a major category of support requests from users who lost font access during network interruptions. The Cython-compiled licensing logic provides meaningful protection for justfont’s business model without impacting user experience.

The automated build pipeline reduced release preparation from a multi-day manual process to a single command that produces signed, notarized installers for both platforms. This made it practical to ship updates frequently, responding to user feedback and fixing issues within days rather than weeks.

(opens in new window)Lessons Learned

Building a desktop application after years of web development taught me how much the web platform abstracts away. On the web, you deploy once and it works everywhere. On desktop, every operating system has its own font APIs, file locking behavior, credential storage, process management, and code signing requirements. The abstraction layer I built to unify these differences was the most architecturally important part of the project and the part that required the deepest understanding of both platforms.

The PyWebView hybrid approach proved that you do not have to choose between a modern UI framework and deep OS integration. React handled the interface beautifully, Python handled the system-level operations that JavaScript can not reach, and the bridge between them once the timing issues were solved felt invisible to users. This architecture pattern is something I would use again for any desktop application that needs both a polished UI and native system access.

The build and distribution pipeline was a bigger investment than I initially expected. Code signing, notarization, Cython compilation, and installer packaging each had their own learning curve and failure modes. But automating all of it into a reproducible pipeline was essential manual release processes do not scale, and any step that requires human judgment is a step that will eventually be done wrong. The pipeline now runs reliably on every release, and that reliability gives us the confidence to ship updates quickly.