Web app update strategy: How to divide App Store·Play Store·Tauri·Electron distribution
We divided app updates made with web code into App Store, Google Play, Tauri, and Electron distribution channels, and organized how to design signatures, versions, release channels, and rollbacks.
Updates are not the deployment team's last job
In a web service, the moment you upload it to the server seems to be the end of deployment. Apps are different. The user's device already contains signed executables, previous data, operating system permissions, and store-managed versions. So updates are less about “adding new features” and more about bringing users of different versions safely to the same product.
It's especially easy to get confused when it comes to apps created with web code. The fact that the screen is made of HTML and JavaScript does not mean that you can change the screen code at any time. Automatic updates from the iOS/Android stores, desktop installers, and Tauri/Electron each operate on different trust mechanisms and user expectations.
This article does not compare which is faster: Capacitor·Tauri·Electron. Creating apps with web code: Tauri·Electron·Capacitor selection guide outlines how to move the selected runtime into actual release operation. If you need an implementation comparison, Tauri vs Electron: Criteria for selecting a SvelteKit desktop app, if you are curious about the boundary between mobile and desktop extensions, [Capacitor vs Tauri: Choosing to extend a single web code base to mobile and desktop apps] Standard](https://sik.limited/think/capacitor-vs-tauri-web-mobile-desktop-app) is also recommended to be read.
First, separate the update targets into three types.
Sending all changes to the same urgency hinders operations. It's a good idea to separate changes into three categories before the first release.
| Change target | Example | Default Deployment Path | A record you must leave behind |
|---|---|---|---|
| Server data/content | Notice text, recommended list, remote settings | Server Deployment | modifier, time, previous value |
| Web assets and product logic | Screen behavior, state management, API call method | New version of app binary/installer | App version, build SHA, compatibility range |
| Native functions/permissions | Camera, Push, File System, Payment SDK | Store or signed desktop release | Signing key, entitlement/permissions, verification result |
This distinction is not just about “which updates are reviewed”. The way to recover when a failure occurs also changes. If the remote settings are wrong, you can turn it off on the server, but the version of the app that changed the local data format may already be on the device. So data migration must have a rollback plan before feature flags.
It would be a good idea for the product team to write down the following four lines on each ticket:
change:
user_value: "오프라인에서도 최근 작성물을 다시 연다"
touches_native_boundary: true
minimum_app_version: "1.4.0"
rollback: "읽기 전용 모드로 전환하고 1.3 데이터 포맷을 유지"
The “distributable” status does not mean that the code has been put together, but that the answer lies in these four lines.
The App Store views the app's content and update path together
If you distribute through the iOS and macOS App Store, your app follows the update path managed by the store. Specifically, there is a stipulation that macOS App Store apps must not use any other update mechanism. Although it may seem like a simple app update to users, from the team's perspective, store submissions, reviewable features, and version metadata are one unit.
What is often missed about the Capacitor app is that it is not a “web screen app.” App Review checks whether an app provides continuous utility and an app-like experience beyond just showing a website. The more there is a real usage flow that combines cameras, notifications, offline actions, sharing, and device status, the clearer the why of the product becomes. Conversely, temporary logins, blank screens, and server conditions that reviewers can't reproduce reduce the chances of passing, even with good updates.
Questions asked for iOS releases
- Can reviewers check the features changed in this version without logging in or using a provided test account?
- Is the context of the function sufficiently revealed the moment permission is first requested?
- Was the screen changed on the web actually included in the store submission binary?
- Even if it is an emergency fix, if the executable code or native behavior changes, should it be handled as the next store version?
- Does the server API support previous app versions for a certain period of time even if the user does not update?
This last question is often missed. Just because you submit a new version of your app doesn't mean all users will update on the same day. When changing required fields in an API or breaking an authentication flow, you should deploy based on the app you will support the longest, not the newest app.
// 서버가 앱 버전을 읽고 점진적으로 응답을 바꾸는 단순한 예시
type ClientInfo = { platform: 'ios' | 'android' | 'desktop' | 'web'; version: string };
function supportsSavedSearchV2(client: ClientInfo) {
return client.platform !== 'ios' || compareSemver(client.version, '1.4.0') >= 0;
}
function serializeSavedSearch(client: ClientInfo, search: SavedSearch) {
return supportsSavedSearchV2(client)
? { id: search.id, query: search.query, filters: search.filters }
: { id: search.id, query: search.query };
}
This code is not a complete version control tool. However, it reveals at the code level whether server changes are “possible only after an app update.” Although client version should not be used to make trust-security decisions, it is useful as a signal to select compatible response types.
Google Play is a release followed by version code and signature
In Android, conditions such as application ID, signing certificate, and version code must be met for an update to be installed over an existing app. Specifically, Google Play determines that your app is newer based on the higher version code. The mere fact that “a new APK file has been created” does not constitute an update.
When managing Capacitor's Android project, you must understand the web build version and Android version code separately. Changes to screen assets start in the web build, but it's the Android app bundle that goes to the store. In the meantime, you should leave a CI log showing what input cap sync and native build used.
npm ci
npm run build
npx cap sync android
cd android
./gradlew bundleRelease
Keep the following information in the same place for each release:
product version: 1.4.0
android versionCode: 10400
web commit: 8f3c1a2
capacitor sync: completed
artifact: app-release.aab
api compatibility: 1.2.0+
In-app updates on Android are a way to notify users of updates while they are inside the app. Flexible flows allow background downloads and state management, while immediate flows can request stronger transitions. Either flow must be designed so that user tasks are not lost. If you have a long article you are writing or a file you are uploading, you must first determine whether forcing an update now is right for the product.
// 네이티브 Android 레이어에서 업데이트 가능 여부를 확인하는 형태의 예시
val appUpdateManager = AppUpdateManagerFactory.create(context)
appUpdateManager.appUpdateInfo.addOnSuccessListener { info ->
if (info.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE &&
info.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) {
// UI는 "다운로드 중에도 계속 작업할 수 있음"을 설명해야 합니다.
appUpdateManager.startUpdateFlowForResult(
info,
AppUpdateType.FLEXIBLE,
activity,
REQUEST_CODE
)
}
}
What is important here is the message rather than the API call. If you just pop up “An update is available,” the user might lose his or her work. For flexible updates, the UI should indicate whether work can continue while downloading, whether a restart is required before installation, or whether it can be done later. If you're writing an update immediately, make sure your users understand why it's required now.
Criteria for handling web asset updates in Capacitor
The temptation to change web properties quickly is great. However, if changes to web assets within an app are intertwined with executable code, app behavior, payments, and permissions, the more they are treated as “content changes,” the more dangerous they become. Platform policies and review perspectives may change at any given time, so any scope for remote change should be double-checked with the latest store policies and legal/security review just prior to launch.
In practice, the following line is safe:
| change | Operating Principles |
|---|---|
| Stationery·Image·List downloaded from server | Management with server changes and cache invalidation |
| Exposure conditions of experiment | Manage with remote settings but have defaults and kill switch |
| Screen structure·navigation·authentication·payment flow | Manage with app releases |
| Native plugin·permission·SDK | Manage with app releases |
| DB format/offline synchronization rules | Manage app releases with migration and recovery plans |
This table is a boundary of responsibility, not a technical limitation. Changes that cause users to behave differently from the app they installed from the store should be treated more conservatively, including in the context of testing, review, and customer support. If you need a quick fix, consider “To whom and how I will revert this change if it fails” rather than “Can I make the change remotely?”
Remote settings should be versioned at the same level as apps.
{
"schemaVersion": 3,
"minimumAppVersion": "1.3.0",
"featureFlags": {
"newEditor": false,
"offlineSearch": true
},
"fallback": {
"newEditor": false,
"offlineSearch": false
}
}
Your app should fall back to safe defaults on network failures, invalid JSON, or unknown schemas. The moment you turn the switch on is more important than the moment you turn it off. The kill switch shouldn't just be on the console; it should be in operational documentation as to who turns it off and under what circumstances.
Tauri updates ensure that the signing key creates product continuity
Tauri v2 updater uses signatures to verify that update files come from a trusted source. The public key goes into app settings, and the private key is used to sign release artifacts. It's especially important that if you lose your private key, you won't be able to issue new updates to apps that are already installed. This is not just a CI secret, it is the key linking the installed product to the next release.
Initial setup creates a checklist of key generation, CI storage, access permissions, and incident response.
# 개발자가 개인 키를 생성하는 예시. 실제 경로·보관 정책은 팀의 비밀관리 체계를 따릅니다.
npm run tauri signer generate -- -w ./secrets/my-app.key
# CI에서는 저장소 파일이 아니라 보호된 secret을 환경 변수로 주입합니다.
export TAURI_SIGNING_PRIVATE_KEY="$TAURI_SIGNING_PRIVATE_KEY"
npm run tauri build
{
"bundle": {
"createUpdaterArtifacts": true
},
"plugins": {
"updater": {
"pubkey": "앱에 포함할 공개 키",
"endpoints": [
"https://updates.example.com/{{target}}/{{arch}}/{{current_version}}"
]
}
}
}
The example URL and key string are not settings that can be copied as is. But the structure is clear. The app receives new version information from an HTTPS endpoint and verifies the output with a signature. Whether you operate the update server with static JSON or dynamic responses, server access control and distribution permissions must be managed at the same level as app distribution permissions.
The update response contains the version, download location, signature, and changes. It’s not just the normal “there’s a new version” path that teams must test.
- Are incorrect signatures not installed?
- What policy do you use when you need to revert to a previous version?
- Can this be prevented with a secure response when update metadata is distributed incorrectly?
- When there is unsaved work by the user, what is the order of download and restart?
- Do all outputs for each platform have the same product compatibility range?
Electron operates both an update server and a security perimeter
Electron's autoUpdater checks update feeds and supports downloading and installation. However, the operation varies depending on the platform and packaging method, and the scope of basic built-in support and Linux distribution method must be viewed separately. Updates are an area directly related to user trust, so it is difficult to just leave it to “the packaging tool will take care of it.”
In its smallest form, the update checking code might look like this:
// electron/main.ts
import { app, autoUpdater, BrowserWindow } from 'electron';
autoUpdater.setFeedURL({
url: 'https://updates.example.com/feed'
});
autoUpdater.on('update-available', () => {
BrowserWindow.getAllWindows()[0]?.webContents.send(
'update:status',
{ state: 'downloading' }
);
});
autoUpdater.on('update-downloaded', (_event, notes, name) => {
BrowserWindow.getAllWindows()[0]?.webContents.send(
'update:status',
{ state: 'ready', notes, name }
);
});
// 사용자가 "재시작하고 설치"를 명시적으로 선택했을 때만 호출합니다.
function installDownloadedUpdate() {
autoUpdater.quitAndInstall();
}
Here quitAndInstall() is technically one line, but in production it takes one screen. You need to explain what the user will lose, when it will reopen, and whether updates can be delayed. The idea that quieter automatic updates are better is also dangerous. We need to distinguish between urgent cases, such as security patches, and cases that must wait for user context, such as feature changes.
In Electron apps, updates themselves should not be an excuse to give arbitrary permissions to the renderer. IPC that communicates update status should be limited to read-only events and should not allow the screen to arbitrarily change the download URL or file system. The narrow boundaries between preload and main processes apply not only to feature development but also to update operations.
By dividing channels, you can distinguish between emergency and reckless distribution.
Release channels aren't just for large enterprises. Even if you are a two-person team, you can reduce the radius of deployment accidents by dividing only stable and preview.
| Channel | target | risk tolerable | Things to observe |
|---|---|---|---|
| internal | Team·Tester | Most functional errors other than data deletion | Installation·Login·Migration |
| preview | Voluntary early adopter | UI·Performance·Compatibility Issues | Crashes, Churn, Support Inquiries |
| stable | All users | Low risk only | Error rate, update completion rate, key actions |
| hotfix | Affected Users | Fix clear regression | Successful recovery, prevention of recurrence |
Here channel is not just a version string. It is an operating device that passes database migration through preview first, determines the period during which the stable server can also read previous apps, and determines in advance whether to turn off server flags, update feed, or store distribution when rolling back.
The version numbers are also separated into user-specific and device-specific names.
사용자에게 보이는 버전: 1.4.0
Android versionCode: 10400
데스크톱 업데이트 채널: stable
서버 API 최소 호환: 1.2.0
데이터 스키마: 7
If these five values are stamped together in release notes, CI artifacts, and error reports, you can narrow down “which version only happens” much faster. Especially for apps that mix web and native, it is difficult to determine the cause just by looking at the screen version.
Automation is not a deployment button, but a flow that leaves evidence
The release pipeline is not complete just because the CI runs the build command and uploads the output. Later, when a failure occurs, the team needs to know “which source was signed with which key and distributed to whom.” That is, the output of automation is not only .ipa, .aab, .dmg, and .exe, but also distribution evidence.
At a minimum, leave the following information in one JSON or human-readable note per release:
{
"release": "1.4.0",
"channel": "preview",
"commit": "8f3c1a2",
"builtAt": "2026-08-22T12:00:00Z",
"artifacts": [
{ "platform": "android", "versionCode": 10400, "file": "app-release.aab" },
{ "platform": "macos", "file": "my-app.app.tar.gz" },
{ "platform": "windows", "file": "my-app-setup.exe" }
],
"minimumApiCompatibility": "1.2.0",
"migration": "schema-7"
}
The values themselves are important because they connect one to the other. In customer support, if the user says 1.4.0, they should be able to find which commit and data schema it is. If incorrect metadata in your update feed is the problem, you need to be able to immediately stop which output was exposed to which channel. You can also check to see if the distribution the store is processing and the desktop installer you download directly are subject to the same API changes.
Automation also requires a separation of “issue” and “approval.” Don't let every PR overwrite the stable update feed, only post test output to internal channels, and create stable only on tags where you've reviewed changes, migrations, and rollback plans. Accessing private keys and store certificates requires further narrowing down. Regularly check that secret values are not recorded in the build log and that keys are not left in the local directory.
# CI 의사 코드: 실제 공급자 문법에 맞게 구현한다.
release:
needs: [test, build]
if: startsWith(git.ref, 'refs/tags/v')
steps:
- verify: version and migration plan
- sign: inject protected credentials only for this job
- publish: upload to preview or stable by approved channel
- record: attach release manifest and checksums
This flow may seem slow, but it is the fastest for hotfixes. This is because we already have an idea of who can change the feed, what versions are out, and what candidates can be safely reverted. Speed does not come from eliminating the approval step. It arises by reducing uncertainty.
Rollback is broader than reuploading a previous file
Recovery does not end with just turning off the new version. This may be because a new app has already stored the data in a new format, the server may have removed old fields, or feature flags may have been cached. You should actually practice the below sequence once before deployment.
1. 영향 범위를 확인한다: 어떤 플랫폼·버전·계정이 문제인가
2. 확산을 멈춘다: 스토어 단계 배포 중지, 업데이트 피드 보류, 플래그 OFF
3. 안전 모드로 전환한다: 쓰기 기능을 막고 읽기·내보내기를 우선한다
4. 데이터 호환을 확보한다: 이전 앱이 읽을 응답과 포맷을 되살린다
5. 복구 버전을 낸다: 새 버전의 목적과 복구 조건을 릴리스 노트에 적는다
6. 사후 검토한다: 어떤 신호가 더 일찍 이 문제를 알려줬어야 하는가
The best rollbacks aren't the ones you don't have to do. Users don't lose their work if something goes wrong, the team knows what to do next, and similar failures are caught earlier in the channel. From this perspective, app updates, data backups, error monitoring, and customer support letters are one system.
The distribution checklist must be closed together by planning, design, and development.
Lastly, you may want to copy the checklist below into your release PR or Notion documentation.
Product and Design
- Did you write in one sentence what this change changes in the user flow?
- Are there screens and text for update download, restart, failure, and permission denied status?
- Are the changes understandable to users of older apps?
- Can the store reviewer reproduce the core functionality?
Development and Security
- Did you record the web build SHA, native version, and output hash?
- Do the app ID, signing key, and version code match the release policy?
- Have you restricted the permissions of the Tauri update private key or Electron update feed to a minimum number of people?
- Are IPC·command·plugin permissions open only to the range necessary for the function?
- Has the compatibility period for the previous app and server API/data format been determined?
Operations and Support
- Where do you export it among internal/preview/stable?
- Have you written down which switch, feed, and store distribution will be stopped first if it fails?
- Have you tested recovery paths for unsaved work and data migration?
- Are current version/restart/recovery guidance prepared to answer user inquiries?
Apps that update smoothly are not just the ones that release new features most often, but also the ones that don't forget about users of previous versions. Capacitor must design the boundary between web and store binaries, Tauri must design the continuity of the signature key and update output, and Electron must design packaging, update feed, and IPC security. By separating these three axes before release, the next release will be faster and less risky.