●RORKMAX — Rork Max builds native Swift apps instead of React Native, targeting iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessage●NATIVE — Rork Max unlocks capabilities React Native can't reach: AR/LiDAR, Metal 3D, widgets, Dynamic Island, Live Activities, HealthKit, HomeKit, NFC, and Core ML●PUBLISH — Two-click App Store publishing cuts the steps between generating an app and shipping it●SIM — A browser-based streaming iOS simulator lets you test in a real Apple environment without Xcode or a Mac●STANDARD — Standard Rork turns a plain-English description into working React Native (Expo) code●PRICING — It's free to start, paid plans begin at $25/month, and Rork Max is $200/month●RORKMAX — Rork Max builds native Swift apps instead of React Native, targeting iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessage●NATIVE — Rork Max unlocks capabilities React Native can't reach: AR/LiDAR, Metal 3D, widgets, Dynamic Island, Live Activities, HealthKit, HomeKit, NFC, and Core ML●PUBLISH — Two-click App Store publishing cuts the steps between generating an app and shipping it●SIM — A browser-based streaming iOS simulator lets you test in a real Apple environment without Xcode or a Mac●STANDARD — Standard Rork turns a plain-English description into working React Native (Expo) code●PRICING — It's free to start, paid plans begin at $25/month, and Rork Max is $200/month
Sunsetting an App Well — Designing the Path from Update Freeze to Full Shutdown
What to do with an app whose revenue no longer covers its server bill. A three-stage decision table, a CDN-based sunset flag with TypeScript implementation, a data export path, and the practical order for winding down auto-renewable subscriptions.
The server bill is a few dollars a month. Ad revenue has been less than that for a while now. If you have an app like this sitting in your portfolio, the next move is not another feature.
There is endless writing about how to build apps and almost none about how to retire them. Running several apps in parallel as a solo developer, I have learned that nobody makes this call for you — and nobody hurries you either. That is exactly why the decision drifts, while a dormant app quietly keeps accruing fixed costs.
This article walks through the stages of winding down an app built with Rork, from the decision to the final shutdown: the in-app sunset notice, the data export path, and the cleanup work that auto-renewable subscriptions demand. The short version: if you wire in one small hook while the app is still healthy, everything that follows becomes far easier.
Retiring an App Has Three Distinct Stages
The word sunset covers very different actions. Separating them makes the costs and the reversibility visible.
Stage
What you do
Ongoing cost
Reversibility
Impact on existing users
1. Update freeze
Stop new features; keep OS compatibility and crash fixes only
Server + membership fee + minimal maintenance time
High — you can resume any time
Practically none
2. Store removal
Remove the app from sale in App Store Connect, stopping new downloads
Server + membership fee
Medium — republishing is possible, but search ranking will not return
Installed copies keep working
3. Full shutdown
Turn off the backend and announce the end inside the app
Near zero (optionally keep static delivery)
Low
Online features stop working
The gap between stages 2 and 3 is the part people underestimate. Removing an app from sale does not remove it from anyone's device. If the app depends on a server, the window between removal and shutdown is where the notice period and the data export window belong. Skip it, and what users remember is only that the app suddenly stopped working one day.
Decide the Thresholds in Numbers, Before You Need Them
Retirement decisions drag on because no threshold was ever set. I track two figures monthly.
The first is fixed cost: server fees, domain fees, and the 99-dollar Apple Developer Program membership divided across the number of apps I run. Add the hours a major OS update costs each year, converted at an honest hourly rate, and even a dormant app turns out to cost real money every month.
The second is the revenue trend — as a three-month moving average, not a single month. Ad revenue swings too easily with seasonality and eCPM to trust one month. When the three-month average falls below fixed costs and keeps trending down, the app enters stage 1. Since I adopted this line, the time I used to spend agonizing has visibly shrunk.
One more check before committing: if only part of the app is the burden, removing features may be the better move than retiring the whole product. The three-step retirement flow in When and How to Remove Features Nobody Uses covers that path.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦A decision table comparing update freeze, store removal, and full shutdown across cost, reversibility, and user impact
✦A minimal TypeScript implementation of a sunset flag served as static JSON from a CDN, with no Remote Config dependency
✦The practical sequence for winding down auto-renewable subscriptions, from closing new sales to caring for remaining subscribers
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
The single most important mechanism in stage 3 is the in-app notice. Editing the store listing reaches nobody who already installed the app. The app itself needs a way to learn that it is ending.
Firebase Remote Config can do this, but for an app on its way out I recommend a static JSON file on a CDN instead, for two reasons. You do not want to add a new SDK dependency to an app you are retiring. And a static file can outlive the backend: after the origin is gone, serving one JSON file from static hosting costs effectively nothing for years.
// app-status.ts — the server side is a single static JSON on a CDNimport AsyncStorage from "@react-native-async-storage/async-storage";export type SunsetMode = "active" | "notice" | "readonly" | "ended";export interface AppStatus { mode: SunsetMode; message?: string; // notice text (switch by device locale) exportUntil?: string; // deadline for data export (ISO 8601) learnMoreUrl?: string; // support page with details}const STATUS_URL = "https://static.example.com/app-status/my-app.json";const CACHE_KEY = "app_status_cache_v1";export async function fetchAppStatus(): Promise<AppStatus> { try { const res = await fetch(STATUS_URL, { headers: { "cache-control": "no-cache" }, }); if (!res.ok) throw new Error(`status ${res.status}`); const status = (await res.json()) as AppStatus; await AsyncStorage.setItem(CACHE_KEY, JSON.stringify(status)); return status; } catch { // On failure, fall back to the last known value. // With no cached value, default to active so normal startup is protected. const cached = await AsyncStorage.getItem(CACHE_KEY); return cached ? (JSON.parse(cached) as AppStatus) : { mode: "active" }; }}
Note the direction of the fallback: a failed fetch resolves to normal startup. Invert it and a brief CDN hiccup turns your entire app into a shutdown screen. For a notice like this, safety beats certainty.
The notice mode keeps the app fully usable with a banner; readonly disables writes and leaves browsing and export. Advancing a stage means editing one JSON file — no app review required. So the practice is: when you decide to retire an app, ship one last update that adds this hook. The apps where I skipped that final update are the ones whose cleanup cost me the most time afterward.
Give Users a Way to Take Their Data With Them
Favorites, settings, anything accumulated on the device deserves an exit door. The implementation is small — serialize to JSON and hand it to the share sheet.
// export-user-data.tsimport AsyncStorage from "@react-native-async-storage/async-storage";import * as FileSystem from "expo-file-system";import * as Sharing from "expo-sharing";export async function exportUserData(): Promise<void> { const favorites = await AsyncStorage.getItem("favorites_v2"); const settings = await AsyncStorage.getItem("settings_v1"); const payload = { exportedAt: new Date().toISOString(), schemaVersion: 2, favorites: favorites ? JSON.parse(favorites) : [], settings: settings ? JSON.parse(settings) : {}, }; const uri = FileSystem.cacheDirectory + "my-data-export.json"; await FileSystem.writeAsStringAsync(uri, JSON.stringify(payload, null, 2)); await Sharing.shareAsync(uri, { mimeType: "application/json" });}
Including a schema version in the export pays off if you ever ship a successor app that imports the file. Even with no successor, the plain fact that users keep their data changes how the shutdown is received.
Winding Down Auto-Renewable Subscriptions
If the app sells subscriptions, the order of operations gets stricter, because removing the app from sale does not stop existing subscriptions — they keep renewing until each subscriber cancels.
First, close the purchase flow inside the app using the same sunset flag, so no new subscriptions begin. Then end the availability of the subscription products in App Store Connect. Existing subscribers get the shutdown date and cancellation instructions through the in-app notice and the support page. Cancellation is an action only the user can take, which is exactly why early notice is the honest form of it.
How you treat the last remaining subscribers is less a question of money than of posture. Do not let renewals continue past the point where the service has effectively ceased: set the shutdown date at least one full billing cycle out. Individual refunds largely flow through Apple's own mechanisms, but whether you did everything you could to inform people shows up, verbatim, in your final reviews.
Do Not Switch the Server Off — Degrade It in Steps
The backend comes down in stages, not with one switch.
Order
State
What you do
1
Writes disabled
Return 410 from write endpoints; the app runs read-only
2
Static snapshot
Replace read responses with a static snapshot served from the CDN
3
Shutdown
Delete the origin; keep only app-status.json and the snapshot
On Cloudflare Workers, step 2 is nothing more than swapping the Worker's responses for fixed values from KV or static assets. Even after the dynamic backend is gone, a static snapshot of the last content you served lets the experience degrade gently instead of breaking. The option to keep that snapshot alive for years at near-zero cost is the second reason I prefer the static JSON approach throughout.
What Remains After You Pull the App
Pressing the remove-from-sale button never gets entirely comfortable — my finger still hesitates every time. Knowing what persists afterward makes it lighter.
Installed copies keep running. The support URL and the privacy policy page must stay up for as long as those users exist; take them down and inquiries have nowhere to go. As long as you maintain the developer account, the same app can even return to sale under the same App ID. Removal is closer to dormancy than to disposal.
The best return on this experience, though, goes to the apps still in service: add the app-status.json hook to them now. The best outcome is never needing it. But if the day comes, the time you saved by not scrambling to ship a final update becomes, quite directly, the care you can put into the goodbye.
Designing the last day is part of designing the app. I hope this serves as a useful piece of preparation that grows more valuable the longer your apps live.
Share
Thank You for Reading
Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.