Two-Part Project Structure

Understand why a Tauri app consists of a frontend web project and a Rust backend project, how they fit together, and what this separation means for development, builds, and security.

A Tauri project is not a single codebase. It is two independent projects that live side by side in the same directory tree. One handles the user interface — a standard web project with HTML, CSS, and JavaScript (or any framework that compiles to those). The other is a Rust project that acts as the native backend, managing system access, file I/O, and the window itself. Understanding this split early prevents confusion about where code belongs, how builds work, and why the security model is structured the way it is.

Why Two Separate Projects?

The split exists because the frontend and the backend have completely different responsibilities, runtimes, and trust levels. The frontend runs inside a system WebView — a sandboxed browser environment that renders your UI. The backend runs as native Rust code with full access to the operating system. Keeping them as separate projects enforces a clear security boundary: the WebView cannot touch the file system, spawn processes, or call system APIs unless the Rust backend explicitly allows it through Tauri’s command system.

This architecture also means you can use any frontend toolchain you want — React, Vue, Svelte, vanilla HTML, even a Rust-based web framework like Yew or Leptos — without changing how the Rust side works. The two projects are only connected during the build phase and at runtime through a well-defined inter-process communication (IPC) bridge.

Separation is a security feature:

The WebView is treated as an untrusted environment. Every interaction between the frontend and the operating system must go through the Rust core. This is not a limitation — it is the foundation that lets Tauri apps be small, fast, and safe by default.

Where the Two Projects Live

The default scaffold places the frontend project at the root of your workspace. The Rust project sits inside a folder named src-tauri/. This is what create-tauri-app produces for a typical TypeScript + React setup:

my-tauri-app/
├── index.html
├── package.json
├── src/                 # Frontend source (React, Vue, vanilla JS, etc.)
│   └── main.tsx
├── tsconfig.json
├── vite.config.ts
└── src-tauri/           # Rust backend project
    ├── Cargo.toml
    ├── build.rs
    ├── tauri.conf.json
    ├── capabilities/
    │   └── default.json
    ├── icons/
    │   ├── icon.png
    │   ├── icon.icns
    │   └── icon.ico
    └── src/
        ├── main.rs
        └── lib.rs

The frontend is just a normal Vite project (or whatever bundler you chose). It has a package.json with scripts for dev, build, and a devDependency on @tauri-apps/cli. The @tauri-apps/api package is installed as a dependency so the frontend can call Rust commands.

The src-tauri/ folder is a fully independent Cargo project. It has its own Cargo.toml, its own build script (build.rs), and the Tauri-specific configuration file tauri.conf.json. Tauri’s CLI uses the presence of tauri.conf.json to locate the Rust project — that file is the marker that tells tauri dev and tauri build where to find the backend.

How the Two Parts Work Together

During development, when you run tauri dev, two things happen in sequence. First, the frontend dev server starts — this is your normal vite dev, next dev, or whatever command is defined in tauri.conf.json under build.devUrl. Then, the Tauri CLI compiles the Rust backend and opens a native window that loads the URL of that dev server. The Rust code is running natively; the frontend is served from the dev server. This gives you hot module reloading for the UI and full access to the Rust backend in the same window.

Confirm your dev setup:

If you see your frontend in a native window and can call a Rust command from the JavaScript console (try window.__TAURI__), both projects are correctly connected.

For a production build, the order is reversed. tauri build first runs the frontend build command (e.g., vite build), which produces a folder of static HTML, CSS, and JavaScript files. Then the Rust project compiles, and Tauri’s build process embeds those static files directly into the final binary. The shipped application contains no dev server — it serves the pre-built frontend assets from memory.

This two-step build is why the frontend and backend can use entirely different package managers and toolchains. They are only linked at the file-output level.

The Rust Project’s Internal Split (lib.rs and main.rs)

Inside src-tauri/src/ you will see two Rust files: main.rs and lib.rs. This is not accidental duplication. Tauri targets both desktop and mobile platforms, and the entry point is different for each. On desktop, the operating system launches a binary, so execution starts at main.rs. On mobile, the platform loads a library, so the Rust code must be compiled as a library and initialized through a different function.

To keep both paths consistent, main.rs contains only a thin wrapper:

src-tauri/src/main.rs
// Prevents an additional console window on Windows in release builds.
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
    app_lib::run();
}

It calls app_lib::run(). The name app_lib comes from the [lib] section in Cargo.toml, specifically the name field. All application logic — command definitions, plugin registrations, the tauri::Builder setup — lives in lib.rs.

src-tauri/src/lib.rs
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

The #[cfg_attr(mobile, tauri::mobile_entry_point)] attribute ensures that on mobile targets, run() is exposed as the entry point that the platform framework calls. On desktop, run() is a regular public function called from main.rs.

Do not write application logic in main.rs:

Adding commands or initialization code to main.rs will work on desktop but silently fail on mobile builds because that code never executes in a library context. Always write your Rust application logic inside lib.rs.

What Is Required in Each Part

The frontend project needs very little from Tauri’s perspective. It only has to:

  • Serve content at the URL defined in tauri.conf.jsonbuild.devUrl during development.
  • Produce static files at the path defined in tauri.conf.jsonbuild.frontendDist during the production build.
  • Optionally include @tauri-apps/api to call Rust commands.

The Rust project has a few mandatory pieces:

  • tauri.conf.json — marks the folder as the Tauri project and contains the application identifier, window configuration, build commands, and security settings.
  • build.rs — must contain tauri_build::build(). This script generates code that helps Tauri’s runtime locate assets and configurations at compile time.
  • capabilities/ — JSON files that declare which commands and permissions the frontend is allowed to use. Without a matching capability entry, even a correctly written Rust command will be blocked at runtime.
  • icons/ — application icon files referenced in the bundle configuration.

When There Is No Frontend Project

While the two-part structure is the default, Tauri also supports a Rust-only mode. If you remove all frontend files and treat src-tauri/ as the top-level project (or include it as a member of a larger Rust workspace), you can build an application with no web UI at all. The window still opens, but it displays whatever minimal HTML you generate from Rust — or you can use the window handle to draw with a graphics library.

This approach is less common but useful for command-line tools, system trays, or applications that render their UI entirely through a Rust-based web framework like Yew or Leptos. In that setup, the frontend and the backend are still two parts conceptually, but both are written in Rust and compiled together, with the WebView loading generated content instead of external static files.

Two-part structure is not always visible in the file tree:

Even when both parts are in Rust, Tauri still separates the UI layer (running in the WebView) from the core layer (running natively). The two-part trust model and IPC boundary remain identical.

Common Mistakes When Working with the Two Parts

  • Editing main.rs instead of lib.rs — commands and setup logic placed in main.rs will not execute on mobile. Always use lib.rs as the application’s real entry point.
  • Running only the frontend dev servernpm run dev starts your Vite/Next server, but that gives you a browser tab, not a native window. You must use tauri dev to compile the Rust backend and create the Tauri window. The browser tab cannot call Rust commands.
  • Omitting capability declarations — writing a #[tauri::command] function does not automatically expose it to the frontend. You must add the command’s name to a capabilities file in src-tauri/capabilities/. If the command is blocked, the frontend receives a permission error with no fallback.
  • Mismatched dev URLs — if the frontend’s dev server port changes (e.g., Vite moves from 5173 to 5174), the devUrl in tauri.conf.json must be updated, or the window will show a blank page. Tauri does not auto-detect the port.

Summary

A Tauri application is the marriage of two separate but cooperating projects: a frontend web project that produces the UI, and a Rust project that provides native capabilities. This separation is what keeps the build process portable, the security model enforceable, and the developer experience modular. The frontend can be swapped or rewritten in any framework without touching the Rust side; the Rust backend can be extended with new commands without altering the frontend’s tooling. The Rust project’s internal split between main.rs and lib.rs ensures the same code runs on both desktop and mobile without duplication.

Understanding this layout prepares you for everything that follows.