Creating apps with web code: Tauri·Electron·Capacitor selection guide
When expanding a web app to iOS, Android, Windows, and macOS, we have organized the criteria for choosing between Tauri, Electron, and Capacitor based on product requirements, permissions, WebView, security, and distribution.
If you choose the framework first, you miss the question
When expanding a product created first on the web into an app, Tauri, Electron, and Capacitor are often met in a one-line comparison table. It’s easy to end with “Tauri is light,” “Electron is mature,” and “Capacitor is mobile.” But the real choice is less about library names and more about which screens users should be on, what risks they take, and at what speed they should receive updates.
For example, a file organization tool that is only used in-house and a consumer service that uses the iPhone's camera and push notifications every day have different centers of operation even if they start with the same web code. In the former, file permissions and desktop distribution come first, and in the latter, store review, notification permissions, and offline conversion come first. Choosing the skin of your app means choosing the promise of your product.
This article does not decide on the superiority or inferiority of the three tools. Instead, we set standards so that product planning, design QA, development, and distribution can all be judged in the same document. If you have already read Tauri vs Electron: Selection criteria for a SvelteKit desktop app and Capacitor vs Tauri: Selection criteria for extending a web codebase to mobile and desktop apps, The article is the hub that connects that comparison to your launch plan.
The boundaries of the three tools are different
First, we need to explain the phrase “making a web app into a native app.” All three tools can utilize screens created with HTML, CSS, and JavaScript. However, they are not the same type of runtime.
| Question | Capacitor | Tauri | Electron |
|---|---|---|---|
| starting point | Connect mobile web app to iOS/Android app | Connecting web front-end and Rust-based native core | Deploy web front-end and Chromium·Node.js environment together |
| The most natural stage | iOS·Android | Windows·macOS·Linux desktop | Windows·macOS·Linux desktop |
| Screen Engine Perspective | Each mobile platform WebView | WebView of the operating system | Chromium included in the app |
| Touch point of native functionality | Plugins and iOS/Android projects | Specified command·plugin capability | main/preload/renderer and IPC |
| Team should design first | Permissions·Plugin·Store Build | Permission scope, Rust boundary, and platform-specific UI QA | IPC boundaries, security settings, packaging and updates |
This table does not mean “one replaces the other.” Capacitor is closer to the mobile-native project management stream, while Tauri and Electron are desktop deployment-focused choices. Tauri also supports mobile, and there is also a combination of capacitor-based code plus a desktop target. However, if the starting point and operating costs are not viewed along the same lines, the time saved initially will be lost during QA and release.
A simpler drawing of the structure is as follows.
공유 가능한 영역
├─ 도메인 모델 · API 클라이언트 · 디자인 토큰 · 화면 컴포넌트
├─ 웹 배포물 (PWA / 브라우저)
└─ 플랫폼 어댑터
├─ Capacitor: iOS / Android 플러그인과 WebView
├─ Tauri: Rust command / capability / 시스템 WebView
└─ Electron: preload / IPC / Chromium + Node.js
The most important principle here is not the amount of shared code, but the amount of responsibility that is acceptable to share. Points where the platform directly interacts with the user's device, such as payment, camera, file, push, and background tasks, must be left with thin adapters. If you hide the border just because the screen code is the same, it becomes difficult to find who called what and with what authority when an error occurs.
First divide the product request into four parts.
Writing the next four chapters before a technical meeting will shorten the framework argument.
- Usage environment: Does the user work on a large screen in front of the desk, open the phone while on the move, or are both necessary?
- Device functions: Are the camera, photo storage, push, biometric authentication, and sharing sheet the core of the product? Or are the file system, tray, multiple windows, and global shortcut keys the key?
- Trust Boundary: What to read and write: local files, credentials, corporate data, or user-generated HTML.
- Release Speed: Have you separated content that needs to be changed on the server from executable code that requires store review or installer updates?
These four chapters are both planning and architecture documents. Statements like “Mobile will be supported later” are not requirements yet. You need to write down the actions required on mobile. For example, if you take a photo, send it to OCR, receive the result by push, and temporarily store it offline, the product scope includes the native plug-in, permission, and recovery flow on the Capacitor side. On the other hand, if you drag and drop multiple files during a meeting, launch them from the menu bar, and edit hundreds of items with the keyboard, the desktop is more than just an extension of your screen size.
Quick Select Questions
| If “yes” to these questions | starting point |
|---|---|
| Are iOS and Android launches key this quarter and are camera, push and share part of the features | Capacitor |
| Desktop apps are the key, can you declare minimal local permissions and have a Rust boundary | Tauri |
| Does Chromium's consistent rendering on the desktop, the Node.js ecosystem, or existing Electron assets matter? Electron | |
| Do you need both mobile and desktop | With Capacitor as the mobile axis, separate consideration of desktop needs with Tauri or Electron |
| Neither one is clear | First, verify core behaviors with responsive web/PWA and record native needs |
“All platforms at once” is often a wish rather than a demand. It's better to create a complete experience on one platform first, and then open the next platform after you've identified what you want to share and what you want to keep separate. This order does not slow down the product; it reduces the number of exceptions that need to be supported.
Capacitor turns mobile WebView problems into product problems
Capacitor is a natural fit for teams accustomed to adding native functionality to web apps. The flow is to build web assets, synchronize them to iOS and Android projects, and call functions such as camera, notifications, and files through plugins. The advantage of this structure is that the front-end team does not have to significantly discard the existing screen, state management, and API layers.
However, you should not choose it just because it is “fast because it is a web view.” The moment it becomes a mobile app, states that were not visible in the browser appear. The screen when permission is denied, the moment the app goes into the background and comes back, the moment the upload is interrupted on a slow network, and the path when the app reviewer fails to log in are all products.
First, we explicitly divide the web layer and native layer.
// src/platform/camera.ts
export type CapturedPhoto = {
dataUrl: string;
format: 'jpeg' | 'png';
};
export async function capturePhoto(): Promise<CapturedPhoto> {
if (typeof window === 'undefined') {
throw new Error('브라우저 렌더링 중에는 카메라를 열 수 없습니다.');
}
const { Camera, CameraResultType } = await import('@capacitor/camera');
const result = await Camera.getPhoto({
quality: 85,
resultType: CameraResultType.DataUrl,
allowEditing: false
});
if (!result.dataUrl || !result.format) throw new Error('사진 데이터를 받지 못했습니다.');
return { dataUrl: result.dataUrl, format: result.format as 'jpeg' | 'png' };
}
The key to the above code is not the Camera API itself. The point is to prevent the screen component from importing the plugin right away. In web previews, you can show an alternative UI, or in tests, you can include a fake implementation that returns the same type. If you do this, the problem of “It works on the web but doesn’t work in the app” won’t be mixed within one component.
The build phase is also treated as a product deployment phase.
npm run build
npx cap sync
npx cap open ios
# 또는
npx cap open android
sync is not a simple copy. A boundary that reflects web build results and plugin settings to the native project. If this step is omitted in CI, you will see the latest screen locally, but the binary uploaded to the store will contain old web assets. So the release pipeline should output the web build number and iOS/Android version information.
Tauri makes sense in smaller permissions than in small bundles
Tauri connects the web frontend and Rust core and uses each operating system's WebView. It's easy to be disappointed if you only use bundle size or memory numbers as a starting point for comparison. The real value comes from an architecture that explicitly narrows down what the frontend can request from the operating system.
Just because you need to save a file doesn't mean you need to access all the paths. Commands and scopes that can be called from the front end must be specifically designed. Here's an example of a mindset that only allows exports under the folders the user selects:
// src-tauri/src/lib.rs
#[tauri::command]
fn export_report(destination: String, body: String) -> Result<(), String> {
if !destination.ends_with(".md") {
return Err("Markdown 파일만 내보낼 수 있습니다.".into());
}
std::fs::write(destination, body)
.map_err(|error| format!("파일을 저장하지 못했습니다: {error}"))
}
// src-tauri/capabilities/default.json
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "main-window",
"windows": ["main"],
"permissions": [
"core:default",
"dialog:allow-save",
{
"identifier": "fs:allow-write-text-file",
"allow": [{ "path": "$HOME/Documents/my-app/**" }]
}
]
}
The actual permission identifier and schema should be checked in the official documentation according to the plugin version you are using. This example shows that even “one save button” involves three decisions. Can the user select a file, what path can the app use, and what kind of input does the command accept? It is important not to open all three wide at once.
Tauri's system WebView creates both advantages and validation items. Since the rendering engine and version management method may be different in Windows, macOS, and Linux, design QA should be performed on the actual device for each operating system, or at least on the corresponding engine. “Visible in Chrome” is not a passing criterion. If you test the screens that are core to your product first, such as font wrapping, file drag and drop, media, editor, and the latest CSS features, WebView differences become a manageable cost rather than a vague anxiety.
Electron is not the opposite of heaviness, but a choice of consistency
Electron distributes Chromium and Node.js along with the app. So it's clear which runtime needs to be addressed for installation and updates. At the same time, that choice allows testing the same Chromium-based screen across multiple operating systems, and teams comfortable with JavaScript and Node.js don't have to reinvent the backend perimeter in Rust.
This advantage does not mean that you can skip security design. In Electron, the default pattern is to expose only the necessary functions in preload, without giving full Node.js permissions to the renderer. Apps that display remote content must be more stringent.
// electron/preload.ts
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('desktop', {
pickExportPath: () => ipcRenderer.invoke('dialog:pick-export-path'),
saveTextFile: (path: string, contents: string) =>
ipcRenderer.invoke('file:save-text', { path, contents })
});
// electron/main.ts
import { dialog, ipcMain } from 'electron';
import { writeFile } from 'node:fs/promises';
ipcMain.handle('dialog:pick-export-path', async () => {
const result = await dialog.showSaveDialog({
filters: [{ name: 'Markdown', extensions: ['md'] }]
});
return result.canceled ? null : result.filePath;
});
ipcMain.handle('file:save-text', async (_event, input: { path: string; contents: string }) => {
if (!input.path.endsWith('.md')) throw new Error('Markdown 파일만 저장할 수 있습니다.');
await writeFile(input.path, input.contents, 'utf8');
});
All the frontend receives is a narrow function called desktop.saveTextFile. Does not expose require('fs') or any Node API to the screen. This restriction is a bit cumbersome, but it is a key boundary that prevents on-screen XSS issues from immediately turning into device permission issues. This is why the Electron official security guide emphasizes context isolation, limited IPC, maintaining the latest version, and CSP.
Share the screen, but do not force the function to be shared
A common failure when running three runtimes together is misunderstanding “one code base” as “one behavior.” It's a good idea to separate from the beginning what you want to share and what you want to leave on each platform.
packages/
core/ # 도메인 타입, API, 유효성 검사
ui/ # 디자인 토큰, 화면 컴포넌트
web/ # 브라우저용 진입점
mobile/ # Capacitor 초기화, 모바일 권한·플러그인
desktop-tauri/ # Tauri command, capability, 번들 설정
desktop-electron/ # preload, IPC, 패키징 설정
core contains product intent, such as “upload a photo” or “export a report.” In mobile, there is a method to check camera permissions, and in desktop-tauri and desktop-electron, there is a method to change the intention of file saving to the operating system API. The design system will also benefit more if it does not just share the shape of the button, but also handles states such as permission denied, waiting for update, and offline failure as common components.
This doesn't mean that one repository is the right answer. If the team is small and the release date for each platform is far away, managing the monorepo itself can be a cost. It is not too late to extract it after the domain logic to be shared is actually created. The important thing is not to have the platform implementation scattered all over the screen, not the directory structure.
Performance comes from the path the work takes, not the framework
The most common thing that comes up when comparing Tauri vs Electron is install size and memory. Although both affect the quality perceived by users, the actual bottleneck of the product often lies elsewhere. This is more noticeable when decoding large images, screens that don't virtualize lists, synchronous API calls, going back and forth between IPC too often, and background tasks blocking the UI.
Don't write your performance hypothesis as a framework alias, write it as a user behavior.
| User Behavior | What to measure | Direction for improvement |
|---|---|---|
| Open the app for the first time | When the home screen is interactive | Lazy loading of initial data/heavy modules |
| Browse 1,000 files | Frames and responses during scrolling/searching | Virtual list, separation of indexing operations |
| Take and upload photos | From permissions to completion instructions | Failure Retry/Background Switch Status |
| Get updates | Download·Restart·Rollback | Designing release channels and recovery paths |
In particular, calls that cross native boundaries should be treated like APIs. Instead of calling Tauri command or Electron IPC for each character input, process screen input locally and call it only when alertness is needed, such as storage, indexing, or file operations.
// 나쁜 예: 입력마다 데스크톱 경계를 넘는다.
editor.on('change', (value) => window.desktop.saveDraft(value));
// 더 나은 예: UI 상태와 영속화를 분리한다.
let pending: ReturnType<typeof setTimeout> | undefined;
editor.on('change', (value) => {
clearTimeout(pending);
pending = setTimeout(() => saveDraft(value), 700);
});
async function saveDraft(value: string) {
await draftRepository.save({ value, savedAt: new Date().toISOString() });
}
This design remains regardless of which runtime you choose. This is why there is no need to explain performance by saying, “Change to Tauri will solve the problem” or “It can’t be helped because it’s Electron.”
Create a small spike that validates the selection
You don't need to create a 15-day demo before deciding on a framework. However, a small spike is needed to select the most dangerous scene in the product and pass it through to the end in the candidate runtime. The purpose of this spike is not to create a pretty first screen, but to break assumptions early that will be difficult to reverse later.
For file-centric desktop products, the following four scenes are good: The user selects a folder in the file selection window. The app reads the list of files in that folder. Modify and save a file. Safely restore recent work even after restarting the app. In Tauri, you can check whether the command·capability·scope does not overly widen this flow, and in Electron, you can check whether preload·IPC exposes only necessary functions. In either case, you must check on the screen not only “the file has been read,” but also rejection, cancellation, no permission, and simultaneous modification.
For mobile products, choose features that break the assumptions of web browsers, such as camera or push. We look at the timing of when permission is first requested, the fallback path after permission is denied, the flow of reopening completed tasks when the app is in the background, and even retries on slow networks. The advantage of Capacitor is not only the reuse of web code, but also the ability to actually see these device boundaries in native projects.
The outcome of a spike should be a checklist, not “success” or “failure.”
| Check items | Conditions for passing | A record to leave when you fail |
|---|---|---|
| Build | Creating deliverables from CI to a clean environment | SDK/runtime version used and error log |
| rendering | Core screens available on target OS | OS, WebView/Chromium version, screenshots |
| Permissions | Even after rejection/cancellation, the next action is visible | User's stuck steps and phrases |
| data | Drafts are preserved even after restarts and updates | Format version and recovery method |
| Distribution | Installation/uninstallation/reinstallation works as expected | Installation path, signature, remaining data |
Spike only checks the learning difficulty of Tauri's Rust or the size of Electron's installations, and this means little. Those costs are already known costs. What you really need to check is what your team's design system looks like in the system WebView, what state the authentication, file, and offline code you're using creates at the native boundary, and whether designers and QA can reproduce that state.
This record will not be discarded if you change the runtime in the next quarter. The permission copy, test device list, API compatibility rules, and error reporting format remain product assets regardless of whether you choose Capacitor, Tauri, or Electron. Conversely, a project started without such records will end up blaming the framework whenever a problem arises.
Rehearse failure paths, not features, before release
If you only demonstrate normal operation before putting your app on the market or download page, important issues remain. The following table is a release rehearsal that planning, design, and development will check together.
| scene | Questions to check |
|---|---|
| Permission denied | Does it explain why it is needed and is there a way to get back to the settings |
| network disconnection | Will I not lose any unsaved work, and will I be able to retry? |
| Restart app | What is the status of ongoing uploads, downloads, and drafts? |
| Old OS | Do you show users the scope and limitations of support? |
| Other WebViews | Are fonts, editing, media, dragging, and keyboard operations as intended? |
| Login/Review | Can the reviewer reproduce the core product? |
The desktop updates for Tauri and Electron, and the mobile updates for the App Store and Play Store are the same “new version” but controlled by different entities. In the next article, Update Strategy for Web Apps: How to Divide App Store·Play Store·Tauri·Electron Distribution, we cover these differences, including release channels, signing keys, hotfixes, and rollbacks.
A choice is not a one-time declaration, but a reversible decision
The last thing a small team should avoid is imagining all future needs and creating the most complex structure first. It is better to pick one core action of your current product and complete it on the platform where that action is most natural.
- If mobile camera, push, and sharing are the key, Capacitor completes permissions and store flow.
- If local files, windows, keyboard, and background tasks are key, Tauri or Electron completes installation, update, and recovery.
- If your team is already running Electron, don't move just because “the new one is lighter,” but write down the costs you're trying to solve in numbers and a replay scenario.
- If you choose Tauri, judge the quality based on how narrowly the capabilities and commands were designed rather than the fact that Rust was introduced.
A good choice is not one that breaks other runtimes later. It's a choice that lowers the cost of the next decision by sharing screens and product rules but leaving device permissions and distribution a thin boundary. The app's framework is not visible to the user. However, the design is revealed as is when asking for permission, when an update fails, or when a draft remains offline.