# How Modly Syncs Built-in Extensions from App Resources to the User Data Directory

> Discover how Modly syncs built-in extensions from app resources to your user data directory on startup. Learn how this process ensures UI consistency and treats them like user-installed extensions.

- Repository: [lightningpixel/modly](https://github.com/lightningpixel/modly)
- Tags: internals
- Published: 2026-08-15

---

**Bold TLDR:** Modly copies built-in extensions from the application bundle to `<userData>/builtin-extensions` at startup using `syncBuiltinExtensions()`, ensuring the UI always loads the latest versions while treating them identically to user-installed extensions.

When an Electron application ships with pre-bundled extensions, those files live inside the application resources where they cannot be modified at runtime. Modly solves this by **syncing built-in extensions from app resources to the user data directory** on every launch. This process guarantees fresh, writable copies that the UI can load and manage like any other extension, while automatically applying updates when the app version changes.

## The Four-Step Sync Process

Modly's built-in extension sync follows a deterministic pipeline implemented in [`electron/main/builtin-sync.ts`](https://github.com/lightningpixel/modly/blob/main/electron/main/builtin-sync.ts). Each step handles a specific concern: path resolution, environment detection, file operations, and lifecycle timing.

### 1. Determine the Destination Path

The `getBuiltinExtensionsDir()` function constructs the target directory using Electron's standard `app.getPath('userData')` API. This ensures cross-platform compatibility without hardcoding paths.

```typescript
// electron/main/builtin-sync.ts#L6-L8
export function getBuiltinExtensionsDir(): string {
  return join(app.getPath('userData'), 'builtin-extensions');
}

```

This resolves to platform-appropriate locations:
- **macOS:** `~/Library/Application Support/Modly/builtin-extensions`
- **Windows:** `%APPDATA%/Modly/builtin-extensions`
- **Linux:** `~/.config/Modly/builtin-extensions`

### 2. Locate the Source Resources

The `getBuiltinResourcesDir()` function distinguishes between **packaged builds** and **development mode**. This dual-path approach allows the same codebase to work during local development and in production releases.

```typescript
// electron/main/builtin-sync.ts#L11-L15
export function getBuiltinResourcesDir(): string {
  if (app.isPackaged) {
    return join(process.resourcesPath, 'builtin-extensions');
  }
  return join(__dirname, '../../out/builtin-extensions');
}

```

- **Packaged builds:** extensions ship inside `Contents/Resources/builtin-extensions` (macOS) or equivalent
- **Development:** extensions compile to `out/builtin-extensions` relative to the source tree

### 3. Perform the Sync Operation

The `syncBuiltinExtensions()` function executes a **destructive copy** that guarantees consistency between source and destination. It implements three safety mechanisms:

| Step | Action | Rationale |
|------|--------|-----------|
| Verify source | Abort with log if resources directory missing | Prevents crashes on incomplete installations |
| Clean destination | `rmSync()` existing `builtin-extensions` folder | Eliminates stale files from previous versions |
| Recursive copy | `copySync()` entire resources directory | Creates fresh, identical copy |

```typescript
// electron/main/builtin-sync.ts#L32-L37
export function syncBuiltinExtensions(): void {
  const src = getBuiltinResourcesDir();
  const dest = getBuiltinExtensionsDir();

  if (!existsSync(src)) {
    console.log('No built-in extensions to sync');
    return;
  }

  // Remove stale extensions to ensure clean state
  if (existsSync(dest)) {
    rmSync(dest, { recursive: true, force: true });
  }

  // Copy fresh extensions from resources
  copySync(src, dest, { recursive: true });
  console.log('Synced built-in extensions to:', dest);
}

```

This **always-overwrite strategy** ensures updates propagate automatically. When Modly releases a new version with updated built-in extensions, users receive the changes on next launch without manual intervention.

### 4. Execute at Application Startup

The sync runs during Electron's `app.whenReady()` lifecycle, positioned strategically in [`electron/main/index.ts`](https://github.com/lightningpixel/modly/blob/main/electron/main/index.ts) after cache cleanup but before backend initialization.

```typescript
// electron/main/index.ts#L89-L103
app.whenReady().then(async () => {
  // Clear cache from previous sessions
  await clearCache();

  // Sync built-in extensions before UI or backend starts
  syncBuiltinExtensions();

  // Initialize Python backend with guaranteed fresh extensions
  const pyProcess = spawnPythonBackend();
  setupIpcHandlers();

  createMainWindow();
});

```

This execution order is critical. The **UI loads extensions from the user data directory**, not the application bundle, so the sync must complete before any renderer process attempts to enumerate available extensions.

## Practical Code Examples

### Trigger Sync Manually for Debugging

During development, you may need to re-sync without restarting the entire application:

```typescript
import { syncBuiltinExtensions } from './builtin-sync';

// Force immediate re-sync
syncBuiltinExtensions();

```

### Inspect Synced Extension Contents

Verify which built-in extensions are currently active in the user environment:

```typescript
import { getBuiltinExtensionsDir } from './builtin-sync';
import { readdirSync } from 'fs';

const extDir = getBuiltinExtensionsDir();
console.log('Built-in extensions path:', extDir);

try {
  const extensions = readdirSync(extDir, { withFileTypes: true })
    .filter(dirent => dirent.isDirectory())
    .map(dirent => dirent.name);
  
  console.log('Installed built-in extensions:', extensions);
} catch (err) {
  console.error('No synced extensions found:', err);
}

```

### Access Extensions in Renderer Process

The renderer receives the resolved path via IPC and loads extensions identically to user-installed ones:

```typescript
// Preload or renderer process
const builtinDir = await window.electron.getBuiltinExtensionsDir();
const extensions = await loadExtensionsFromDirectory(builtinDir);

```

## Why This Architecture Works

Modly's sync approach solves three distinct problems common to Electron extension systems:

**Problem: Read-only application bundles.** Electron apps on macOS are signed and cannot modify their own resources. The user data directory is always writable.

**Problem: Extension hosting constraints.** The extension loader expects a flat directory structure with [`package.json`](https://github.com/lightningpixel/modly/blob/main/package.json) manifests. The sync produces this structure without encoding special cases for built-ins versus user installs.

**Problem: Version drift on updates.** Without cleaning the destination, old built-in extensions would persist alongside new ones. The `rmSync` + `copySync` pattern guarantees atomic replacement.

## Summary

- **`getBuiltinExtensionsDir()`** resolves `<userData>/builtin-extensions` as the writable destination
- **`getBuiltinResourcesDir()`** selects `process.resourcesPath` for packaged builds or `out/builtin-extensions` for development
- **`syncBuiltinExtensions()`** performs destructive copy: validates source, removes stale destination, recursively copies fresh files
- **Execution timing** in [`electron/main/index.ts`](https://github.com/lightningpixel/modly/blob/main/electron/main/index.ts) places sync between cache clear and backend start, ensuring UI sees current extensions

## Frequently Asked Questions

### Where are built-in extensions stored after Modly syncs them?

After sync, built-in extensions reside in the `builtin-extensions` subdirectory of Electron's `userData` folder—typically `~/Library/Application Support/Modly/builtin-extensions` on macOS, `%APPDATA%/Modly/builtin-extensions` on Windows, and `~/.config/Modly/builtin-extensions` on Linux. The `getBuiltinExtensionsDir()` function computes this path dynamically using `app.getPath('userData')`.

### Does Modly delete my user-installed extensions during the sync?

No. The sync operation only targets the `builtin-extensions` folder. User-installed extensions live in a separate directory managed by [`extension-install-utils.ts`](https://github.com/lightningpixel/modly/blob/main/extension-install-utils.ts). The `rmSync()` call in `syncBuiltinExtensions()` is scoped strictly to the built-in destination path.

### How does Modly handle built-in extension updates?

Updates are automatic and require no user action. When Modly launches after an app upgrade, the sync process detects changed source files (newer versions in `process.resourcesPath`), deletes the old `builtin-extensions` folder in user data, and copies the updated files. The destructive copy guarantees the running instance matches the application version.

### Can I prevent built-in extensions from syncing to我的 user data directory?

There is no supported configuration to disable the sync. The architecture assumes built-in extensions must be writable for the extension system to function. If you need to inspect the original files, examine `process.resourcesPath/builtin-extensions` directly in a packaged build, though modifying these has no effect on the running application.