Resources
Bundle and access additional files like fonts, databases, and configuration in Tauri v2 from Rust and React with practical examples and best practices
When you build a Tauri app, the final executable is often just a few megabytes. But real-world applications need more than code — they need fonts, database seeds, JSON configs, or machine learning models. These extra files are what Tauri calls resources.
Unlike the frontend assets in your public folder, resources are not served by a development server. They get physically copied into the app bundle and live alongside the binary at runtime. This distinction matters because it determines how you load them, where they end up on the user's machine, and how you handle platform differences.
Understanding Resources
A resource is any file or folder you want to ship with your application that isn't part of the executable itself or the frontend source. The Resources API chapter covers the JavaScript side of the same files. The idea is simple: you tell Tauri which files to include, Tauri copies them into a special directory when building, and you read them at runtime using Tauri's path APIs.
This mechanism solves a concrete distribution problem. You could embed some data directly in your Rust binary using include_bytes!, but that bloats the binary and makes updating the data cumbersome. Resources stay as separate files on disk, so they can be swapped, patched, or even generated after installation without recompiling the app.
Resources vs. frontend assets:
Files in your Vite public directory are available to the webview at their relative paths during development and get bundled into the final HTML output. Resources live in a directory that Rust can read using absolute paths or asset protocol URLs, and they are not directly served by the dev server. Use resources for data the Rust backend needs, or files too large or sensitive to bundle with the frontend.
Where exactly do these files end up? The resource directory is platform-dependent and Tauri manages it automatically. On Windows, it's typically next to the .exe; on macOS, inside the .app bundle in Contents/Resources; on Linux, in a location like /usr/lib/myapp/. From your code, you rarely need to know the absolute path — the path API resolves it.
Adding Resources
The bundle.resources field in tauri.conf.json acts as a packing list. Everything you list there gets copied into the resource directory.
Basic configuration
Open src-tauri/tauri.conf.json and add a resources array inside the bundle object. Paths are relative to the src-tauri folder.
{
"productName": "MyApp",
"version": "0.1.0",
"bundle": {
"resources": [
"resources/**/*"
]
}
}
This tells the Tauri CLI to copy everything inside src-tauri/resources/ (and its subfolders) into the final resource directory. A typical project layout after adding the folder would look like:
src-tauri/
├── resources/
│ ├── fonts/
│ │ └── OpenSans-Regular.ttf
│ ├── config.json
│ └── seed.db
├── src/
│ └── main.rs
├── tauri.conf.json
└── Cargo.toml
After building, you can verify that the files made it by inspecting the output directory. For a debug build on Windows, look inside src-tauri/target/debug/; the resource folder will be visible there. If you ship an installer, the files are embedded and extracted to the installation directory.
Glob patterns and folder inclusion
You can list individual files, glob patterns, or entire directories. All of these are valid:
"resources": [
"resources/fonts/*.ttf",
"resources/config.json",
"resources/seeds/"
]
When you specify a directory like "resources/seeds/", Tauri includes its contents recursively. Avoid pointing to the entire resources folder if there are files you don't need — every extra megabyte adds to the installer size.
Platform-specific resources
Sometimes you need different resources per operating system. You can use Tauri's platform-specific configuration merging to include Windows-only DLLs or macOS-specific plist templates without cluttering other builds.
For example, add a font only on macOS by creating src-tauri/tauri.macos.conf.json:
{
"bundle": {
"resources": [
"resources/fonts/SF-Pro.ttf"
]
}
}
Tauri merges this with the base configuration, so the macOS build gets both the shared resources from the main config and the platform-specific ones. This approach avoids manual configuration juggling and keeps your file list clean.
Path base is always src-tauri:
No matter where you write the resource path — in the main config or a platform-specific one — it is always resolved relative to the src-tauri directory. Using an absolute path or ../ to point outside the project will cause build failures.
Accessing Resources
Knowing where resources live is half the battle; reading them at runtime is the other half. Tauri gives you two ways to get at resource files: directly from Rust using filesystem paths, and from the frontend using asset protocol URLs.
Reading resources from Rust
Inside your Rust backend, you resolve the resource path with app.path().resolve() and BaseDirectory::Resource. The resolved path is an absolute filesystem path that you can pass to standard std::fs functions.
Here's a complete example that reads a text resource and returns its content to the frontend:
use std::fs;
#[tauri::command]
fn read_config() -> Result<String, String> {
let resource_path = app_handle()
.path()
.resolve("resources/config.json", tauri::path::BaseDirectory::Resource)
.map_err(|e| e.to_string())?;
fs::read_to_string(&resource_path)
.map_err(|e| format!("Failed to read config: {}", e))
}
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![read_config])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
The resolve method joins the relative path you provide with the resource base directory for the current platform. If the resource file doesn't exist or permissions prevent reading, the function returns an error. Always handle that error gracefully — your app shouldn't crash because a font file is missing.
Everything is working if...:
After building a debug binary, run it and check that read_config returns your file's content without panicking. You can test by invoking the command from the frontend with @tauri-apps/api/core's invoke function and logging the result.
For binary resources like database files or images, use fs::read to get a Vec<u8>. If you need to stream large files, avoid reading the entire contents into memory and instead use std::fs::File and buffered reads.
Accessing resources from the frontend (React)
The webview can't directly open local file paths. You must convert a resource path into a URL that the webview can load, using the asset protocol. Tauri's JavaScript API provides convertFileSrc for this.
First, import the necessary functions in your component:
import { useEffect, useState } from "react";
import { convertFileSrc } from "@tauri-apps/api/core";
import { resourceDir } from "@tauri-apps/api/path";
function App() {
const [configText, setConfigText] = useState("");
useEffect(() => {
async function loadConfig() {
const resourcePath = await resourceDir();
const configFile = `${resourcePath}resources/config.json`;
const assetUrl = convertFileSrc(configFile);
try {
const response = await fetch(assetUrl);
const text = await response.text();
setConfigText(text);
} catch (error) {
console.error("Failed to load config:", error);
}
}
loadConfig();
}, []);
return <pre>{configText}</pre>;
}
export default App;
resourceDir() returns the absolute resource directory path for the current platform. You then join it with your relative resource path to get the full file path. convertFileSrc transforms this path into a URL using the asset:// or https://asset.localhost scheme, which the webview can fetch.
The same approach works for images, audio, or any file the browser can natively load. For an image:
const logoPath = `${resourcePath}resources/images/logo.png`;
const logoSrc = convertFileSrc(logoPath);
return <img src={logoSrc} alt="App logo" />;
This avoids needing a separate HTTP server to serve bundled assets. The asset protocol handles the translation transparently, so your <img>, <video>, or fetch calls work just like any other URL.
Asset URLs are platform-specific:
The exact URL scheme used by convertFileSrc differs between operating systems. On Windows, it may produce https://asset.localhost/path, while on mobile it's asset://localhost/path. Never hardcode these URLs — always use convertFileSrc.
Common Resource Types
Not all resources are the same. Some are read once at startup, others stream data, and a few need to be writable. The approach you choose depends on the file's role.
Configuration files
JSON, YAML, or TOML files that store app settings are the most common type. Read them once during app initialization and keep the parsed data in memory. If the config changes at runtime, write back to the resource file only if you intend to ship updated defaults in future builds — resources are typically read-only from the app's perspective. For user-modifiable settings, use the app data directory instead.
Example: loading a JSON config in Rust and exposing it via a Tauri command, as shown in the previous section.
Fonts
Shipping custom fonts ensures your app looks consistent across platforms. Place .ttf or .otf files in resources/fonts/ and load them from the frontend using CSS @font-face with a src pointing to the asset URL.
@font-face {
font-family: 'OpenSans';
src: url('asset://localhost/resources/fonts/OpenSans-Regular.ttf') format('truetype');
}
Because the asset URL varies, generate the CSS rule dynamically in your React component using convertFileSrc. Alternatively, copy fonts to the frontend public folder if you only need them for the webview, but this will increase the frontend bundle size.
Database seeds
If your app ships with a pre-populated SQLite database, put the .db file in resources and copy it to the app's data directory on first launch. The resource copy is read-only; the working copy lives where the user can write to it.
Rust code sketch:
use std::fs;
use tauri::path::BaseDirectory;
fn init_database(app: &tauri::AppHandle) -> Result<(), Box<dyn std::error::Error>> {
let seed_path = app.path().resolve("resources/seed.db", BaseDirectory::Resource)?;
let data_dir = app.path().app_data_dir()?;
fs::create_dir_all(&data_dir)?;
let db_path = data_dir.join("app.db");
if !db_path.exists() {
fs::copy(&seed_path, &db_path)?;
}
// Open db_path with your database library
Ok(())
}
This pattern ensures the user's modifications don't overwrite the original seed, and updates can be applied by comparing the seed version with the existing database.
Machine learning models
Large model files (.onnx, .tflite, etc.) belong in resources. Load them lazily to keep startup fast, and consider splitting models per platform to avoid bundling unnecessary variants. Use Rust's include_bytes! alternative only if the model is tiny; otherwise the resource path approach keeps memory usage predictable.
Resource Best Practices
A resources folder that balloons to hundreds of megabytes can hurt install size, startup time, and even crash mobile builds. The best-practices page expands on these patterns. Keeping resources lean and organized prevents these problems.
Organize with a single resources folder
Keep all resource files under src-tauri/resources/ and mirror the structure you would use in a regular application. This makes it obvious what gets bundled and avoids scattering files across the project.
Avoid hardcoded paths
Always use app.path().resolve() and resourceDir() instead of guessing the file location. Paths change between development, production, and platforms. Relying on relative paths like "./resources/config.json" will break when the working directory is not what you expect.
Be deliberate with what you include
Use precise glob patterns instead of blanket wildcards like "resources/**/*" if you have large files you don't need. Every file in resources adds to the final installer and extraction time. A tool like cargo bundle shows the total size of included resources; keep it under a reasonable threshold for your app's scope.
Test on all target platforms early
Resource handling differs subtly across operating systems. What works on your development machine might fail on Linux because of file permission issues, or on Android because the asset protocol implementation has known bugs.
Android resource access limitation:
As of Tauri v2, reading resource files on Android via the standard path().resolve() method can fail with a NotFound error. This is a known issue tracked at tauri#11823. If Android is a target for your app, test resource loading on a device or emulator early, and keep an eye on the issue for official fixes.
Use platform-specific configs wisely
When a resource is needed only on one platform, add it in that platform's config file instead of the main one. This reduces bundle size on other platforms and keeps your configuration explicit. For example, Windows-specific native dependencies should only be listed in tauri.windows.conf.json.
Consider alternatives for large, rarely-changing files
If a resource is enormous and changes infrequently, you might load it from an external server on first launch instead of bundling it. This keeps your installer small and allows updating the resource without shipping a new app version. The trade-off is that the user needs an internet connection and you must handle download failures gracefully.
When you've structured your resources following these patterns, you'll have a clean separation between your app's logic and its supporting data.
Understanding Resources
Learn what resources are in Tauri v2 - how they differ from assets, when to use them, and how to configure and access them in your React plus Vite app
Adding Resources
Configure your Tauri v2 app to bundle additional files—language JSON, images, data—that live outside the frontend directory
Accessing Resources
Learn how to locate and read bundled resource files from both Rust and React with platform-specific handling in Tauri v2.
Common Resource Types
A detailed guide to the different file types you can bundle as resources in Tauri v2 with React and Vite, and how to access each one correctly at runtime
Resource Best Practices
Organizing, naming, and accessing bundled resources in Tauri v2 applications with React and Vite, ensuring cross-platform compatibility and maintainability.