Building Desktop Applications with Tauri, Nextjs, Firebase

·

7 min read

Featured on Hashnode

TL;DR

In this article, I will compare Tauri and Electron and explore the possible challenges when applying Tauri to real-world projects, especially when integrating with Firebase.

Background

I'm working on a whiteboard product at https://doodleboard.pro and trying to make it work on different devices, such as phones and iPads. I've been exploring various options because I don't want to force users to install yet another browser by using Electron, which is the most established choice out there. Here are some of the other options I've looked at:

And DoodleBoard is built on Next.js and Firebase, so I need to figure out how to integrate these technologies together.

Why choose Tauri?

Image description

I had to pick an option that would work for me, and after weighing my options, I landed on Tauri.

Electron is cool and all, but it's a biggie in terms of package size, plus it's not mobile-friendly. I needed something that could run on multiple platforms and not make users install more browsers.

Flutter is a well-known framework, but using it for a whiteboard and text editor would be a massive undertaking. Plus, it can't reuse web code, which is a no-go for me.

React Native is another popular choice, but reusing the code is only possible for some parts of the project. And, let's face it, implementing a whiteboard using React Native would be quite a challenge.

Wails and Tauri have very similar architecture. Compare to Wails, Tauri has a more active and experienced community. I figured I'd go with the more established option to avoid unnecessary headaches. Also, Wails doesn't work on mobile devices.

Tauri could reuse almost all of my existing code and offered a solution for multiple platforms. Plus, the package size is nice and small. So, that's why I went with Tauri.

What critical problems need to be solved to integrate Firebase + Next.js with Tauri?

Tauri's hello world demo is too simple, but there may be various issues when applied to real projects. The core of this article is to sort out these issues and share them with the community.

  1. How Tauri work?

  2. How to solve CORS network request issues in Tauri

  3. How to compile Tauri and Next.js into one project?

  4. How to reuse existing code as much as possible?

  5. How to add Deeplink to your Tauri app?

  6. How to integrate Firebase Auth in Tauri?

How Tauri work?

To better tackle real problems,we must understand the underlying architecture of Tauri. Tauri essentially uses the WRY library to wrap the native browser of the operating system, providing a convenient interface layer for the browser and Rust to call each other.

Therefore, our frontend code runs in the native browser, while our backend code runs in the Rust layer provided by Tauri. This is also why Tauri is fast and has a small footprint.

Below is the architecture diagram I have mapped out for Tauri:

Image description

How to solve CORS network request issues in Tauri?

Because the frontend code in Tauri runs in a browser, it will encounter the CORS problem when making network requests. The following is a simplified diagram of the structure. Since the frontend code runs in an environment with a special domain name, requesting https://your-backend.com will result in a cross-origin problem.

Image description

So how can we solve this problem? There are two ways:

Solution One:

To configure CORS in your own API request, I use Next.js, so you only need to add CORS in your own handler.

Image description

// https://www.npmjs.com/package/nextjs-cors 
import NextCors from 'nextjs-cors';

async function handler(req, res) {
   // Run the cors middleware
   // nextjs-cors uses the cors package, so we invite you to check the documentation https://github.com/expressjs/cors
   await NextCors(req, res, {
      // Options
      methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'],
      origin: '*',
      optionsSuccessStatus: 200, // some legacy browsers (IE11, various SmartTVs) choke on 204
   });

   // Rest of the API logic
   res.json({ message: 'Hello NextJs Cors!' });
}

Solution Two:

In some cases, CORS cannot be modified, so you can also use the Fetch library provided by Tauri Runtime layer. This means the request is sent through the Rust backend. Here's how to do it:

Image description

Configure tauri.conf.json to support the corresponding API.

{
  "tauri": {
    "allowlist": {
      "http": {
        "all": true, // enable all http APIs
        "request": true // enable HTTP request API
                "scope": ["https://api.github.com/repos/tauri-apps/*"]
      }
    }
  }
}

You can also add * in scope to allow any request. It's worth noting that Tauri has done a great job in security handling, and many areas can be restricted through configuration.

Use fetch in the frontend through Tauri API.

import { fetch } from '@tauri-apps/api/http';
const response = await fetch('http://localhost:3003/users/2', {
  method: 'GET',
  timeout: 30,
});

In real-world applications, different environments may require different fetch methods, so you can encapsulate the fetch method into a custom module and dynamically obtain the required fetch (Browser Native) for different scenarios.

How to compile Tauri and Next.js into one project?

I need to integrate Tauri into my project, which is built based on pnpm workspace with a basic structure as follows:

--doodleboard
  --apps
    --main-next-project
  --packages
    --pkg1
    --pkg2
  --Readme.md

After adding Tauri, my project structure becomes like this, which basically reuses the main body of the Next.js project and adds a new src-tauri directory. So with Tauri added, the overall project structure is very clean.

It is worth noting that, due to the code reuse of Next.js for both front-end and back-end, when using Next.js to write the Tauri front-end, it needs to be compiled and packaged separately. Only the front-end code will run, and the back-end code will not run. Therefore, SSR code should not appear here.

--doodleboard
  --apps
    --main-next-project
    --desktop-app
      --pages
        --_app.tsx
        --index.tsx
      --src-tauri
        --src
                    --main.rs
        --tauri.config.json
      --next.config.js
  -- packages
    --pkg1
    --pkg2
  --Readme.md

And Tauri is also very convenient in the development and debugging stage. Basically, it just adds the original Next.js command in the tauri.config.json file. After configuring it, running pnpm tauri dev
will automatically compile and package the frontend to generate the desktop project.

"build": {
    "beforeBuildCommand": "pnpm build && pnpm export",
    "beforeDevCommand": "pnpm dev",
    "devPath": "http://localhost:3000",
    "distDir": "../out"
},

It is worth noting that, due to the code reuse of Next.js for both front-end and back-end, when using Next.js to write the Tauri front-end, it needs to be compiled and packaged separately. Only the front-end code will run, and the back-end code will not run. Therefore, SSR code should not appear here.

How to Maximize Code Reuse?

After creating your project structure according to the above guide, your Tauri desktop project can reuse other packages in the pnpm workspace, including code from the main-next-project.

In my experience, Tauri can achieve about 95% code reuse, with customization only necessary for desktop interactions. Therefore, when writing business code, try to abstract it as much as possible into workspace packages.

How to Add Deeplinks to Your Tauri App?

Deeplinks are a way to use custom URIs within an application, allowing browsers to redirect to the app in scenarios such as authorization.

Image description

However, after some research, I found that Tauri does not currently support deeplinks. So what can you do? Especially when you need to use authorization login, how do you get the browser and Tauri to communicate? I discovered two possible solutions:

Solution 1: Use a server as an intermediary for communication, such as Firebase. For a more detailed example, see here: https://sevenbits.hashnode.dev/demystifying-user-authentication-with-google-in-electron. The author's scenario involves using Electron, but the essence is the same.

Image description

Solution 2: Use Rust to implement a local server and have the frontend communicate with it via HTTP.

Image description

Additional: Deeplinks can also be implemented as a Tauri plugin, for example, with this library: https://github.com/maidsafe-archive/system_uri/blob/master/src/lib.rs. However, it currently cannot pass parameters.

How to integrate Firebase Auth in Tauri?

Once the communication problem mentioned above is solved, it is easy to implement Firebase Auth. I chose the second communication method. In order to enable Firebase authorization login on the desktop side, you can use the sign in with custom token API.

https://firebase.google.com/docs/auth/admin/create-custom-tokens

The following figure shows the entire authorization process.

Image description

What's Next?

After solving these problems, the product can be up and running. Here is the result of my successful run, but there are still many issues to be resolved, so it cannot be used in real life.

Image description

Image description

For the DoodleBoard scenario, the following issues need to be addressed:

  1. How to make Tauri support multi-tab structures like Figma & Notion, as Tauri does not have an Electron Browser view.

  2. Tauri uses a native browser, and Safari's performance with WebGL is very poor. Even Figma has lag issues when running in Safari. I don't want my product to encounter performance problems in the basic experience, so I need to think about solutions.

Overall, Tauri has already solved the majority of issues and is still rapidly iterating. I hope it will continue to improve.

Finally, everyone is welcome to use my product: https://doodleboard.pro.

Originally published at medium