tauri

$npx mdskill add EpicenterHQ/epicenter/tauri

Handles Tauri commands, file paths, and cross-platform desktop app security

  • Solves Tauri command setup, permissions, and filesystem path issues in desktop apps
  • Uses Rust commands, @tauri-apps/api, and native filesystem APIs
  • Analyzes security config, capabilities, and platform-specific file operations
  • Provides precise code patterns for Tauri app development workflows
SKILL.md
.github/skills/tauriView on GitHub ↗
---
name: tauri
description: Tauri commands, permissions, capabilities, security config, path handling, cross-platform file ops, and native filesystem APIs. Use when mentioning Tauri, desktop apps, Rust commands, invoke, capabilities, permissions, ResourceId, file paths, or platform differences.
metadata:
  author: epicenter
  version: '1.0'
---

# Tauri Patterns
## Reference Repositories

- [Tauri](https://github.com/tauri-apps/tauri) — Desktop app framework with Rust backend and web frontend

## When to Apply This Skill

Use this pattern when you need to:

- Add or change Tauri commands, permissions, capabilities, or security config.
- Build file paths in Tauri frontend code running in the webview.
- Choose correctly between `@tauri-apps/api/path` and Node/Bun `path` APIs.
- Replace manual slash concatenation with `join()`, `dirname()`, and related helpers.
- Handle cross-platform filesystem behavior for desktop apps.
- Combine Tauri path APIs with `@tauri-apps/plugin-fs` operations.

## Commands, Permissions, And Security

- Expose focused Rust APIs with `#[tauri::command]`, register them with `generate_handler!`, and return `Result<T, E>` for fallible work.
- Validate command inputs on the Rust side. TypeScript callers are not the trust boundary.
- Keep capabilities least-privilege in `app.security.capabilities`, scoped to the windows or webviews that need them. Avoid broad permission wildcards.
- Treat CSP, `devCsp`, asset protocol configuration, `convertFileSrc`, `freezePrototype`, and remote IPC as security-sensitive config.
- Long-lived Rust objects should be Tauri resources with frontend `ResourceId`s. Do not serialize complex long-lived objects through command responses.

## Context Detection

Before choosing a path API, determine your execution context:

| Context                 | Location                                       | Correct API            |
| ----------------------- | ---------------------------------------------- | ---------------------- |
| **Tauri frontend**      | `apps/*/src/**/*.ts`, `apps/*/src/**/*.svelte` | `@tauri-apps/api/path` |
| **Node.js/Bun backend** | `packages/**/*.ts`, CLI tools                  | Node.js `path` module  |

**Rule**: If the code runs in the browser (Tauri webview), use Tauri's path APIs. If it runs in Node.js/Bun, use the Node.js `path` module.

## Available Functions from `@tauri-apps/api/path`

### Path Manipulation

| Function               | Purpose                                    | Example                                           |
| ---------------------- | ------------------------------------------ | ------------------------------------------------- |
| `join(...paths)`       | Join path segments with platform separator | `await join(baseDir, 'workspaces', id)`           |
| `dirname(path)`        | Get parent directory                       | `await dirname('/foo/bar/file.txt')` → `/foo/bar` |
| `basename(path, ext?)` | Get filename, optionally strip extension   | `await basename('/foo/bar.txt', '.txt')` → `bar`  |
| `extname(path)`        | Get file extension                         | `await extname('file.txt')` → `.txt`              |
| `normalize(path)`      | Resolve `..` and `.` segments              | `await normalize('/foo/bar/../baz')` → `/foo/baz` |
| `resolve(...paths)`    | Resolve to absolute path                   | `await resolve('relative', 'path')`               |
| `isAbsolute(path)`     | Check if path is absolute                  | `await isAbsolute('/foo')` → `true`               |

### Platform Constants

| Function      | Purpose                 | Returns                      |
| ------------- | ----------------------- | ---------------------------- |
| `sep()`       | Platform path separator | `\` on Windows, `/` on POSIX |
| `delimiter()` | Platform path delimiter | `;` on Windows, `:` on POSIX |

### Base Directories

| Function                | Purpose                            |
| ----------------------- | ---------------------------------- |
| `appLocalDataDir()`     | App's local data directory         |
| `appDataDir()`          | App's roaming data directory       |
| `appConfigDir()`        | App's config directory             |
| `appCacheDir()`         | App's cache directory              |
| `appLogDir()`           | App's log directory                |
| `tempDir()`             | System temp directory              |
| `resourceDir()`         | App's resource directory           |
| `resolveResource(path)` | Resolve path relative to resources |

## Patterns

### Constructing Paths (Correct)

```typescript
import { appLocalDataDir, dirname, join } from '@tauri-apps/api/path';

// Join path segments - handles platform separators automatically
const baseDir = await appLocalDataDir();
const filePath = await join(baseDir, 'workspaces', workspaceId, 'data.json');

// Get parent directory - cleaner than manual slicing
const parentDir = await dirname(filePath);
await mkdir(parentDir, { recursive: true });
```

### Logging Paths (Exception)

For human-readable log output, hardcoded `/` is acceptable since it's not used for filesystem operations:

```typescript
// OK for logging - consistent cross-platform log output
const logPath = pathSegments.join('/');
console.log(`[Persistence] Loading from ${logPath}`);
```

## Anti-Patterns

### Never: Manual String Concatenation

```typescript
// BAD: Hardcoded separator breaks on Windows
const filePath = baseDir + '/' + 'workspaces' + '/' + id;

// BAD: Template literal with hardcoded separator
const filePath = `${baseDir}/workspaces/${id}`;

// GOOD: Use join()
const filePath = await join(baseDir, 'workspaces', id);
```

### Never: Manual Parent Directory Extraction

```typescript
// BAD: Manual slicing is error-prone
const parentSegments = pathSegments.slice(0, -1);
const parentDir = await join(baseDir, ...parentSegments);

// GOOD: Use dirname()
const parentDir = await dirname(filePath);
```

### Never: Hardcoded Separators in Filesystem Operations

```typescript
// BAD: Windows uses backslashes
const configPath = appDir + '/config.json';

// GOOD: Platform-agnostic
const configPath = await join(appDir, 'config.json');
```

### Never: Assuming Path Format

```typescript
// BAD: Splitting on '/' fails on Windows paths
const parts = filePath.split('/');

// GOOD: Use dirname/basename for extraction
const dir = await dirname(filePath);
const file = await basename(filePath);
```

## Import Pattern

Always import from `@tauri-apps/api/path`:

```typescript
import {
	appLocalDataDir,
	dirname,
	join,
	basename,
	extname,
	normalize,
	resolve,
	sep,
} from '@tauri-apps/api/path';
```

## Note on Async

All Tauri path functions are **async** because they communicate with the Rust backend via IPC. Always `await` them:

```typescript
// All path operations return Promises
const baseDir = await appLocalDataDir();
const filePath = await join(baseDir, 'file.txt');
const parent = await dirname(filePath);
const separator = await sep();
```

## Filesystem Operations

Use `@tauri-apps/plugin-fs` for file operations, combined with Tauri path APIs:

```typescript
import { appLocalDataDir, dirname, join } from '@tauri-apps/api/path';
import { mkdir, readFile, writeFile } from '@tauri-apps/plugin-fs';

async function saveData(segments: string[], data: Uint8Array) {
	const baseDir = await appLocalDataDir();
	const filePath = await join(baseDir, ...segments);

	// Ensure parent directory exists
	const parentDir = await dirname(filePath);
	await mkdir(parentDir, { recursive: true });

	await writeFile(filePath, data);
}
```
More from EpicenterHQ/epicenter