Adding Resources
Configure your Tauri v2 app to bundle additional files—language JSON, images, data—that live outside the frontend directory
When your Tauri app needs files that are not part of your React frontend or too large to inline into the Rust binary, you add them as resources. This page covers every syntax for the bundle.resources configuration, how to resolve the resulting paths at runtime, and how to actually read the files from both Rust and JavaScript. Executable helpers belong in external binaries instead.
Configuring bundle.resources
The bundle.resources field lives inside tauri.conf.json. It tells Tauri which extra files or directories to include in the final application bundle. The configuration accepts two notations: a simple list of paths and an object map that gives you full control over where each file ends up.
All source paths are relative to the src-tauri directory (the folder containing tauri.conf.json) unless they start with /, which makes them absolute. Paths that go outside the project root with .. are also allowed.
List notation
In this form, resources is an array of strings. Each entry can be a file, a directory, or a glob pattern. The destination path mirrors the original structure, with a few transformations for special prefixes.
{
"bundle": {
"resources": [
"./path/to/some-file.txt",
"/absolute/path/to/textfile.txt",
"../relative/path/to/jsonfile.json",
"some-folder/",
"resources/**/*.md"
]
}
}
Here is exactly where each item ends up, relative to the resource directory $RESOURCE:
"./path/to/some-file.txt"→$RESOURCE/path/to/some-file.txt
Relative paths are preserved as-is."/absolute/path/to/textfile.txt"→$RESOURCE/_root_/absolute/path/to/textfile.txt
An absolute path’s root becomes the folder_root_."../relative/path/to/jsonfile.json"→$RESOURCE/_up_/relative/path/to/jsonfile.json
Each..segment is turned into_up_."some-folder/"→$RESOURCE/some-folder/…
The entire directory tree insidesome-folderis copied recursively. The original folder name and its internal structure are preserved."resources/**/*.md"→$RESOURCE/resources/…
All.mdfiles undersrc-tauri/resources/are copied, keeping their subfolder layout.
Where is $RESOURCE?:
On Windows the resource directory lives next to the .exe, on macOS inside the .app bundle, and on Linux next to the binary. The Tauri APIs hide this detail—you never need to construct the path by hand.
Map notation
When you need to rename files or gather many source files into a single output folder, use the object notation. Each key is a source path and each value is the destination relative to $RESOURCE.
{
"bundle": {
"resources": {
"/absolute/path/to/textfile.txt": "resources/textfile.txt",
"relative/path/to/jsonfile.json": "resources/jsonfile.json",
"resources/": "",
"docs/**/*md": "website-docs/"
}
}
}
The placements with this configuration:
"resources/textfile.txt"— file is renamed and placed exactly at$RESOURCE/resources/textfile.txt."resources/jsonfile.json"— same idea, explicit target path."resources/"with target""— the entire directory is copied directly into$RESOURCE, without an extra wrapper folder. Soresources/config.jsonbecomes$RESOURCE/config.json."docs/**/*md": "website-docs/"— this is a crucial difference from the list notation. In map notation, glob patterns flatten the directory structure. All matching.mdfiles from any subdirectory ofdocs/land directly insidewebsite-docs/. For example:docs/index.md→$RESOURCE/website-docs/index.mddocs/plugins/setup.md→$RESOURCE/website-docs/setup.md
Glob flattening can surprise you:
If you meant to preserve the subfolder layout but used the map notation with a glob like "docs/**/*.md": "docs/", the entire internal structure disappears. Use the list notation when you want to keep the original hierarchy.
Source path reference
These are the exact matching rules for any single source entry, whether in a list or as a map key.
| Pattern | Behaviour |
|---|---|
"dir/file.txt" | Copies the single file. |
"dir/" | Copies all files and folders recursively, preserving the tree. Equivalent to "dir/**/*". |
"dir/*" | Copies only the files directly inside dir; subdirectories are skipped. |
"dir/**" | ❌ Error. ** matches directories only, so no files can be found. |
"dir/**/*" | Copies all files recursively. In list notation the structure is kept; in map notation files are flattened into the destination. |
Resolving the runtime path
Bundling is half the story. At runtime you need the actual filesystem path to open a resource file. Tauri gives you a path resolver on both the Rust and JavaScript sides, and the string you pass to it follows the same rules as the config entries.
use tauri::Manager;
// Inside setup or a command
let resource_path = app.path()
.resolve("lang/de.json", tauri::path::BaseDirectory::Resource)?;
// On desktop: a normal PathBuf
// On Android: an asset:// URI
The string "lang/de.json" here corresponds to a source entry like "lang/*" or "lang/de.json" in your resources config. Relative paths work exactly as they do in the config, so a resource bundled via "../data/info.txt" must be resolved with the path "../data/info.txt".
Matching paths exactly:
If your config says "some-folder/" and you want the file some-folder/config.json, pass "some-folder/config.json" to the resolver. The _root_ and _up_ transformations happen automatically; you don't add those by hand.
Reading a bundled resource end-to-end
A concrete example ties everything together. Suppose you ship internationalisation files:
src-tauri/
├── tauri.conf.json
├── lang/
│ ├── de.json
│ └── en.json
└── src/
└── main.rs
1. Declare the resources in tauri.conf.json:
{
"bundle": {
"resources": ["lang/*"]
}
}
2. Create a Tauri command that reads a resource and returns its contents.
This is the bridge that lets your React frontend consume the file.
#[tauri::command]
fn read_resource(app: tauri::AppHandle, relative_path: String) -> Result<String, String> {
let path = app
.path()
.resolve(&relative_path, tauri::path::BaseDirectory::Resource)
.map_err(|e| e.to_string())?;
// On desktop we use standard file I/O; on Android we use Tauri's fs plugin
#[cfg(not(target_os = "android"))]
{
std::fs::read_to_string(&path).map_err(|e| e.to_string())
}
#[cfg(target_os = "android")]
{
app.fs().read_to_string(&path).map_err(|e| e.to_string())
}
}
3. Call the command from React:
import { invoke } from '@tauri-apps/api/core';
async function loadGermanStrings() {
const raw = await invoke<string>('read_resource', {
relativePath: 'lang/de.json',
});
const strings = JSON.parse(raw);
console.log(strings.hello); // "Guten Tag!"
}
Resources are not served to the webview:
The frontend cannot fetch resources with a regular HTTP call. Files are on the native filesystem, invisible to the browser. Always use a Tauri command to shuttle the data across.
Android: asset URIs, not file paths
On Android resources are stored inside the APK’s assets directory. The resolved path is an asset://localhost/... URI, not a regular filesystem path. That is why the code above uses app.fs().read_to_string() instead of std::fs. If your app needs a real file on disk (for example, to pass to a native library), copy the resource to the app’s data folder with the fs plugin.
Crash risk on Android:
Using std::fs::read_to_string on an asset:// URI will fail at runtime. Always guard Android file I/O with conditional compilation or use the fs plugin.
Permission requirements
Resource resolution from JavaScript requires the resources:default permission. Without it the API call will be denied.
Add this to your capability file (e.g., src-tauri/capabilities/default.json):
{
"permissions": [
"resources:default"
]
}
If you also plan to resolve paths from the frontend with resolveResource, include path:default in the same permission list.
Common mistakes
- Using
"folder/**"alone — this pattern matches zero files because**only stands for directories. Always write"folder/**/*". - Forgetting to add a trailing slash to a directory path —
"resources"without/may be interpreted as a file reference and silently skipped. Write"resources/"to copy the folder. - Map‑notation glob flattening — as described earlier, this is intentional but often unexpected. If you need the original subfolder layout, switch to the list notation.
- Embedding very large files directly in the binary — if a file is several megabytes or more, use resources instead of
include_bytes!. Large binary embeds can cause out‑of‑memory build failures. - Missing
resources:defaultpermission — the frontend API simply won’t work, and the error might only appear as a generic “not allowed” message.
Verify your bundle:
After running tauri build, inspect the generated package (or the target directory) to confirm your resource files are present where you expect them. A quick manual check saves debugging time.
Summary
You add resources by listing file paths, directories, or globs in bundle.resources, either as a flat array or as a source‑destination map. At runtime you resolve the path with PathResolver::resolve (Rust) or resolveResource (JavaScript), then read the file using native filesystem APIs—or the fs plugin on Android. Keeping the config rules straight and handling Android separately prevents the most frequent pitfalls.