Public Directory
How to place and reference static assets in a Tauri v2 application using the public directory of a Vite and React frontend
Most web applications need static files — images, fonts, JSON data, robots.txt, or a favicon. The public directory is the simplest place to store these files in a Vite + React project, and Tauri automatically makes them available inside your desktop app window. Files the Rust backend must read as separate disk files belong in resources instead.
What the Public Directory Is
In a Vite project, the public folder sits at the project root, next to src. Any file you place there gets copied as-is into the final build output. There is no processing, no hashing, no module transformation — the file keeps its name and its raw content.
For example, if your project looks like this:
my-tauri-app/
├── public/
│ └── logo.png
├── src/
│ ├── App.tsx
│ └── main.tsx
└── ...
Then after a build, the dist folder (which Tauri uses as the frontend source) will contain logo.png at the root. You can refer to it directly from your HTML or JavaScript.
Vite docs term:
Vite calls this the "public directory" and documents its behavior here. Tauri inherits that behavior because Tauri simply serves the output of your frontend build tool.
How the Public Directory Works with Vite
When you run npm run build, Vite compiles your React code and places the output into ../dist by default (the Tauri convention). During that process, it also copies the entire public folder into the root of dist. No renaming, no fingerprinting — just a direct copy.
Because of this, the public directory is best for files that:
- Must keep a predictable name (like
favicon.icoormanifest.json). - Are referenced by a hard‑coded path in your HTML or CSS (like
<img src="/logo.png" />). - Don’t need versioning or cache busting (for an installed desktop app, cache busting is less important than in a web app).
If you need hashed filenames and automatic import resolution, you import the asset directly into a JavaScript module instead of putting it in public.
Referencing Assets in Your React Frontend
Assets in the public directory are served at the root path of your application. To use them in a component, you write an absolute path starting with /.
Place a file public/banner.jpg. Then, in your React component:
// src/App.tsx
function App() {
return (
<div>
<img src="/banner.jpg" alt="App banner" />
</div>
);
}
export default App;
During development (npm run dev), Vite’s dev server serves public files at the root, so /banner.jpg works immediately. Inside the Tauri window, the custom protocol serves the same path from the embedded frontend assets — so no URL changes are needed between dev and production.
Consistent URLs:
Because you always access public assets with a root‑relative path like /image.png, the same code works in the browser dev server and inside the final Tauri app window. There’s nothing platform‑specific to adjust.
For files in subdirectories, use the same root‑relative path. If you have public/data/config.json, access it with fetch('/data/config.json') or reference it as /data/config.json.
How Tauri Serves Public Directory Assets
When you ship a Tauri application, all frontend files — including everything from the public directory — become embedded inside the final executable. Tauri does not rely on a local file server or a file:// URL; it uses its own custom protocol to serve these assets securely.
The default protocol is tauri://localhost (though you rarely need to type it). A request to /logo.png inside your webview gets routed through this protocol and resolved against the embedded dist folder content. That means:
- No extra network calls.
- No need to configure a local server.
- The assets are available even when the user is offline.
In tauri.conf.json, the app.security.assetProtocol setting controls the scope of this protocol. The default configuration allows access to the entire distDir, which includes your public files. You can tighten the scope if you need to restrict which paths are reachable, but for typical use, the default works without any changes.
// src-tauri/tauri.conf.json (partial)
{
"app": {
"security": {
"assetProtocol": {
"scope": ["$APPDATA/**"]
}
}
}
}
The example above shows an explicit scope, but by default the scope includes the frontend assets directory, so public files are covered.
Build Output and Distribution
When you run npm run tauri build, the following happens in order:
- Vite builds your React frontend and outputs files to
../dist(thebuild.distDirconfigured intauri.conf.json). - Vite copies the entire
publicfolder into the root of../dist. - Tauri’s Rust build tooling embeds all files inside
../distinto the final binary as resources.
The result is a single executable (or an installer) that contains every public asset. There is no separate public folder that the user needs to keep next to the .exe — the images, fonts, and JSON files are already baked in.
Large files can break the build:
Embedding a few hundred megabytes of video or a large database in the public directory will cause the Rust compiler to run out of memory during the build. Tauri is not designed to bundle gigabytes of media as embedded assets. For that, use the Resources feature described in the Resources section of this guide.
Cache Considerations
Because Tauri serves assets through a custom protocol rather than HTTP, the usual browser caching headers (Cache-Control, ETag) don’t apply automatically. However, the WebView engine may still cache network responses in memory or on disk depending on the platform.
For an installed desktop application, this is rarely a problem:
- The entire frontend is embedded, so the webview never re-downloads it from a remote server. A restart of the app reloads everything from the binary.
- Development with
tauri devuses Vite’s dev server with hot module replacement, so stale public assets are only a concern if you manually replace a file while the server is running. Restarting the dev server fixes any caching inconsistency.
If you do want to guarantee a fresh load for a specific public asset (for example, a JSON config file that changes between releases), you can append a version query string:
const configUrl = `/data/config.json?v=${APP_VERSION}`;
const response = await fetch(configUrl);
const config = await response.json();
Because the asset protocol doesn’t parse query strings as cache busters in a standard HTTP way, this is more of a logical convention than a technical guarantee. In practice, a version query string will still bypass any WebView in‑memory cache because the URL string is different.
When Not to Use the Public Directory
The public directory is ideal for icons, small JSON payloads, fonts, and other lightweight static files. It is not the right place for:
- Files that should not be accessible to the frontend at all.
- Sensitive configuration that must remain hidden from the user (the user can inspect any file served to the webview).
- Large media files (videos, high‑resolution images, databases) that would inflate the binary size or cause build failures.
No secrets in public:
Any file placed in the public directory is, as the name suggests, publicly accessible to any code running in the webview. An attacker with access to the DevTools (or a user who deliberately inspects the app bundle) can read its contents. Never put API keys, tokens, or sensitive credentials in the public folder.
For large files, Tauri provides a separate Resources mechanism that bundles files alongside the binary without embedding them directly into the executable. For files that need to stay completely outside the frontend’s reach, you can load them from the Rust backend and expose only what is necessary to the frontend.
Practical Example: Adding a Logo and a JSON Dataset
Let’s walk through a small, complete example that uses two public assets: an image and a static JSON file.
1. Add the files to the public directory
my-tauri-app/
├── public/
│ ├── logo.svg
│ └── data/
│ └── categories.json
The JSON file could be a simple category list:
// public/data/categories.json
[
{ "id": 1, "name": "Electronics" },
{ "id": 2, "name": "Books" }
]
2. Reference the logo in your main component
// src/App.tsx
import { useEffect, useState } from 'react';
interface Category {
id: number;
name: string;
}
function App() {
const [categories, setCategories] = useState<Category[]>([]);
useEffect(() => {
fetch('/data/categories.json')
.then((res) => res.json())
.then((data) => setCategories(data));
}, []);
return (
<main style={{ padding: '2rem' }}>
<img src="/logo.svg" alt="Logo" width="120" />
<h1>Categories</h1>
<ul>
{categories.map((cat) => (
<li key={cat.id}>{cat.name}</li>
))}
</ul>
</main>
);
}
export default App;
3. Run the app
During development:
npm run tauri dev
The logo and the JSON data load as expected because Vite serves the public folder at the root. No special Tauri configuration is needed.
When you build for production (npm run tauri build), the same paths work out of the box because the entire dist folder — including the copied public files — becomes embedded in the final binary.
Everything works identically:
If your app shows the logo and the category list both in development and after installation, your public directory setup is correct. You don’t need to change a single line of code when moving from dev to production.
Summary
The public directory is the simplest on‑ramp for static assets in a Tauri + Vite + React application. It mirrors familiar web development patterns: drop a file in public, reference it with an absolute path, and it works. Tauri embeds these files directly into your app binary, so they are always available offline and require no additional server configuration.
For files that change rarely and don’t need import‑based processing (hashing, optimization), public is the right home. When you outgrow it — because files become too large or need access control — Tauri’s Resources system and Rust‑side file loading give you the next level of flexibility without leaving the platform.