Skip to content

Tauri

Use this playbook to design, build, secure, test, and release a Tauri v2 application.

This guidance targets Tauri v2, as the official documentation describes it on the last updated date. Recheck the linked documentation before each major release.

The one rule: minimize the authority reachable from the webview. Then enforce every important rule again inside the Rust core.

A Tauri application has two trust zones.

  • The webview renders the user interface.
  • The Rust core owns native authority.

Treat all webview input as untrusted. A frontend defect can become native code access through IPC.

The Rust core must enforce every security rule. Frontend validation improves usability but does not provide security.

Tauri uses the system webview. Windows uses WebView2, macOS uses WKWebView, and Linux uses WebKitGTK.

The installed webview version can differ by platform and device. Test real target platforms and supported operating system versions.

Sources: process model, webview versions, runtime authority.

Choose Tauri when all of these conditions apply.

  • The product needs a desktop or mobile application.
  • The team can maintain Rust and web code.
  • Small bundles or native integration are important.
  • The user interface works as static web assets.
  • The team will test different system webviews.

Choose another design when any of these conditions apply.

  • The application requires a bundled, identical browser runtime.
  • The application depends on server-side rendering inside the desktop process.
  • The team cannot support platform-specific packaging and signing.
  • The application can remain a normal website without native access.

Tauri serves static assets. Use an SPA, SSG output, or a classic multi-page application.

Do not place an SSR server inside the application without a specific requirement. Use a normal client-server API boundary.

Source: frontend configuration.

Use a small baseline unless product requirements require more.

  • Tauri v2.
  • Stable Rust through rustup.
  • TypeScript for a JavaScript frontend.
  • Vite for an SPA.
  • One package manager and one lockfile.
  • Official Tauri plugins when they meet the requirement.
  • Platform-native CI runners for release builds.

Commit these files.

  • Cargo.lock.
  • The frontend package lockfile.
  • src-tauri/tauri.conf.json.
  • All capability and permission files.
  • Platform configuration files.
  • The updater public key.

Never commit these items.

  • Signing private keys.
  • Signing passwords.
  • Updater private keys.
  • API secrets.
  • Production user data.

Install the platform prerequisites first. Tauri requires different native tools on Linux, macOS, and Windows.

Source: prerequisites.

Create a standard project with the official scaffold.

Terminal window
pnpm create tauri-app
cd APP-NAME
pnpm install
pnpm tauri dev

Replace the following:

  • APP-NAME: the project name that you gave the scaffold.

Use the manual initializer only when you already have a frontend.

Source: create a project.

For Vite, align the development port with build.devUrl. Set build.frontendDist to the generated static directory.

{
"build": {
"beforeDevCommand": "pnpm dev",
"beforeBuildCommand": "pnpm build",
"devUrl": "http://localhost:5173",
"frontendDist": "../dist"
}
}

Configure Vite to ignore src-tauri. This prevents unnecessary rebuild work.

Source: Vite configuration.

Keep framework code separate from application logic.

APP-NAME/
├── src/
│ ├── app/ # startup and routing
│ ├── features/ # product features
│ ├── components/ # reusable UI
│ ├── lib/tauri/ # typed IPC client
│ └── types/ # shared frontend types
├── src-tauri/
│ ├── capabilities/ # window and webview privileges
│ ├── permissions/ # custom command permissions
│ ├── src/
│ │ ├── commands/ # thin IPC adapters
│ │ ├── domain/ # rules and domain types
│ │ ├── services/ # files, network, database
│ │ ├── platform/ # target-specific adapters
│ │ ├── error.rs # serializable command errors
│ │ └── lib.rs # builder and registration
│ ├── Cargo.toml
│ └── tauri.conf.json
└── tests/

Keep lib.rs focused on composition. Do not place all commands and business rules in this file.

Keep commands thin. A command must validate input, authorize work, call a service, and map the result.

Test domain and service code without a webview. This design reduces slow end-to-end tests.

Use commands for requests that return a result. Use channels for ordered streams.

Use events only for notifications. Events are not type-safe and cannot return values.

Define stable transport types on both sides. Use one naming convention.

#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct CreateNoteRequest {
title: String,
body: String,
}
#[derive(serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct NoteDto {
id: String,
title: String,
}

Create one frontend IPC module. Do not scatter command names across UI components.

import { invoke } from "@tauri-apps/api/core";
export type CreateNoteRequest = { title: string; body: string };
export type NoteDto = { id: string; title: string };
export function createNote(input: CreateNoteRequest): Promise<NoteDto> {
return invoke<NoteDto>("create_note", { input });
}

Generate bindings when the selected generator is maintained and reviewed. Generated types do not replace runtime validation.

Return Result<T, AppError> from fallible commands. Serialize stable error codes for the frontend.

Do not return internal paths, SQL text, tokens, or debug details to the webview.

#[derive(Debug, thiserror::Error, serde::Serialize)]
#[serde(tag = "code", content = "details", rename_all = "snake_case")]
enum AppError {
#[error("The note was not found")]
NotFound,
#[error("The input is invalid")]
InvalidInput { field: String },
#[error("An internal error occurred")]
Internal,
}

Log the internal cause in Rust. Return a safe error and a correlation identifier to the frontend.

Source: command error handling.

Use asynchronous commands for file, network, database, and other slow work.

Synchronous commands run on the main thread unless configured otherwise. Long work can freeze the user interface.

Move CPU-heavy work to a blocking worker. Add cancellation for work that the user can stop.

Set timeouts for network and sidecar operations. Bound queues, retries, payload sizes, and concurrency.

Source: asynchronous commands.

Use these rules.

  • Use commands for request-response operations.
  • Use a raw IPC response for large binary results.
  • Use channels for progress, chunks, and ordered streams.
  • Use targeted events for small notifications.
  • Use global events only when every listener needs the event.

Clean up every frontend event listener during component unmount. Async listeners can process rapid events out of order.

Source: commands, channels, and events.

Place long-lived services in Tauri managed state. Retrieve state through State or the Manager trait.

Use a type alias for wrapped state. This prevents runtime panics from mismatched state types.

struct AppStateInner {
repository: NoteRepository,
}
type AppState = std::sync::Mutex<AppStateInner>;

Tauri already shares managed state. Do not add Arc without another ownership requirement.

Use std::sync::Mutex for short synchronous critical sections. Use an async mutex only across required await points.

Never hold a standard mutex guard across an await point. Never perform network or disk work while holding a lock.

Prefer message passing or a dedicated worker for complex mutable resources. Keep lock order explicit when multiple locks exist.

Source: state management.

Use Tauri path APIs and platform base directories. Do not construct platform paths manually.

  • Put user configuration in the application config directory.
  • Put mutable application data in the application data directory.
  • Put caches in the cache directory.
  • Put logs in the log directory.
  • Use a user-selected path for user documents.

Use narrow file scopes. Prefer one application directory or one selected file over the home directory.

Close file handles after use. Use atomic replacement for important configuration files.

Validate file size and format before parsing. Treat imported files as untrusted input.

Source: file system plugin.

  • Use a key-value store for small, non-secret preferences.
  • Use SQLite for structured, relational, or queryable data.
  • Use Stronghold or an operating system credential store for secrets.
  • Use normal files for user-owned documents.

Do not put credentials in localStorage, plain JSON, logs, or frontend state persistence.

Source: Stronghold plugin.

Plan schema migrations before the first public release. Make every migration transactional and retry-safe.

Back up valuable data before a destructive migration. Test upgrades from every supported stored schema version.

Tauri capabilities control which windows and webviews can call native APIs. Permissions enable commands and scopes restrict their behavior.

Use one capability file per security role. Reference capability identifiers explicitly from tauri.conf.json.

Avoid window wildcards. Avoid capability overlap because privileges merge for matching windows and webviews.

Use platform-specific capabilities when a privilege only applies to one target.

Source: capabilities.

Add one command permission for one requirement. Add the narrowest scope that supports the operation.

{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "main-documents",
"description": "The main webview can read selected application documents.",
"windows": ["main"],
"platforms": ["linux", "macOS", "windows"],
"permissions": [
"dialog:allow-open",
{
"identifier": "fs:allow-read-text-file",
"allow": [{ "path": "$APPDATA/documents/**" }]
}
]
}

Confirm each permission identifier against the generated schema. Plugin permissions can change across releases.

Do not grant a plugin default permission set without reviewing every included command.

Use deny scopes for sensitive subdirectories. A deny scope takes precedence over an allow scope.

Source: permissions, command scopes, plugin permission workflow.

Register only required commands. Custom commands are otherwise available to local application webviews by default.

Declare custom commands in the application manifest when different webviews need different command access.

Enforce custom scope rules inside each command. Tauri passes scopes, but the command implements the validation.

Audit path normalization, URL parsing, glob rules, case handling, symbolic links, and deny precedence.

Source: custom command capability guidance, scope enforcement.

Bundle the production frontend with the application. Do not load the main interface from a remote URL.

Avoid remote scripts, styles, fonts, and CDN assets. A remote asset can change without an application release.

Import Tauri APIs from packages. Keep app.withGlobalTauri disabled unless a reviewed integration requires the global object.

Set a strict Content Security Policy. Tauri only applies CSP protection when the configuration defines it.

{
"app": {
"security": {
"csp": {
"default-src": "'self'",
"connect-src": "'self' ipc: http://ipc.localhost https://api.example.com",
"img-src": "'self' asset: http://asset.localhost blob: data:",
"style-src": "'self'",
"font-src": "'self'",
"object-src": "'none'",
"base-uri": "'none'",
"frame-ancestors": "'none'"
}
}
}
}

Adapt the policy to actual requirements. Do not add 'unsafe-eval' or broad hosts to fix an unexplained error.

Some frontend tools need a different development policy. Keep development exceptions out of production configuration.

Source: Content Security Policy.

Use the isolation pattern for an additional IPC inspection layer when practical. It does not replace Rust validation.

Source: isolation pattern.

Open normal web links in the system browser. Do not navigate a privileged application webview to arbitrary content.

If remote content must appear, place it in a separate webview with no native permissions.

Do not grant remote IPC access unless the product cannot work without it. Pin exact trusted origins and minimize permissions.

Domain ownership must remain secure for the complete application lifetime. A domain takeover can become native compromise.

Validate these properties in Rust.

  • String length and character rules.
  • Numeric range.
  • Collection length.
  • File size and format.
  • Normalized file path and allowed root.
  • Parsed URL scheme, host, port, and path.
  • Identifier ownership and current user authority.
  • Operation state and replay behavior.

Use parser types instead of string prefix checks. Canonicalize paths before authorization when the operation permits it.

Prevent time-of-check to time-of-use errors. Open files with safe platform flags when replacement matters.

Prefer a Rust library over a shell command. Prefer a fixed sidecar over an arbitrary system executable.

Never concatenate user input into a shell string. Pass fixed executables and separate arguments.

Allow only named commands, fixed argument forms, and required working directories. Set execution timeouts and output limits.

Package one sidecar for each target triple. Verify the sidecar source, license, signature, and update process.

Source: embedding external binaries.

Prefer official plugins for common platform functions. Review each plugin’s maintenance, permissions, scopes, and native code.

Remove unused plugins and Cargo features. Every dependency increases build time, bundle size, and supply-chain exposure.

Use these automated checks in CI.

  • Rust formatting and linting.
  • Rust tests.
  • Frontend formatting, linting, type checks, and tests.
  • Rust dependency advisories.
  • Frontend dependency advisories.
  • License policy checks.
  • Secret scanning.
  • Generated capability schema validation.

Pin CI actions to reviewed commit hashes for high-assurance releases. Review automated dependency updates before merging.

Build from committed lockfiles. Record toolchain and runner versions for release reproduction.

Update Tauri promptly after a security release. Use Tauri 2.11.1 or later: it fixes remote-origin ACL bypasses in specific configurations.

Source: Tauri 2.11.1 security fixes, ecosystem security.

Do not assume browser behavior is identical across system webviews. Avoid new web features without support verification.

Test these areas on every supported platform.

  • Font metrics and text rendering.
  • Focus, keyboard shortcuts, and input methods.
  • File dialogs and path behavior.
  • Window size, scale, and multiple monitors.
  • Dark mode and accessibility settings.
  • Clipboard and drag operations.
  • Deep links and single-instance behavior.
  • Sleep, resume, shutdown, and application exit.
  • Offline startup and proxy behavior.

Use target-specific Rust modules and configuration files. Keep conditional logic out of normal domain code.

Build Windows artifacts on Windows and macOS artifacts on macOS. Cross-compilation can have packaging and signing limits.

Source: Windows installer guidance.

Use separate mobile capability files with the mobile schema. Do not expose desktop-only authority on mobile targets.

Tauri capability permission and operating system permission are separate controls. Satisfy both controls for protected mobile features.

Request an operating system permission only when the user starts the related feature. Explain the reason before a sensitive request.

Handle denial, restricted status, and later revocation. Do not make basic startup depend on an optional permission.

Test physical devices in addition to simulators. Test backgrounding, resume, rotation, safe areas, deep links, and memory pressure.

Expose the development server only on a trusted network. A physical mobile device must reach that server during development.

Sources: mobile plugin permissions, application lifecycle threats.

Define the expected behavior for each window.

  • Create or reuse.
  • Hide or destroy on close.
  • Restore focus.
  • Persist size and position.
  • Handle invalid saved monitor coordinates.
  • Release listeners and resources.

Do not create duplicate windows after rapid user actions. Use stable labels and idempotent creation logic.

Do not assume closing the main window exits a tray application. Test explicit exit and operating system shutdown behavior.

Give untrusted or less trusted windows separate capabilities. Multi-webview windows should use webview-level capability matching.

Measure startup time, command latency, memory, CPU, binary size, and packaged size on release builds.

Apply these changes when measurements justify them.

  • Lazy-load large frontend features.
  • Defer noncritical startup work.
  • Batch repeated IPC calls.
  • Stream large data through channels.
  • Return raw bytes instead of JSON byte arrays.
  • Move heavy work away from the main thread.
  • Remove unused Rust features and frontend packages.
  • Cache only immutable or correctly invalidated results.

Do not optimize only the Rust binary. Frontend JavaScript, images, fonts, and startup requests can dominate user experience.

Use development instrumentation only in debug builds.

Source: large responses and channels, debug-only DevTools.

  • Test Rust domain rules with unit tests.
  • Test service adapters with controlled fixtures.
  • Test frontend components in a browser test environment.
  • Mock invoke at the frontend IPC boundary.
  • Test transport serialization and error mapping.

Tauri provides frontend IPC mocks and a Rust mock runtime.

Sources: test overview, mock Tauri APIs.

  • Test database migrations on copied databases.
  • Test file scopes with allowed and denied paths.
  • Test custom command authorization.
  • Test sidecar failure, timeout, and malformed output.
  • Test offline and partial network failures.

Use WebdriverIO with the Tauri service for cross-platform desktop tests. It supports native IPC execution and mocking.

Keep the end-to-end suite focused on critical journeys. Use browser mode for faster renderer-only flows.

Source: WebDriver testing.

Test each supported architecture and operating system. Include a clean installation, upgrade, rollback policy, and uninstall behavior.

Run smoke tests against packaged artifacts. Development success does not prove bundle success.

Use structured logs in Rust and the frontend. Include timestamps, levels, module names, and correlation identifiers.

Write logs to the platform application log directory. Set rotation, retention, and maximum size.

Never log these values.

  • Passwords.
  • Tokens.
  • Signing material.
  • Full personal records.
  • Sensitive file contents.
  • Unnecessary absolute paths.

Make diagnostic export user-controlled. Redact sensitive values before export.

Source: logging plugin.

Capture these operational events without sensitive payloads.

  • Application version and platform.
  • Startup stages and duration.
  • Migration results.
  • Command category and duration.
  • Update checks and installation state.
  • Sidecar exit category.
  • Unhandled frontend and Rust failures.

Use a protected release workflow. Require review before release publication.

Perform these steps.

  1. Set one authoritative application version.
  2. Run all static checks and tests.
  3. Build each target on an appropriate runner.
  4. Sign each application and installer.
  5. Notarize macOS artifacts when required.
  6. Generate updater artifacts and signatures.
  7. Test packaged artifacts.
  8. Publish a draft release.
  9. Review artifacts and release notes.
  10. Publish the release and update metadata.

Tauri recommends the version in tauri.conf.json. Most distribution targets require code signing.

Source: distribution overview.

Store signing credentials in protected CI secrets or a dedicated signing service. Restrict release workflow access.

Use short-lived credentials when the platform supports them. Rotate compromised credentials and document the response procedure.

Do not expose secrets to pull-request workflows from untrusted branches. Do not print encoded certificates or passwords.

Sources: macOS signing, Windows signing.

Use HTTPS updater endpoints. Tauri enforces TLS in production unless an unsafe option disables it.

Keep the updater private key offline or in protected release infrastructure. Back it up securely.

The embedded updater public key verifies signatures. Losing the private key can block updates for installed applications.

Do not enable downgrade support without a signed rollback procedure. Test update interruption and recovery.

Use staged rollout when product risk requires it. Keep a previous signed release available for operational recovery.

Source: updater plugin.

Use this sequence for each native feature.

  1. Write the user outcome and failure states.
  2. Identify all untrusted input.
  3. Design the smallest IPC contract.
  4. Put business logic in a Rust service.
  5. Add structured request, response, and error types.
  6. Implement Rust validation and authorization.
  7. Register only the required command.
  8. Add the narrowest capability, permission, and scope.
  9. Add a typed frontend wrapper.
  10. Add unit and integration tests.
  11. Add one critical end-to-end path.
  12. Test the packaged build on each affected platform.
  13. Review logs, diagnostics, and privacy effects.
  14. Document recovery and update behavior.
Anti-patternFailure modeAccepted pattern
Trust frontend validationA compromised webview bypasses itRepeat validation and authorization in Rust
Load the production UI remotelyA server or domain compromise gains application reachBundle the UI locally
Set csp to nullXSS has fewer containment controlsDefine and test a strict CSP
Enable the global Tauri objectAny frontend script can discover a broad API surfaceImport required APIs from packages
Add broad CSP hostsAny allowed origin can increase attack reachAllow exact required origins
Enable remote IPC broadlyRemote content receives native authorityAvoid remote IPC or isolate exact origins
Use windows: ["*"]Every matching window gains authorityName each privileged window or webview
Overlap capabilities casuallyTauri merges their privilegesUse distinct, reviewed security roles
Grant plugin defaults blindlyThe frontend receives unused commandsReview and grant individual permissions
Scope the full home directoryOne frontend defect exposes user filesScope one application directory or selected file
Treat scopes as automaticCustom commands can ignore their scopeEnforce and audit scopes inside commands
Accept raw user pathsTraversal and link behavior can escape intentNormalize, authorize, and safely open paths
Build shell command stringsUser input can change command meaningUse fixed executables and argument arrays
Put business rules in commandsLogic becomes hard to test and reuseUse thin commands and tested services
Scatter invoke callsContracts drift without reviewUse one typed frontend IPC module
Return error strings onlyThe UI parses unstable textReturn stable error codes and safe messages
Use unwrap on input pathsA normal error terminates the processReturn typed errors
Run slow synchronous commandsWork blocks the main threadUse async commands or blocking workers
Hold a lock during I/ORequests stall or deadlockCopy required state, release the lock, then await
Send large data as JSONSerialization increases latency and memoryUse raw responses or channels
Use events for request-responseNo type or reply contract existsUse commands
Use events for ordered streamsAsync handlers can reorder workUse channels
Leak event listenersDuplicate work grows over timeStore and call each unlisten function
Persist secrets in web storageFrontend code can read the secretsUse Stronghold or an OS credential store
Log command payloads by defaultLogs collect secrets and personal dataLog metadata and redacted fields
Test only one webviewPlatform-specific defects reach usersTest every supported target
Treat Tauri permissions as OS permissionsMobile features fail or surprise usersDesign both permission layers
Test only development buildsPackaging and CSP defects remain hiddenTest signed or release-mode bundles
Cross-compile every releasePackaging and signing behavior can differUse native platform runners
Ship unsigned installersUsers see warnings and cannot verify originSign and notarize as required
Use an HTTP updaterNetwork attackers can alter metadata trafficUse HTTPS plus mandatory update signatures
Store updater keys in the repositoryRepository access enables malicious updatesProtect and back up private keys
Enable devtools in release buildsDiagnostics expose internals and add attack surfaceGate devtools with debug_assertions
Depend on asset secrecyBundled frontend assets can be extractedKeep secrets and enforcement in Rust or servers

Use this checklist for each pull request.

  • The command is a thin adapter.
  • Domain logic has direct tests.
  • The IPC contract uses explicit types.
  • The change handles cancellation and timeout requirements.
  • The change avoids unnecessary global state.
  • Rust validates all untrusted input.
  • Rust performs authorization before work.
  • The capability names exact windows or webviews.
  • The permission enables only required commands.
  • The scope covers only required resources.
  • The CSP remains strict.
  • No secret enters frontend storage or logs.
  • Remote content receives no native authority.
  • Shell and sidecar arguments cannot change command meaning.
  • Errors use stable codes.
  • Slow work does not block the main thread.
  • Locks do not cover I/O or await points.
  • Event listeners have cleanup.
  • Streams and queues have bounds.
  • Data migrations are retry-safe.
  • Paths use Tauri or platform APIs.
  • The change accounts for different system webviews.
  • Target-specific code stays behind an adapter.
  • The CI matrix covers affected targets.
  • Rust formatting, linting, and tests pass.
  • Frontend formatting, linting, type checks, and tests pass.
  • Capability schemas validate.
  • Dependency and license checks pass.
  • A packaged artifact passes its smoke test.
  • The support matrix names operating systems and architectures.
  • Every target builds from committed lockfiles.
  • Every target passes a packaged smoke test.
  • The capability set has a manual least-privilege review.
  • Production CSP has no unexplained exception.
  • Dependency and secret scans pass.
  • Database upgrades work from supported versions.
  • Signing succeeds for each required artifact.
  • macOS notarization and stapling succeed when required.
  • Updater metadata and signatures validate.
  • The updater private key has a secure backup.
  • Update interruption recovery works.
  • Logs rotate and redact sensitive data.
  • Crash and support procedures identify the application version.
  • Release notes describe migrations and known limitations.

Use these references during implementation and review.