TNTStack
Architecture

Native App

Tauri desktop and mobile application. Next.js frontend with Rust backend.

A Next.js frontend statically exported into a Tauri 2 webview with a Rust backend. Produces desktop binaries for Windows, macOS, and Linux, plus mobile builds for Android and iOS.

Directory Structure

tauri.conf.json
Cargo.toml
build.rs
next.config.ts
package.json

Architecture

  1. Next.js pages are statically exported to dist/
  2. Tauri loads the static files into the OS-native webview
  3. Frontend calls Rust functions via invoke() over IPC
  4. Rust commands return JSON to the webview

Tauri Configuration

src-tauri/tauri.conf.json
{
  "$schema": "https://schema.tauri.app/config/2",
  "productName": "tntstack",
  "version": "x.x.x",
  "identifier": "com.tntstack.app",
  "build": {
    "beforeDevCommand": "pnpm dev",
    "devUrl": "http://localhost:3000",
    "beforeBuildCommand": "pnpm build",
    "frontendDist": "../dist"
  },
  "app": {
    "windows": [{ "title": "tntstack", "width": 800, "height": 600 }],
    "security": { "csp": null }
  },
  "bundle": {
    "active": true,
    "targets": "all",
    "icon": [
      "icons/32x32.png",
      "icons/128x128.png",
      "icons/128x128@2x.png",
      "icons/icon.icns",
      "icons/icon.ico"
    ]
  }
}

In dev mode, Tauri runs pnpm dev and points the webview at localhost:3000. In production, it runs pnpm build and loads static files from dist/.

Rust Backend

src-tauri/src/lib.rs
use serde::Serialize;

#[derive(Serialize)]
struct GreetResponse {
    message_key: String,
    name: String,
    source: String,
}

#[tauri::command]
fn greet(name: &str) -> GreetResponse {
    GreetResponse {
        message_key: "successGreeting".to_string(),
        name: name.to_string(),
        source: "Tauri".to_string(),
    }
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
  tauri::Builder::default()
    .plugin(tauri_plugin_opener::init())
    .invoke_handler(tauri::generate_handler![greet])
    .run(tauri::generate_context!())
    .expect("error while running tauri application");
}

The greet command returns a GreetResponse struct with a message_key field. The frontend resolves this key to a translated string via next-intl. Register plugins with .plugin() and IPC commands with invoke_handler. The mobile_entry_point attribute makes the same entry point work on both desktop and mobile.

src-tauri/src/main.rs
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

fn main() {
  tntstack_lib::run()
}

The crate name comes from Cargo.toml's [lib] name field. After scaffolding with the CLI, this will match your project name.

CrateRole
tauriCore runtime (with devtools feature)
tauri-plugin-openerOpen URLs and files with the OS default
serde / serde_jsonJSON serialization for IPC
tauri-buildBuild-time code generation

The custom-protocol feature flag switches between dev server and static file loading in production builds.

Capabilities

Tauri 2 gates every API behind explicit permissions per window:

src-tauri/capabilities/default.json
{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "enables the default permissions",
  "windows": ["main"],
  "permissions": ["core:default", "opener:default"]
}
PermissionWhat it grants
core:defaultWindow, event, and app lifecycle operations
opener:defaultOpen URLs and files with system apps

Adding a new Tauri plugin? You'll need to register it in lib.rs and add its permission here.

Next.js Configuration

next.config.ts
import type { NextConfig } from "next";
import createNextIntlPlugin from "@workspace/i18n/plugin";

const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts");

const nextConfig: NextConfig = {
  reactStrictMode: true,
  output: "export",
  trailingSlash: true,
  images: {
    unoptimized: true,
  },
  distDir: "dist",
  transpilePackages: ["@workspace/ui", "@workspace/core", "@workspace/i18n"],
};

export default withNextIntl(nextConfig);

Native vs Web

Native AppWeb App
OutputStatic export (dist/)Server-rendered
Imagesunoptimized: trueServer-optimized
API RoutesNot availableYes
SSR / ISRNot availableYes
i18n plugincreateNextIntlPlugin("./src/i18n/request.ts")createNextIntlPlugin()
Extra pluginsNonewithMDX, withSerwist
Server logicRust commands via IPCAPI routes

Static export means no API routes, no SSR, no ISR. If you need server-side logic in the native app, put it in Rust commands instead.

On this page