# How Fluxer Handles the Custom App URL Feature for Self-Hosted Instances

> Learn how Fluxer manages custom app URLs for self-hosted instances. Fluxer validates trusted origins, reloads the main window, and preserves permissions for a seamless experience.

- Repository: [Fluxer/fluxer](https://github.com/fluxerapp/fluxer)
- Tags: how-to-guide
- Published: 2026-03-17

---

**Fluxer stores self-hosted URLs in a local [`settings.json`](https://github.com/fluxerapp/fluxer/blob/main/settings.json) file, validates them as trusted origins, and reloads the Electron main window to point at the custom instance while preserving WebAuthn and media permissions.**

Fluxer is an open-source Electron-based desktop application that can connect to self-hosted web instances instead of its default public URLs. Understanding how the **custom app URL feature for self-hosted instances** works requires examining the configuration persistence layer, the trust validation system, and the IPC handlers that coordinate between the renderer and main processes.

## Storing the Custom URL in settings.json

When the desktop application starts, it attempts to load a per-user configuration file located in the application's data directory. According to the `fluxerapp/fluxer` source code, the [`DesktopConfig.tsx`](https://github.com/fluxerapp/fluxer/blob/main/DesktopConfig.tsx) module manages this persistence layer.

The `loadDesktopConfig()` function reads from [`settings.json`](https://github.com/fluxerapp/fluxer/blob/main/settings.json) located at `userDataPath`:

```typescript
// fluxer_desktop/src/common/DesktopConfig.tsx
configPath = path.join(userDataPath, CONFIG_FILE_NAME);
if (fs.existsSync(configPath)) {
    const data = fs.readFileSync(configPath, 'utf-8');
    config = JSON.parse(data) as DesktopConfig;
}

```

The JSON structure supports an optional `app_url` key that overrides the default production endpoints:

```json
{
  "app_url": "https://my.selfhosted.instance"
}

```

If this file is absent or the key is undefined, Fluxer falls back to the official **stable** or **canary** URLs defined in [`Constants.tsx`](https://github.com/fluxerapp/fluxer/blob/main/Constants.tsx).

## Resolving the Effective App URL at Runtime

The `getAppUrl()` function in [`DesktopConfig.tsx`](https://github.com/fluxerapp/fluxer/blob/main/DesktopConfig.tsx) determines which URL the Electron window should actually load. It implements a simple priority check:

```typescript
// fluxer_desktop/src/common/DesktopConfig.tsx
export function getAppUrl(): string {
    if (config.app_url) {
        return config.app_url;               // <- custom self‑hosted URL
    }
    return BUILD_CHANNEL === 'canary' ? CANARY_APP_URL : STABLE_APP_URL;
}

```

For trust-validation purposes, `getCustomAppUrl()` returns the stored value (or `null`) without falling back to defaults. This separation ensures that the security logic can distinguish between user-defined instances and official origins.

## Validating the Custom URL as a Trusted Origin

Security in Fluxer relies on an origin allow-list. The `isTrustedOrigin()` function in [`Window.tsx`](https://github.com/fluxerapp/fluxer/blob/main/Window.tsx) extends this trust to self-hosted instances by comparing navigation targets against the stored custom URL:

```typescript
// fluxer_desktop/src/main/Window.tsx
function isTrustedOrigin(url?: string): boolean {
    const origin = getOrigin(url);
    if (!origin) return false;
    if (trustedWebOrigins.has(origin)) return true;
    const customUrl = getCustomAppUrl();
    if (customUrl) {
        try {
            return new URL(customUrl).origin === origin;
        } catch {
            return false;
        }
    }
    return false;
}

```

This mechanism ensures that **permissions such as WebAuthn, media access, and notifications** function identically for self-hosted instances and official Fluxer domains.

## Loading the Custom URL in the Main Window

During window creation, the Electron `BrowserWindow` loads the URL returned by `getAppUrl()`. The `createWindow()` function in [`Window.tsx`](https://github.com/fluxerapp/fluxer/blob/main/Window.tsx) orchestrates this:

```typescript
// fluxer_desktop/src/main/Window.tsx
const appUrl = getAppUrl();               // <-- respects custom URL
mainWindow.loadURL(appUrl).catch(error => {
    logger.error('Failed to load app URL:', error);
});

```

All subsequent navigation events—including `will-navigate` and `setWindowOpenHandler`—consult `isTrustedOrigin()` to determine whether to allow in-app navigation or escalate the request to the system's default browser.

## Switching Instances via IPC

The renderer process triggers URL changes through the `switch-instance-url` IPC channel. The handler in [`IpcHandlers.tsx`](https://github.com/fluxerapp/fluxer/blob/main/IpcHandlers.tsx) validates the new origin, persists it, and reloads the window:

```typescript
// fluxer_desktop/src/main/IpcHandlers.tsx
ipcMain.handle('switch-instance-url', async (_event, options) => {
    const instanceOrigin = normalizeInstanceOrigin(options.instanceUrl);
    await assertValidFluxerInstance(instanceOrigin);
    setCustomAppUrl(instanceOrigin);          // store new custom URL
    await mainWindow.loadURL(instanceOrigin); // reload the window
});

```

If the new URL fails to load, the handler clears the stored configuration (`setCustomAppUrl(null)`) to revert to the default instance. Every call to `setCustomAppUrl()` invokes `saveDesktopConfig()`, which atomically writes the updated JSON to disk:

```typescript
// fluxer_desktop/src/common/DesktopConfig.tsx
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');

```

This guarantees that the custom instance setting survives application restarts.

## Practical Implementation Examples

### Prompting for a Self-Hosted URL (Renderer Process)

To initiate a switch from the UI layer, invoke the IPC channel with a validated HTTPS URL:

```typescript
import { ipcRenderer } from 'electron';

async function switchToSelfHosted(url: string) {
  if (!/^https?:\/\//.test(url)) {
    throw new Error('URL must include protocol (https://)');
  }
  
  await ipcRenderer.invoke('switch-instance-url', {
    instanceUrl: url,
    desktopHandoffCode: null,
  });
}

```

### Resetting to the Official Instance

Passing an empty string to the same handler clears the custom configuration and reloads the default URL:

```typescript
await ipcRenderer.invoke('switch-instance-url', {
  instanceUrl: '',
  desktopHandoffCode: null,
});

```

### Checking Configuration in the Main Process

Main-process modules can inspect the current configuration using the `DesktopConfig` helpers:

```typescript
import { getAppUrl, getCustomAppUrl } from '../common/DesktopConfig';

const currentUrl = getAppUrl();        // Returns custom or default
const customOnly = getCustomAppUrl();  // Returns custom URL or null

```

## Summary

- **Persistence**: Custom URLs are stored in [`settings.json`](https://github.com/fluxerapp/fluxer/blob/main/settings.json) via [`DesktopConfig.tsx`](https://github.com/fluxerapp/fluxer/blob/main/DesktopConfig.tsx) and survive application restarts.
- **Resolution**: `getAppUrl()` selects the custom URL over defaults, while `getCustomAppUrl()` enables trust checks.
- **Security**: `isTrustedOrigin()` in [`Window.tsx`](https://github.com/fluxerapp/fluxer/blob/main/Window.tsx) treats self-hosted origins as trusted, enabling WebAuthn and media permissions.
- **IPC**: The `switch-instance-url` channel in [`IpcHandlers.tsx`](https://github.com/fluxerapp/fluxer/blob/main/IpcHandlers.tsx) handles runtime switching with validation and rollback on failure.
- **Core files**: [`DesktopConfig.tsx`](https://github.com/fluxerapp/fluxer/blob/main/DesktopConfig.tsx), [`Window.tsx`](https://github.com/fluxerapp/fluxer/blob/main/Window.tsx), and [`IpcHandlers.tsx`](https://github.com/fluxerapp/fluxer/blob/main/IpcHandlers.tsx) implement the complete self-hosted instance workflow.

## Frequently Asked Questions

### Where does Fluxer store the custom self-hosted URL?

Fluxer writes the custom URL to a [`settings.json`](https://github.com/fluxerapp/fluxer/blob/main/settings.json) file located in the user's application data directory (determined by `userDataPath`). The [`DesktopConfig.tsx`](https://github.com/fluxerapp/fluxer/blob/main/DesktopConfig.tsx) module handles all reads and writes to this file using standard Node.js `fs` operations.

### How does Fluxer ensure my self-hosted instance is secure?

The `isTrustedOrigin()` function in [`Window.tsx`](https://github.com/fluxerapp/fluxer/blob/main/Window.tsx) validates that navigation targets match either the official Fluxer domains or the exact origin stored in your custom configuration. This prevents phishing attempts while granting your instance the same permissions (WebAuthn, camera, microphone) as the official sites.

### Can the custom URL be changed without restarting the application?

Yes. The renderer process can invoke the `switch-instance-url` IPC channel at any time. The main process validates the new URL, updates [`settings.json`](https://github.com/fluxerapp/fluxer/blob/main/settings.json) via `setCustomAppUrl()`, and immediately reloads the main window using `mainWindow.loadURL()` without requiring an app restart.

### What happens if the self-hosted instance becomes unreachable?

If `mainWindow.loadURL()` fails after switching, the `switch-instance-url` handler automatically clears the custom configuration by calling `setCustomAppUrl(null)`. This reverts the application to the default stable or canary URL on the next load cycle.