Database Connection in the Tauri SQL Plugin

Step-by-step guide to connecting your Tauri v2 app to SQLite, MySQL, or PostgreSQL databases using the official SQL plugin.

The frontend of a Tauri application lives inside a webview and cannot directly open a raw TCP connection or access a local file database. The SQL plugin hands your React code a clean JavaScript API that tunnels through Rust to the database, managing connection pools, authentication, and lifecycles behind the scenes. This document focuses entirely on that handshake: what you need to install, how to configure the driver, the exact format of connection strings, and the runtime methods that give you a ready‑to‑use database handle.

Plugin Setup

Before any connection can be opened, the plugin must be present in your Rust backend and available to your frontend. If you have not installed the plugin at all, run the automatic setup command from your project root, or follow the SQL Plugin introduction:

npm run tauri add sql

This adds the Rust crate and the JavaScript bindings in one step. If you prefer manual control, or need to target a specific version, the steps below lay out each piece individually.

Rust Side

Add the crate to src-tauri/Cargo.toml. You must also enable at least one database feature — the connection code will not compile without it.

cargo add tauri-plugin-sql --features sqlite

Next, register the plugin in your Tauri builder. Open src-tauri/src/lib.rs (or main.rs depending on your template) and add the plugin initialization:

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

Missing Feature Flag:

If you forget the --features sqlite (or mysql, postgres) flag, the compiler will not know which SQL driver to bundle. You will see errors about unresolved imports inside tauri-plugin-sql. Always pick at least one feature.

Frontend Bindings

Install the JavaScript package that your React code will import:

npm install @tauri-apps/plugin-sql

Tauri CLI manages versions:

When you used npm run tauri add sql, both the Rust crate and the npm package were added with compatible versions. Manual installation requires you to ensure the JS bindings version matches the plugin version in Cargo.toml.

Permissions

The plugin’s commands are locked by default. Modify src-tauri/capabilities/default.json to grant the permissions needed for establishing a connection:

src-tauri/capabilities/default.json
{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "Capability for the main window",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "sql:allow-load",
    "sql:allow-close"
  ]
}
  • sql:allow-load permits calling Database.load() or Database.get().
  • sql:allow-close permits closing the connection pool. These are part of the sql:default permission set, so adding "sql:default" automatically includes both. If you later need execute or select, add sql:allow-execute and sql:allow-select. Plugin permissions covers how those identifiers are granted per window.

The Database Object

Every interaction with a database starts by obtaining a Database instance. The plugin exposes two static methods for this: Database.load() and Database.get().

load()

load() returns a promise that resolves once the connection pool is established and any preloaded migrations have run. You should use this when you need the database to be fully ready before the application proceeds.

import Database from '@tauri-apps/plugin-sql';

const db = await Database.load('sqlite:test.db');

If the connection fails (invalid path, authentication error), the promise rejects. Always wrap it in a try/catch.

get()

get() is synchronous. It returns a Database object immediately, but the actual connection is deferred until the first query runs. This is useful when you want to set up the database handle early without blocking the UI thread on a connection handshake.

import Database from '@tauri-apps/plugin-sql';

const db = Database.get('sqlite:test.db');
// No connection yet. It will open when you run db.select(...) or db.execute(...).

Deferred errors are harder to trace:

With get(), an invalid connection string will not throw at the call site. The error surfaces later, during the first query. For early validation, prefer load().

close()

Once you are done with a database, call close() to shut down the connection pool and free resources. The method returns Promise<boolean> indicating success.

await db.close();

If you manage multiple database connections (e.g., a sqlite:app.db and a mysql://...), passing the exact connection string to close() will target only that pool. Omitting the argument closes all open pools.

Connection Strings

The format of the connection string determines which driver is used and where the database lives.

const db = await Database.load('sqlite:myapp.db');

The path after sqlite: is relative to tauri::api::path::BaseDirectory::AppConfig.
During development (tauri dev), this resolves to the src-tauri directory. In a packaged app, it points to the platform‑specific application configuration folder. A leading ./ is not required — sqlite:test.db and sqlite:./test.db are equivalent.

SQLite path resolution can surprise you:

A common mistake is to place the SQLite file inside your React src folder and use a relative path like sqlite:../src/mydb.db. The root is always the app config directory, not your frontend source tree. If the file is missing, SQLite will create an empty database at that location, leading to "no such table" errors later.

Preloading a Connection with Migrations

Sometimes you want the database to be ready before the first line of React code executes — with the schema already applied. The plugin can preload a connection on startup and run Rust‑defined migrations against it. This involves three pieces: a migration definition, a builder call, and a configuration entry.

1

Step 1: Define migrations in Rust

Create one or more Migration structs. Each migration has a version number, a description, the SQL to execute, and a direction (Up for forward migrations, Down for rollbacks).

src-tauri/src/lib.rs
use tauri_plugin_sql::{Migration, MigrationKind};

let migrations = vec![
    Migration {
        version: 1,
        description: "create users table",
        sql: "CREATE TABLE IF NOT EXISTS users (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            name TEXT NOT NULL
        );",
        kind: MigrationKind::Up,
    },
];
2

Step 2: Attach migrations to the plugin builder

Use add_migrations() to associate the migrations with a specific connection string.

src-tauri/src/lib.rs
.plugin(
    tauri_plugin_sql::Builder::default()
        .add_migrations("sqlite:myapp.db", migrations)
        .build(),
)

This call must come before .build().

3

Step 3: Declare the connection in tauri.conf.json

List the connection string in the plugins.sql.preload array so Tauri opens it at startup and runs all attached migrations.

src-tauri/tauri.conf.json
{
  "plugins": {
    "sql": {
      "preload": ["sqlite:myapp.db"]
    }
  }
}

The preloaded database is available immediately — you can still call Database.load('sqlite:myapp.db') on the frontend; it will reuse the already‑open pool.

Migrations are idempotent and version‑controlled internally. Running the same migration twice does not duplicate the table creation.

Building a Connection Component in React

The following React component demonstrates a minimal, production‑ready connection pattern. It loads the database on mount, catches errors, and shows the user whether the connection succeeded.

src/App.tsx
import { useEffect, useState } from 'react';
import Database from '@tauri-apps/plugin-sql';

function App() {
  const [status, setStatus] = useState<'loading' | 'connected' | 'error'>('loading');

  useEffect(() => {
    let db: Database | null = null;

    async function connect() {
      try {
        db = await Database.load('sqlite:test.db');
        setStatus('connected');
      } catch (err) {
        console.error('Database connection failed:', err);
        setStatus('error');
      }
    }

    connect();

    return () => {
      // Close the pool when the component unmounts
      if (db) db.close();
    };
  }, []);

  return (
    <main style={{ padding: '2rem', fontFamily: 'system-ui' }}>
      <h1>Tauri SQL Connection Demo</h1>
      {status === 'loading' && <p>Connecting to database…</p>}
      {status === 'connected' && <p style={{ color: 'green' }}>✅ Database connected.</p>}
      {status === 'error' && (
        <p style={{ color: 'red' }}>
          ❌ Failed to connect. Check the developer console for details.
        </p>
      )}
    </main>
  );
}

export default App;

The connection only happens once, inside useEffect. The cleanup function closes the database pool when the component unmounts, preventing lingering connections. In a larger application you would likely lift the database handle into a React context, but the core flow is identical.

Confirmation:

If your screen shows “✅ Database connected”, the plugin is correctly installed, the feature flag matches your connection string, and the permissions allow the load command.

Troubleshooting Common Connection Issues

No such file or directory (SQLite)

If you see a Rust‑side error about being unable to open a file, check the path. Remember: the root is BaseDirectory::AppConfig, not your project folder. Use a simple name like sqlite:myapp.db and let the plugin create it if it doesn’t exist.

Connection refused (MySQL / PostgreSQL)

Verify that the database server is running and reachable from your machine. The connection string must include the correct host and port. For local development, localhost and default ports are assumed, but a firewall or Docker binding can block access.

Authentication failed

Double‑check the username and password in the connection string. Special characters should be URL‑encoded (e.g., %40 for @).

Plugin not initialized

An error like plugin not initialized means the Tauri builder never called .plugin(tauri_plugin_sql::Builder::default().build()). Ensure the code in lib.rs is exactly as shown.

Permissions block the load command:

If Database.load() hangs or immediately throws a permission error, open src-tauri/capabilities/default.json and confirm that sql:allow-load is listed. Without it, the frontend cannot invoke the backend command that opens the connection.

Summary

You now have a Database handle that represents an open, managed connection to a SQL database — fully initialized from your React code. The connection string selects the driver, load() or get() provides the handle, and migrations pre‑wire your schema.