<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Despia Blog: Build Native iOS & Android Apps from Web Code]]></title><description><![CDATA[Guides on shipping web apps to iOS and Android with Despia. Native device features, hybrid app patterns, and over-the-air updates from one codebase.]]></description><link>https://blog.despia.com</link><image><url>https://cdn.hashnode.com/uploads/logos/67f0239ff4f5a0b7b4a852c4/6b25cfda-185c-4d83-a8f5-d0fdc336f89b.png</url><title>Despia Blog: Build Native iOS &amp; Android Apps from Web Code</title><link>https://blog.despia.com</link></image><generator>RSS for Node</generator><lastBuildDate>Sat, 17 Oct 2026 08:51:33 GMT</lastBuildDate><atom:link href="https://blog.despia.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How to Build a Base44 Mobile App You Own]]></title><description><![CDATA[There are two ways to get a Base44 app onto the App Store and Google Play, and they differ less in how hard they are than in what you end up holding afterwards. This is a guide to the decisions that d]]></description><link>https://blog.despia.com/how-to-build-a-base44-mobile-app-you-own</link><guid isPermaLink="true">https://blog.despia.com/how-to-build-a-base44-mobile-app-you-own</guid><category><![CDATA[base44]]></category><category><![CDATA[iOS]]></category><category><![CDATA[ios app development]]></category><category><![CDATA[ios app developer]]></category><category><![CDATA[Android]]></category><category><![CDATA[android app development]]></category><category><![CDATA[#ai-tools]]></category><category><![CDATA[ai agents]]></category><dc:creator><![CDATA[Despia CEO]]></dc:creator><pubDate>Wed, 19 Aug 2026 21:49:59 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/67f0239ff4f5a0b7b4a852c4/afe73013-6adc-4d2a-baaf-2ecc1f640d9f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>There are two ways to get a Base44 app onto the App Store and Google Play, and they differ less in how hard they are than in what you end up holding afterwards. This is a guide to the decisions that determine whether the app is an asset you control or an output of a platform you rent.</p>
<h2>What does owning a mobile app actually mean?</h2>
<p><strong>Five things, and they are separable. The developer accounts, the app identifier, the address the app loads, the backend behind that address, and the native project itself.</strong></p>
<p>You can own all five, some of them, or effectively none, and most people never notice which until they want to change something.</p>
<p>Taken one at a time:</p>
<p><strong>The developer accounts.</strong> Your Apple Developer Program membership and your Google Play Console account, in your name or your company's, paid for by you. This one is easy and both paths get it right. Base44's documentation is explicit that you are responsible for creating and managing these accounts and that you submit through them.</p>
<p><strong>The app identifier.</strong> The bundle ID on iOS and the package name on Android. This is the permanent, public key both stores use for your app, and it cannot be renamed later on either platform.</p>
<p><strong>The address the app loads.</strong> Every wrapped app points at a URL. Whoever controls that URL controls what the installed app shows.</p>
<p><strong>The backend.</strong> Where your data lives and what happens to it if you stop paying the platform.</p>
<p><strong>The binary.</strong> Whether you can open the actual Xcode and Android Studio projects, or whether the compiled file is the only artifact you will ever see.</p>
<h2>Which of those does the built-in path give you?</h2>
<p><strong>The accounts, and the submission.</strong></p>
<p>Base44 documents the rest as fixed: the bundle ID and signing key are set automatically and cannot be changed in the generated IPA or AAB, the package name follows a set format, and the entry URL comes from your published app rather than from you.</p>
<p>The package name format is documented as <code>com.base[app-id].app</code>, where the app ID comes from your editor URL. It is a functional identifier and it works. It is also permanent and publicly visible in the Play Store URL, and it names the platform that generated it. Whether that matters depends entirely on who ends up reading it: invisible for an internal tool, a detail that gets noticed in an enterprise security review or an acquisition diligence pass.</p>
<p>The entry URL constraint is the one with longer consequences. If the installed binary is bound to a Base44-controlled address, then moving off Base44 later means the shipped app no longer points at your product. That is not a criticism of the design. It is a straightforward consequence of the platform generating the build.</p>
<h2>What does the ownership decision cost?</h2>
<p><strong>Almost nothing at the start and almost everything later, which is exactly why it gets skipped. A domain is a few dollars a year. Choosing an identifier takes 30 seconds. Neither is recoverable once the app is live.</strong></p>
<p>The asymmetry is worth stating plainly. If you take the defaults and never need to move, you lost nothing. If you take the defaults and do need to move, you abandon the listing, and with it the reviews, the ratings, the install base and whatever store ranking you earned. There is no partial version of that outcome and no support ticket that fixes it, because the constraint belongs to Apple and Google rather than to any vendor.</p>
<p>That makes it a straightforward asymmetric bet. The downside of owning your identifier and domain is a rounding error. The downside of not owning them is the only asset a small app really has.</p>
<h2>How do you keep all five?</h2>
<p><strong>Own the domain first, then choose the identifier yourself, then ship through a runtime that hands you the native projects. In that order, because each one constrains the next and only the first is free to change later.</strong></p>
<p><strong>1. Point the app at a domain you own.</strong> A custom domain is not required to submit, and Base44 says so directly. It is required for portability. The domain is the indirection layer between the installed binary and whatever is serving your app this year. Buy it before you build the app, not after you have 5,000 installs.</p>
<p><strong>2. Publish the Base44 app to that domain.</strong> Publishing is a separate action from editing. The binary should be built against the published app on your custom domain, never against an editor preview URL, because a preview URL can show App Review a different state from the one you meant to submit.</p>
<p><strong>3. Choose your own bundle ID and package name.</strong> With Despia you set these when you create the project, in reverse domain form from the domain you just bought, for example <code>com.yourcompany.yourapp</code>. After the first signed build they lock, because Apple and Google treat the identifier as immutable, so this is a decision to make deliberately at the start.</p>
<p><strong>4. Add the native capabilities your app needs.</strong> From the same Base44 codebase, through one JavaScript function. Environment detection uses the user agent, and native calls go inside that gate:</p>
<pre><code class="language-javascript">import despia from 'despia-native'

const isDespia = navigator.userAgent.toLowerCase().includes('despia')

if (isDespia) {
    despia('successhaptic://')
}
</code></pre>
<p><strong>5. Ship it, and keep the projects.</strong> Code signing and store submission run from a browser, with no CLI and no Mac. The full Xcode and Android Studio projects export whenever you want them, which is what turns the runtime into a choice rather than a dependency.</p>
<h2>What do you actually need for a first submission?</h2>
<p><strong>Less than most guides suggest. A published app on your own domain, both developer accounts, an icon, a privacy policy and terms reachable before signup, store screenshots, and demo credentials if anything sits behind a login.</strong></p>
<p>Two of those are worth extra attention because they cause avoidable rejections: <strong>Privacy policy and terms have to be reachable before account creation.</strong> If your app opens on a login screen, the links need to be on that screen or in a footer a reviewer can reach without an account.</p>
<p><strong>Demo credentials must work on the day you submit.</strong> Tick "Sign-in required" in App Review Information, enter working credentials, verify them on a real device the same day, and name in the notes exactly where a reviewer should look for anything non-obvious. Guideline 2.3.1(a) requires new functionality to be described with specificity and explicitly rejects generic descriptions.</p>
<h2>What has to be true to migrate later?</h2>
<p><strong>The identifier, the domain and the store listings have to be yours, and the app has to load from an address you can repoint. If all three hold, migration is a DNS change rather than a new app.</strong></p>
<p>This is the part worth thinking through before you need it, because the cost of getting it wrong is not paid at migration time, it is paid in the listing you have to abandon. Reviews, ratings, install base and store ranking all attach to the identifier. Start a new identifier and you start those at zero.</p>
<p>With the domain and the identifier in your hands, the paths out are all cheap:</p>
<ul>
<li><p><strong>Rebuild the front end.</strong> Move from Base44 to Next.js, Remix, or a team of developers writing whatever they prefer. Point the domain at the new build. Every installed app follows, with no resubmission.</p>
</li>
<li><p><strong>Move the backend.</strong> Swap the managed services for your own Supabase project, or anything else. The mobile app never knew the difference.</p>
</li>
<li><p><strong>Leave the runtime.</strong> Export the native projects and continue in Xcode and Android Studio, keeping the same identifier and the same store listings.</p>
</li>
</ul>
<p>None of that is a prediction that you will leave Base44. Plenty of apps stay for years and should. It is insurance, and the premium is one domain registration plus 30 seconds of thought about an identifier.</p>
<h2>What else decides whether the app ships?</h2>
<p><strong>Four things beyond ownership: whether the app looks like an app, whether it does something a browser tab does not, whether it sells digital content, and whether a reviewer can actually reach every feature.</strong></p>
<p>Those are the questions guideline 4.2 and the review process actually turn on, and none of them are about which tool built the app.</p>
<p><strong>Does it look like an app?</strong> Guideline 4.2 asks for features, content and UI, and the UI half is where most resubmissions fail. Sidebars, hamburger menus, desktop top navigation, a visible sitemap footer and a cookie banner all read as website. What reviewers expect is a top bar with the current screen title, a content area, and a bottom navigation bar with icons.</p>
<p><strong>Does it do something the installed experience adds?</strong> Not what a browser technically cannot do, since web push, geolocation and the Web Share API all exist. The credible answers are integrations: native purchases, a biometric flow tied to real credential storage, camera and picker flows, contacts and calendar, background location, haptics, background audio, offline that survives a cold start, and deep and universal links. Name 3 a reviewer will meet without hunting.</p>
<p><strong>Does it sell digital content?</strong> If so, it has to go through StoreKit and Google Play Billing. Physical goods and services consumed outside the app are the opposite case, and guideline 3.1.3(e) requires a payment method other than in-app purchase for those.</p>
<p><strong>Can a reviewer reach everything?</strong> Tick "Sign-in required" in App Review Information, verify the demo credentials on a real device the same day you submit, and describe new functionality with specificity in the notes, since guideline 2.3.1(a) explicitly rejects generic descriptions.</p>
<h2>How do you get it onto the stores?</h2>
<p><strong>Take the Base44 app you already built and ship it under a bundle ID you chose, from a domain you own, with the native capabilities the stores expect.</strong></p>
<p>Code signing and submission run from the browser, and the full native projects export whenever you want them.</p>
<p><a href="https://setup.despia.com">See the setup docs at setup.despia.com</a></p>
]]></content:encoded></item><item><title><![CDATA[Base44 Mobile App Limitations Worth Knowing First]]></title><description><![CDATA[Base44 documents the constraints of its mobile app openly. The problem is that they sit across a submission guide, an FAQ accordion and a troubleshooting section, so nobody reads them in one sitting a]]></description><link>https://blog.despia.com/base44-mobile-app-limitations-worth-knowing-first</link><guid isPermaLink="true">https://blog.despia.com/base44-mobile-app-limitations-worth-knowing-first</guid><category><![CDATA[base44]]></category><category><![CDATA[AI]]></category><category><![CDATA[#ai-tools]]></category><category><![CDATA[aitools]]></category><category><![CDATA[AppReview]]></category><category><![CDATA[appstore]]></category><category><![CDATA[googlePlay]]></category><category><![CDATA[iOS]]></category><category><![CDATA[ios app development]]></category><category><![CDATA[ios app developer]]></category><category><![CDATA[Android]]></category><category><![CDATA[android app development]]></category><dc:creator><![CDATA[Despia CEO]]></dc:creator><pubDate>Wed, 19 Aug 2026 21:46:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/67f0239ff4f5a0b7b4a852c4/521622ae-c471-4f66-8820-397836abc2cc.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Base44 documents the constraints of its mobile app openly. The problem is that they sit across a submission guide, an FAQ accordion and a troubleshooting section, so nobody reads them in one sitting and most people meet them at submission time. Here they are together.</p>
<p>None of what follows is a defect. It is the shape of a version-one wrapper inside a much larger product, and Base44's documentation says the platform continues to expand native feature coverage. The useful question is not whether a constraint will lift eventually. It is whether it is lifted on the day you need to ship.</p>
<h2>What native features does the Base44 mobile app support?</h2>
<p><strong>A web view around your published app, plus push notifications. That is the whole list. Base44's own FAQ states it directly and names full offline mode and HealthKit as unsupported.</strong></p>
<p>The FAQ adds that some native capabilities may need additional review by Apple or Google depending on which permissions your app requests.</p>
<p>That coverage line is the most consequential sentence in the documentation, because guideline 4.2 asks for features, content and UI that elevate an app beyond a repackaged website, and Apple's standard 4.2 rejection letter names push notifications specifically as an addition that does not carry an app on its own. An app whose entire native surface is push is exactly the case that letter was written for.</p>
<p>That does not mean push is worthless to a submission. It means it is one input among several, and it is the input Apple has already said does not carry an app on its own. Three further guidelines constrain what you can do with it: 4.5.4 says push must not be required for the app to function, 5.1.2(i) prohibits requiring users to enable push in order to access functionality or content, and 4.10 prohibits monetizing built-in capabilities and names push explicitly. An app whose value proposition is the notification has the dependency backwards.</p>
<h2>Can you sell subscriptions through a Base44 mobile app?</h2>
<p><strong>Not compliantly today. Base44's documentation states that Stripe must not be used for digital goods inside the mobile app, and that an app using Stripe for digital content is rejected.</strong></p>
<p>A StoreKit and Play Billing integration is described as in progress. Both stores require their own billing systems for digital content, so this is not a Base44 rule, it is the platform rule every app has to meet. Our <a href="https://blog.despia.com/base44-revenuecat-setup-storekit-and-play-billing">guide to selling subscriptions from a Base44 app</a> covers the compliant setup in full.</p>
<p>Read carefully, that line is not a warning about a risky pattern. It is a description of a closed path. If your product sells subscriptions, in-app features, credits or any other digital content, the built-in wrapper has no route to the stores until the native billing integration ships.</p>
<p>The distinction that matters: physical goods and services consumed outside the app are fine on Stripe, and guideline 3.1.3(e) actually requires a payment method other than in-app purchase for them. It is digital content specifically that must go through StoreKit and Play Billing.</p>
<h2>Can you change the bundle ID or signing key?</h2>
<p><strong>No. Base44's troubleshooting documentation states that both are configured automatically and cannot be changed inside the generated IPA or AAB files.</strong></p>
<p>If they do not match a version you uploaded previously, the stores block the update, and the documented remedy is to create a new app entry rather than to fix the mismatch. Three consequences follow, in ascending order of expense:</p>
<ol>
<li><p>The package name follows the documented <code>com.base[app-id].app</code> format, which is permanent and publicly visible in the Play Store URL</p>
</li>
<li><p>An app already on the stores from another toolchain cannot be updated with a Base44 build</p>
</li>
<li><p>Because the signing key is not yours, the identity of the app is not portable if you leave</p>
</li>
</ol>
<p>None of that bites on day one. All of it bites at exactly the moment you least want a new store listing.</p>
<h2>Can you choose which URL the app opens?</h2>
<p><strong>No. Base44 documents that the main entry URL is selected automatically from your published app, and that a different start page cannot currently be chosen for the app.</strong></p>
<p>For most apps this is invisible, because the published app is what you want to ship. It matters in two situations.</p>
<p>If you want the mobile app to open on a mobile-specific route rather than your web landing page, you cannot. And because the binary is bound to a Base44-controlled address rather than one you choose, moving off the platform later means the installed app no longer points at your product.</p>
<p>Owning a custom domain does not remove the constraint, but it is the thing that makes it survivable, which is why it is the first recommendation in the ownership guide.</p>
<h2>Can you control the permissions your app requests?</h2>
<p><strong>No. Base44 documents that every mobile app package includes device-level permissions, that an AI scan sets them, and that they are not editable in the interface.</strong></p>
<p>This is worth understanding because permissions are a rejection surface of their own. Guideline 5.1.1(iii) asks for data minimization, meaning apps should request access only to data relevant to core functionality, and reviewers do ask why a permission is present. If a permission is declared that your app does not visibly use, the answer has to come from your privacy policy and store listing rather than from a settings toggle.</p>
<p>There is a documented instance of exactly this: the iOS build has included a HealthKit entitlement without the matching <code>NSHealthShareUsageDescription</code> key, which surfaces as an App Store Connect rejection for apps that never touch health data. Base44 notes that HealthKit is not supported and that no action is required from developers whose apps do not use it.</p>
<h2>What operational limits show up during submission?</h2>
<p><strong>Three, all documented. Downloading store files requires the Builder plan or higher, push notifications do not reach existing installs until you ship an update, and every iOS build consumes an Apple distribution certificate slot.</strong></p>
<p>That last one catches teams that iterate. Apple allows up to 3 active production distribution certificates on the standard Apple Developer Program, and Base44 documents that generating an IPA creates one each time. The fourth generation returns an ALREADY_EXISTS error, and the fix is revoking a certificate you no longer need in your Apple Developer account.</p>
<p>Worth planning around rather than discovering: IPA generation is not a free action you can repeat casually while testing.</p>
<h2>Who handles the store review conversation?</h2>
<p><strong>You do. Base44 documents that its support does not track submission status, contact Apple or Google on your behalf, or manage review feedback, and that it cannot guarantee approval.</strong></p>
<p>That is an honest boundary and every platform is entitled to draw one. It is worth reading before you submit, because the moment a rejection letter arrives is the moment people go looking for help, and this determines where that help comes from.</p>
<p>If your team has never handled an App Review rejection, the difference between a vendor that draws this line and one that treats submissions as its core business is the difference between a two-day fix and a three-week loop.</p>
<h2>Which of these does a specialist runtime change?</h2>
<p><strong>Most of them, though it moves them rather than removes them. Apple and Google still own the store rules. What changes is which side of the constraint you sit on.</strong></p>
<table>
<thead>
<tr>
<th>Constraint</th>
<th>Base44 mobile app</th>
<th>Despia</th>
</tr>
</thead>
<tbody><tr>
<td>Native features</td>
<td>Web view plus push notifications</td>
<td>Over 50 capabilities via one JavaScript function</td>
</tr>
<tr>
<td>Digital purchases</td>
<td>No compliant path yet</td>
<td>RevenueCat paywalls, purchases and Customer Center</td>
</tr>
<tr>
<td>Bundle ID</td>
<td>Generated, not changeable</td>
<td>Chosen by you at project creation</td>
</tr>
<tr>
<td>Entry URL</td>
<td>Selected automatically</td>
<td>Any URL you control</td>
</tr>
<tr>
<td>Permissions</td>
<td>Set by AI scan, not editable</td>
<td>Declared per app</td>
</tr>
<tr>
<td>Offline</td>
<td>Not supported</td>
<td>Your own service worker, enabled by the runtime</td>
</tr>
<tr>
<td>HealthKit</td>
<td>Not supported</td>
<td>HealthKit and Health Connect</td>
</tr>
<tr>
<td>Native project export</td>
<td>Not offered</td>
<td>Full Xcode and Android Studio projects</td>
</tr>
<tr>
<td>Store review help</td>
<td>Out of scope for support</td>
<td>Core to the business</td>
</tr>
</tbody></table>
<p>The last row is the hardest to put in a table and the easiest to feel. Despia's runtime started as Advanced WebView in 2011 and carried over 7,500 apps before becoming Despia in 2023, which spans the introduction of guideline 4.2, several rewrites of the review rules, App Tracking Transparency and privacy manifests. Store submission is not a feature sitting next to the editor and the AI. It is the whole job.</p>
<h2>When do none of these limitations matter?</h2>
<p><strong>When your app sells nothing digital, needs no offline capability, requests no unusual permissions, and will never move off Base44.</strong></p>
<p>Add one more condition: the app is substantial enough on its own that push is a nice addition rather than the entire native story.</p>
<p>That describes a real and reasonable category of app, and for it the built-in path is genuinely fewer moving parts. One vendor, one dashboard, one bill. The reason to read this list is not that the constraints are unreasonable. It is that four of them are permanent decisions disguised as defaults, and the cheapest time to think about them is before the first submission rather than after.</p>
<h2>When do you need more than the store files?</h2>
<p><strong>When purchases, offline, biometrics, persistent storage or a bundle ID you chose are core to the product rather than optional.</strong></p>
<p>Base44 can scan your app against store guidelines, generate the IPA and the AAB, and add push notifications, and for apps that need nothing else that may be everything. Despia is what you reach for when the list above is not optional, from the same Base44 codebase you already have.</p>
<p><a href="https://setup.despia.com">Learn more about Despia in the docs</a></p>
]]></content:encoded></item><item><title><![CDATA[Base44 Bundle ID and Package Name: What to Know]]></title><description><![CDATA[Your bundle ID and package name are the permanent identity of your app on both stores. Base44 generates them for you in a fixed format derived from your app ID, and they cannot be changed in the gener]]></description><link>https://blog.despia.com/base44-bundle-id-and-package-name-what-to-know</link><guid isPermaLink="true">https://blog.despia.com/base44-bundle-id-and-package-name-what-to-know</guid><category><![CDATA[base44]]></category><category><![CDATA[iOS]]></category><category><![CDATA[ios app development]]></category><category><![CDATA[ios app developer]]></category><category><![CDATA[Android]]></category><category><![CDATA[android app development]]></category><dc:creator><![CDATA[Despia CEO]]></dc:creator><pubDate>Wed, 19 Aug 2026 21:38:18 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/67f0239ff4f5a0b7b4a852c4/1ab7764b-bdc7-4165-ac56-b8e5ac5308f9.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Your bundle ID and package name are the permanent identity of your app on both stores. Base44 generates them for you in a fixed format derived from your app ID, and they cannot be changed in the generated build. That is a five second decision at submission time with consequences you live with for the life of the listing.</p>
<h2>What is my Base44 package name?</h2>
<p><strong>Base44 documents the format directly: your Google Play package name is</strong> <code>com.base[app-id].app</code><strong>, where</strong> <code>[app-id]</code> <strong>is your Base44 app ID, taken from the URL of your app editor.</strong></p>
<p>An app whose editor URL contains <code>69e0c4bdd31bdu8fda51775g</code> gets the package name <code>com.base69e0c4bdd31bdu8fda51775g.app</code>. There is no field to change it. Base44's documentation states that the bundle ID and signing key are configured automatically and that these values cannot be changed inside the generated IPA or AAB files.</p>
<h2>Why does the package name matter after launch?</h2>
<p><strong>Because it is public and permanent. On Google Play the package name appears in the app's own store URL, so anyone looking at the address bar reads it.</strong></p>
<p>It is also the immutable key both stores use for the app, so it cannot be edited later.</p>
<p>Baked into that identifier on both platforms:</p>
<ul>
<li><p>The signing certificate and provisioning profile used to build the binary</p>
</li>
<li><p>The App Store Connect and Google Play Console records</p>
</li>
<li><p>Push notification credentials, App Groups, associated domains and every other entitlement granted to that identity</p>
</li>
</ul>
<p>This is not a platform quirk. Apple and Google both treat the app identifier as the unique key across their entire ecosystem, which is why neither offers an in-place rename. Changing it means a new store record, and a new store record does not inherit reviews, ratings or installs from the old one.</p>
<p>So the question is not whether you can change it later. It is what you want printed on the app for as long as it exists.</p>
<h2>Does carrying a builder's name in the identifier matter?</h2>
<p><strong>It depends on who reads it. For an internal tool or a side project, nobody looks.</strong></p>
<p>For an app you are selling, raising against, or licensing to an enterprise buyer, an identifier naming the platform you built on is a detail people notice.</p>
<p>Three situations where it comes up:</p>
<p><strong>Enterprise and procurement.</strong> Security reviews read package names. An identifier that resolves to a third-party app generation service invites a question about who controls the binary, and you answer it from a defensive position.</p>
<p><strong>Acquisition and diligence.</strong> A buyer looking at your app assets finds an identifier they cannot rebrand without abandoning the listing and its review history.</p>
<p><strong>Brand consistency.</strong> Everything else in your product carries your domain. The one string a curious user can read in the Play Store URL carries somebody else's.</p>
<p>None of this is fatal. It is just permanent, which is a strange property for a decision most people make without noticing they are making it.</p>
<h2>Can I set my own bundle ID with Despia?</h2>
<p><strong>Yes, at project creation. You choose the bundle ID and package name yourself, in the reverse domain format of your own domain, for example</strong> <code>com.yourcompany.yourapp</code><strong>.</strong></p>
<p>The same immutability rules apply afterwards, because they are Apple's and Google's rules rather than anyone's product decision. Once a build has been signed against an identifier, it cannot be edited from inside the editor, since changing it in place would invalidate every certificate, profile and store record tied to it.</p>
<p>What you get is a path that does not exist when the identifier is generated for you: if you set the wrong value before publishing, Despia support can repoint the project manually, typically within 72 hours. Email support with the old and new values and which platform it applies to. After the app is live on a store, a change starts a fresh identity with a new store record, so the practical rule is the same on any platform: get it right before you publish.</p>
<h2>Do I need a custom domain to submit?</h2>
<p><strong>Not for approval. Base44's documentation is clear that a custom domain is optional and that it can scan your app and generate the store files from the default Base44 URL.</strong></p>
<p>The reasons to own the domain are branding and portability, not store compliance. Portability is the one that compounds. The mobile app has to point at some address, and whichever address you choose becomes the thing the binary is bound to. If that address is a subdomain on the platform that built the app, then leaving the platform means changing the URL the shipped app loads, which is a new build at best.</p>
<p>If the address is a domain you own, it is a redirection layer you control. Point it at the Base44 app today, point it at something else later, and the installed app on every user's phone follows without a resubmission. It costs about the price of a coffee per year and it is the cheapest optionality you will ever buy.</p>
<h2>What happens when I outgrow the platform I started on?</h2>
<p><strong>The identifier and the codebase move separately, and only one of them is portable by default. This is the part worth thinking about before you pick a stack, not after.</strong></p>
<p>A Base44 app shipped through the built-in wrapper ties three things together: where the app is built, where it is hosted, and the identity it holds on both stores. Move the first one and you are re-entering the store as a different app, because the identifier you have cannot travel and the signing key is not yours.</p>
<p>Despia is framework agnostic. It ships whatever is served at a URL you control, so the arrangement is different in a useful way:</p>
<ul>
<li><p>The <strong>bundle ID and package name</strong> are yours, chosen up front</p>
</li>
<li><p>The <strong>domain</strong> is yours, which means the app points at an address you own rather than a subdomain on someone else's platform</p>
</li>
<li><p>The <strong>stack behind that domain</strong> can change without touching the app record</p>
</li>
</ul>
<p>Rebuild the front end in Next.js, hire developers and move to a different framework entirely, migrate the backend off the platform's managed services. As long as the same domain serves the app, the mobile app keeps its identity, its reviews, its ratings and its installs. If you eventually want out of the runtime too, the full Xcode and Android Studio projects export, so the escape hatch is a real project rather than a promise.</p>
<p>The general principle is the one worth taking away regardless of which tool you pick: <strong>choose the constraint you can undo.</strong> A framework choice is reversible. A hosting choice is reversible. A public, permanent app identifier tied to a platform you may leave is not.</p>
<h2>When is the generated identifier fine?</h2>
<p><strong>When the app is internal, experimental, or genuinely disposable, and when you have no plan to move.</strong></p>
<p>If you are validating an idea and the listing may not exist in six months, spending time on this is misallocated effort. It is also worth being honest about the failure mode in the other direction: picking a beautiful bundle ID for an app that never ships is not a win. The decision only matters in proportion to how long the app lives and who inspects it. If the answer to both is "not much," take the default and move on.</p>
<p>Where it stops being fine is the moment the app becomes the business. At that point the identifier is infrastructure, and infrastructure you cannot change is worth choosing deliberately.</p>
<h2>How do you ship under your own identifier?</h2>
<p><strong>Take the Base44 app you already built and ship it under an identifier you own, with the native capabilities the stores expect, from the same codebase.</strong></p>
<p>Code signing and submission run from the browser, with no CLI and no Mac required, and the full native projects export whenever you want them.</p>
<p><a href="https://setup.despia.com">See the setup docs at setup.despia.com</a></p>
]]></content:encoded></item><item><title><![CDATA[Despia vs Base44: Which One Ships Your Mobile App]]></title><description><![CDATA[These two get compared as if they compete, and for about 90% of their surface they do not. Base44 builds the app. Despia ships it to the App Store and Google Play as a real native binary. The overlap ]]></description><link>https://blog.despia.com/despia-vs-base44-which-one-ships-your-mobile-app</link><guid isPermaLink="true">https://blog.despia.com/despia-vs-base44-which-one-ships-your-mobile-app</guid><category><![CDATA[base44]]></category><category><![CDATA[webview]]></category><category><![CDATA[iOS]]></category><category><![CDATA[ios app development]]></category><category><![CDATA[ios app developer]]></category><category><![CDATA[Android]]></category><category><![CDATA[android app development]]></category><dc:creator><![CDATA[Despia CEO]]></dc:creator><pubDate>Wed, 19 Aug 2026 21:19:26 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/67f0239ff4f5a0b7b4a852c4/799701ee-5faf-49ce-a070-a9e95e662ae1.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>These two get compared as if they compete, and for about 90% of their surface they do not. Base44 builds the app. Despia ships it to the App Store and Google Play as a real native binary. The overlap is one step, the last one, and that is the only place a comparison means anything.</p>
<h2>Are Despia and Base44 competitors?</h2>
<p><strong>Only at the final step. Base44 is an AI app builder: you describe what you want, it writes and hosts the app, and it is good at that.</strong></p>
<p>Despia is a native runtime that ships a web app to both stores with device access. The two meet at submission. This matters because the framing decides the question. If you are choosing which tool to build your app in, Despia is not on the list, and neither of us would suggest otherwise. If you have already built in Base44 and you are working out how the mobile app gets made, then there are two paths and they lead to different places.</p>
<p>Most Despia customers use both. That is the normal configuration rather than a compromise.</p>
<h2>What is Base44 genuinely good at?</h2>
<p><strong>Getting from an idea to a working, hosted web app faster than almost anything else, and getting non-developers all the way there.</strong></p>
<p>The editor, the AI chat, the built-in backend and the one-click publishing are the product, and they are strong. The store submission tooling reflects that too. Base44 scans your app against Apple and Google guidelines, gives you a readiness score, offers AI-generated fixes for what fails, and generates a signed IPA and AAB from inside the editor. For a solo builder who has never opened App Store Connect, that is a real reduction in unfamiliar work.</p>
<p>Base44's documentation is also unusually straight about what the mobile wrapper does and does not do, which is more than most platforms offer.</p>
<h2>Where do the two paths diverge?</h2>
<p><strong>At what the mobile app is. The Base44 mobile app is documented as a web view that opens your published URL, with push notifications available.</strong></p>
<p>Despia is a native runtime with over 50 device capabilities callable from the same web code, plus full native project export. The practical divergence is not the count. It is which specific capabilities decide whether your app can ship at all.</p>
<table>
<thead>
<tr>
<th></th>
<th>Base44 mobile app</th>
<th>Despia</th>
</tr>
</thead>
<tbody><tr>
<td>What it is</td>
<td>Web view around your published URL</td>
<td>Native runtime, WKWebView and Chromium WebView</td>
</tr>
<tr>
<td>Native features</td>
<td>Push notifications</td>
<td>Over 50, called from one JavaScript function</td>
</tr>
<tr>
<td>Digital purchases</td>
<td>No compliant path documented yet</td>
<td>RevenueCat paywalls, purchases, Customer Center</td>
</tr>
<tr>
<td>Push</td>
<td>Sent by Base44 via your APNs key and Firebase</td>
<td>Sent by OneSignal, from an account you own</td>
</tr>
<tr>
<td>Biometrics</td>
<td>Not supported</td>
<td>Face ID and Touch ID bound to encrypted storage</td>
</tr>
<tr>
<td>Persistent storage</td>
<td>Not supported</td>
<td>Survives uninstall and reinstall, syncs per account</td>
</tr>
<tr>
<td>Offline</td>
<td>Not supported</td>
<td>Your own service worker, enabled by the runtime</td>
</tr>
<tr>
<td>Background audio</td>
<td>Not supported</td>
<td>Native player with lock screen controls</td>
</tr>
<tr>
<td>Health data</td>
<td>Not supported</td>
<td>HealthKit and Health Connect</td>
</tr>
<tr>
<td>Bundle ID</td>
<td>Generated, not changeable</td>
<td>Chosen by you at project creation</td>
</tr>
<tr>
<td>Entry URL</td>
<td>Selected automatically</td>
<td>Any URL you control</td>
</tr>
<tr>
<td>Native project export</td>
<td>Not offered</td>
<td>Full Xcode and Android Studio projects</td>
</tr>
<tr>
<td>Over-the-air updates</td>
<td>Web layer only</td>
<td>Remote hydration, no resubmission</td>
</tr>
<tr>
<td>Where it runs</td>
<td>Base44 hosting</td>
<td>Any host, your domain</td>
</tr>
</tbody></table>
<p>Three rows do most of the deciding.</p>
<p><strong>Digital purchases.</strong> Base44 documents that Stripe must not be used for digital goods in the mobile app, that both stores require their own billing, and that a StoreKit and Play Billing integration is in progress. If you sell subscriptions, that is not a preference, it is a blocked path.</p>
<p><strong>Bundle ID and entry URL.</strong> Both are generated and fixed, which means the app's permanent public identity and the address it loads are decided by the platform. Neither can be renamed later on either store.</p>
<p><strong>Native capability breadth.</strong> This is what guideline 4.2 is actually asking about, and an app whose entire native surface is push notifications is the case Apple's standard 4.2 rejection letter names by example.</p>
<h2>Why does Despia integrate OneSignal instead of building push?</h2>
<p><strong>Because the best version of a notification platform already exists and it is not something a mobile runtime should be trying to rebuild. Despia's principle is narrow: build the runtime, and integrate the category leader for everything else.</strong></p>
<p>The consequence for you is that you onboard to OneSignal directly and own that account. You get segmentation, scheduling with per-user local timezone delivery, Journeys automation, A/B testing, analytics, templates, in-app messages, email and SMS on the same user record, and integrations with the tools you already run. OneSignal has also opened a hosted MCP server on the open protocol, so Claude, ChatGPT, Cursor and Copilot can operate the account directly.</p>
<p>The same principle runs through billing. In-app purchases integrate the official RevenueCat SDK rather than a homegrown layer, and SDK versions are maintained and provisioned on every rebuild.</p>
<p>The test of a philosophy like this is whether it holds when you need something unusual, and here it does: the full OneSignal REST API is available to you, so you are never waiting on a wrapper method to be exposed for a field the provider already supports.</p>
<h2>Does it matter that Despia only does this?</h2>
<p><strong>It matters at the edges, and store submissions fail at the edges. Base44 is an AI builder where the mobile wrapper is one feature among many. Despia's runtime has done nothing but this since 2011.</strong></p>
<p>Both are legitimate positions, and they produce different release cadences. The runtime began as Advanced WebView in 2011 for agency client apps, and over 7,500 apps shipped on it before it became Despia in 2023. That window covers the introduction of guideline 4.2, several rewrites of the review rules, App Tracking Transparency, privacy manifests and two generations of submission tooling on both platforms. Keeping shipped apps compliant through each of those is not a feature you add to a roadmap.</p>
<p>It also sets the cadence. On a general-purpose platform, native capability competes for engineering time with the editor, the AI, the hosting and the billing, and it should, because those are the product. On a specialist runtime it is the product. Base44's documentation says it continues to expand native feature coverage and points to its release notes, and there is no reason to doubt the intent. The question a builder faces is not whether a capability is coming, it is whether it is there on the day you need to ship.</p>
<h2>What does using both actually look like?</h2>
<p><strong>You build in Base44 exactly as you do now, publish to a domain you own, point a Despia project at it, and add native calls from the same codebase.</strong></p>
<p>Nothing about the Base44 workflow changes, and content updates still publish without a store resubmission.</p>
<pre><code class="language-javascript">import despia from 'despia-native'

const isDespia = navigator.userAgent.toLowerCase().includes('despia')

if (isDespia) {
    // Store the session behind Face ID or Touch ID
    await despia('setvault://?key=sessionToken&amp;value=' + token + '&amp;locked=true')
}
</code></pre>
<p>Native calls sit inside the <code>isDespia</code> gate, so the same code runs unchanged in a browser. Global callbacks that the runtime invokes by name are assigned outside it.</p>
<h2>What happens if you outgrow Base44?</h2>
<p><strong>That depends on decisions made before you shipped rather than on anything you do at the time.</strong></p>
<p>If the identifier, the domain and the listings are yours and the app loads from an address you control, leaving is a DNS change. If not, it means a new store listing.</p>
<p>This is worth thinking about while the app has 0 users rather than 5,000, because reviews, ratings and install base all attach to the identifier and none of them survive a new one.</p>
<p>Despia is framework agnostic, so it ships whatever is served at a URL you control. Rebuild the front end in Next.js, hire developers, move the backend to your own Supabase project, and as long as the same domain serves the app, every installed copy follows without a resubmission. If you want out of the runtime as well, the full Xcode and Android Studio projects export and you continue with the same identifier and the same listings.</p>
<p>None of that predicts you will leave Base44. Plenty of apps stay for years and should. It is the difference between a tool you chose and a tool you are stuck with, and it costs one domain registration to keep the difference.</p>
<h2>When is the built-in path the better call?</h2>
<p><strong>When your app sells nothing digital, needs no offline capability, does not depend on biometrics or persistent credentials, and you have no plan to move off Base44.</strong></p>
<p>One vendor and one bill is a real advantage, and a second tool buys complexity you will not use. That is a genuine and reasonably common category. An internal tool, a members area, a booking front end for an existing business, a content app with enough substance that a reviewer immediately sees why it is an app. If that is your product, use the built-in path and spend the saved time on the app itself.</p>
<p>Where it stops being the right call is specific and testable. You sell subscriptions or credits. Your users need the app to work on a plane or a subway. You want a login that survives a reinstall. You want the identifier and the address to be yours because the app is the business. Any one of those is enough.</p>
<h2>Which one should you actually use?</h2>
<p><strong>If you are going to keep building in Base44, keep building there.</strong></p>
<p>Despia ships that app to the App Store and Google Play as a real native binary, gives it the device access the stores expect, and updates it over the air, from the one codebase you already have.</p>
<p><a href="https://setup.despia.com">Learn more about Despia in the docs</a> or <a href="https://despia.com">start building at despia.com</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Base44 Push Notifications and Apple Guideline 4.2]]></title><description><![CDATA[Base44 apps can send push notifications, and the setup is documented and straightforward. Push alone does not satisfy Apple's guideline 4.2. Apple's own minimum functionality rejection letter names pu]]></description><link>https://blog.despia.com/base44-push-notifications-and-apple-guideline-4-2</link><guid isPermaLink="true">https://blog.despia.com/base44-push-notifications-and-apple-guideline-4-2</guid><category><![CDATA[base44]]></category><category><![CDATA[iOS]]></category><category><![CDATA[ios app development]]></category><category><![CDATA[ios apps]]></category><category><![CDATA[webview]]></category><category><![CDATA[#ai-tools]]></category><category><![CDATA[No Code]]></category><category><![CDATA[ai agents]]></category><dc:creator><![CDATA[Despia CEO]]></dc:creator><pubDate>Wed, 19 Aug 2026 21:10:35 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/67f0239ff4f5a0b7b4a852c4/9021c391-6519-4edb-8ced-560a43e182e5.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Base44 apps can send push notifications, and the setup is documented and straightforward. Push alone does not satisfy Apple's guideline 4.2. Apple's own minimum functionality rejection letter names push notifications, Core Location and sharing as features that do not make an app robust enough on their own. Here is what does.</p>
<h2>What did Base44 actually ship?</h2>
<p><strong>Push notifications for the generated iOS and Android builds, configured in the same window where you generate your store files. It is a real feature and it works.</strong></p>
<p>It is also, per Base44's own documentation, the only native capability the Base44 mobile app currently supports. The setup runs through your own credentials on both platforms. On iOS you create an APNs auth key under Certificates, Identifiers and Profiles in your Apple Developer account, download the <code>.p8</code> file, and upload it with the matching Key ID. This is a different key from the App Store Connect API key you upload in the same flow, and both files end in <code>.p8</code>, so label them as you download them. Apple lets you download each one exactly once.</p>
<p>On Android you create your own Firebase project, register the Android app against the package name Base44 gives you, then upload two files: <code>google-services.json</code>, which is compiled into the build so the device can receive notifications, and a service account key, which lets Base44 send them on your behalf.</p>
<p>Sending happens from your app backend through the SendPushNotification integration. Each notification carries a title, a message, an optional action button label, and an optional link to a page inside your app.</p>
<p>One operational detail worth planning around: people who already installed your app do not start receiving notifications until you generate new store files and ship an update through App Store Connect and Play Console. Push is compiled into the shell, so turning it on is a new binary, not a publish.</p>
<h2>Does adding push notifications clear a guideline 4.2 rejection?</h2>
<p><strong>No. Guideline 4.2 asks for features, content and UI that elevate the app beyond a repackaged website.</strong></p>
<p>Push is one input to that judgment, and Apple's standard 4.2 rejection letter singles it out by name as something that does not carry the app on its own.</p>
<p>The guideline text itself, <a href="https://developer.apple.com/app-store/review/guidelines/">last updated June 8, 2026</a>, is short:</p>
<blockquote>
<p>Your app should include features, content, and UI that elevate it beyond a repackaged website. If your app is not particularly useful, unique, or "app-like," it doesn't belong on the App Store.</p>
</blockquote>
<p>The sentence people quote about push notifications is not in the guideline. It is in the rejection letter reviewers send under 4.2, and developers have posted it on Apple's own forums for years:</p>
<blockquote>
<p>Your app provides a limited user experience as it is not sufficiently different from a mobile browsing experience. As such, the experience it provides is similar to the general experience of using Safari. Including iOS features such as push notifications, Core Location, and sharing do not provide a robust enough experience to be appropriate for the App Store.</p>
</blockquote>
<p>That distinction matters. The rulebook states the standard. The letter tells you which specific additions reviewers have already decided do not meet it. If you are shipping a wrapper whose one native addition is push, the letter you are most likely to receive has a sentence in it about push.</p>
<p>Guideline 4.2.2 sits underneath the same idea: other than catalogs, apps should not primarily be marketing materials, advertisements, web clippings, content aggregators, or a collection of links.</p>
<p>Note that the guideline names 3 things, features, content and UI. Teams reading it after a rejection tend to fix only the first, add a capability, resubmit, and receive the same letter, because the interface half never changed. An app that navigates like a desktop website, with a sidebar, a hamburger menu or a cookie banner, reads as a website no matter what runs underneath it.</p>
<h2>Why is push a weaker differentiator than it used to be?</h2>
<p><strong>Because the web platform caught up. Web Push has worked on iOS and iPadOS since 16.4 for web apps added to the Home Screen, and Safari 18.4 added Declarative Web Push.</strong></p>
<p>The Badging API and the Web Share API are available in the same context. This is the part of the argument most wrapper marketing gets wrong, and getting it wrong is expensive, because any developer can dismantle the claim with one link. Push notifications are not something the web cannot do. Neither is location: the Geolocation API is standard and has been for well over a decade.</p>
<p>So the useful question is not what a browser is technically capable of. It is the one a reviewer is actually asking: <strong>what does the installed app experience add over opening the same site in Safari?</strong> If the honest answer is "notifications and an icon," you have described a Home Screen web app with an App Store listing attached, and 4.2 exists precisely for that case.</p>
<p>Examples of app-level integration that answer the question properly:</p>
<ul>
<li><p>StoreKit and Google Play Billing for digital purchases</p>
</li>
<li><p>A platform biometric flow tied to real credential storage</p>
</li>
<li><p>Native camera, photo picker and file picker flows</p>
</li>
<li><p>Native share integration in both directions</p>
</li>
<li><p>Contacts, calendar and precise or background location</p>
</li>
<li><p>Native haptics</p>
</li>
<li><p>Background audio with lock screen and Control Center controls</p>
</li>
<li><p>Storage that survives a cold start, and offline that survives one too</p>
</li>
<li><p>Deep links and universal links</p>
</li>
<li><p>Health data through HealthKit or Health Connect</p>
</li>
</ul>
<p>None of these are magic. What they have in common is that they are integrations with the operating system rather than features of the page.</p>
<h2>Can push notifications be the feature your app depends on?</h2>
<p><strong>No, and this is the trap that catches apps whose only native capability is push.</strong></p>
<p>Three separate guidelines constrain what you can do with it, and a wrapper leaning entirely on push tends to collide with at least one of them.</p>
<p><strong>Guideline 4.5.4</strong> states that push notifications must not be required for the app to function. If your onboarding dead-ends when a user declines the permission prompt, that is a rejection independent of 4.2.</p>
<p><strong>Guideline 5.1.2(i)</strong> goes further: your app may not require users to enable system functionality such as push notifications, location services or tracking in order to access functionality, content, use the app, or receive compensation of any kind. A "turn on notifications to continue" gate is a documented violation, not a growth tactic.</p>
<p><strong>Guideline 4.10</strong> prohibits monetizing built-in capabilities provided by the hardware or operating system, and names push notifications explicitly. Selling notifications as the premium tier is out.</p>
<p>Read together, the position is consistent: Apple treats push as an enhancement layered on top of an app that already justifies itself. An app whose value proposition is the notification has the dependency backwards.</p>
<h2>What does the Base44 mobile app support today?</h2>
<p><strong>A web view around your published Base44 app, plus push notifications. Base44's documentation is direct about the shape of it and about what is missing, which is worth reading before you plan a submission around it.</strong></p>
<p>From Base44's own store submission documentation: - The mobile app runs your published app inside a secure web view, described as a lightweight native wrapper that opens only your app's URL</p>
<ul>
<li><p>Full offline mode and HealthKit are not supported</p>
</li>
<li><p>Device permissions are set by an AI scan and are not editable in the Base44 interface</p>
</li>
<li><p>The main entry URL is chosen automatically from your published app and cannot be changed for the app specifically</p>
</li>
<li><p>Bundle ID and signing key are configured automatically and cannot be changed inside the generated IPA or AAB, and the package name follows a documented <code>com.base[app-id].app</code> format that is permanent and publicly visible in the Play Store URL</p>
</li>
<li><p>For digital goods, Stripe is not permitted inside the mobile app, and a StoreKit and Play Billing integration is described as in progress</p>
</li>
</ul>
<p>That last one is the sharpest constraint for anyone selling subscriptions. Apple and Google require their own billing systems for digital content, and Base44's documentation states plainly that an app using Stripe for digital content is rejected. Until the native billing integration ships, a Base44 app that sells digital content has no compliant path to the stores through the built-in wrapper.</p>
<p>There is also a known limitation where the iOS build includes a HealthKit entitlement without the matching <code>NSHealthShareUsageDescription</code> key, which surfaces as an App Store Connect rejection even for apps that never touch health data.</p>
<p>Base44's documentation also says the platform continues to expand native feature coverage and points readers to its release notes. That is a fair statement of intent and there is no reason to doubt it. It does mean the practical question for anyone planning a submission is not whether a capability is coming, but whether it is there on the day you need to ship.</p>
<h2>Does it matter that Despia only does this?</h2>
<p><strong>It matters at the edges, and store submissions fail at the edges. Base44 is an AI app builder where the mobile wrapper is one feature among many. Despia's runtime has done nothing but this since 2011.</strong></p>
<p>Both are legitimate positions, and they produce different release cadences. The runtime started life in 2011 as Advanced WebView, built for agency client apps, and over 7,500 apps shipped on it before it became Despia in 2023. That period covers the introduction of guideline 4.2, several rewrites of the review rules, the arrival of App Tracking Transparency, the privacy manifest requirements, and two full generations of store submission tooling on both platforms. Watching those land, one after another, and keeping shipped apps compliant through each of them is not a feature you can add to a roadmap. It is just time.</p>
<p>That shows up in two places a builder actually feels.</p>
<p><strong>Coverage.</strong> Over 50 native capabilities against a wrapper that currently supports push notifications. The number matters less than the shape of the gap: in-app purchases, biometrics, persistent credential storage, offline, background audio and health data are the ones that decide whether a specific app can ship at all.</p>
<p><strong>Cadence.</strong> On a general-purpose platform, native capability competes for engineering time with the editor, the AI, the hosting, the backend and the billing, and it should, because those are the product. On a specialist runtime it is the product. When a store policy changes or a new platform capability lands, the response time is different, and if you want to be among the first to ship against something new, that difference is the whole reason to pick one over the other.</p>
<p>None of this makes the built-in wrapper a bad choice. Fewer vendors is a genuine advantage, and plenty of apps never need a capability the wrapper lacks. The trade is straightforward: one platform to manage against a deeper native surface and a team whose entire job is the part where submissions get rejected.</p>
<h2>Who actually sends the notification?</h2>
<p><strong>With the Base44 wrapper, Base44 does, using the APNs key and Firebase service account you upload. With Despia, OneSignal does, from an account you own and log into.</strong></p>
<p>That difference has almost nothing to do with delivery and almost everything to do with what surrounds the send. Despia does not run its own push service, and that is a deliberate decision rather than a gap. The principle is narrow and easy to state: build the runtime, and integrate the category leader for everything else. Nobody needs a fifth-best notification backend bolted onto a mobile runtime, and building one would mean spending years rebuilding a product that already exists and is better than anything we would ship.</p>
<p>OneSignal has been building messaging infrastructure since 2014 and does nothing else. It describes itself as serving over 1 million businesses and delivering more than 12 billion messages a day. When you ship a Despia app, you onboard to OneSignal directly, which means you get the platform rather than an endpoint:</p>
<ul>
<li><p>A dashboard with delivery, open and conversion analytics rather than a send confirmation</p>
</li>
<li><p>Segmentation and targeting, so a campaign goes to the users who should get it</p>
</li>
<li><p>Scheduling, including delivery at a chosen local time in each user's own timezone</p>
</li>
<li><p>Journeys, which is the automation layer for onboarding sequences, win-back flows and lifecycle messaging</p>
</li>
<li><p>A/B testing and templates</p>
</li>
<li><p>In-app messages, email and SMS on the same user record, so retention is one system rather than four</p>
</li>
<li><p>Integrations with the tools you already use, including Zapier, n8n, warehouses and analytics platforms</p>
</li>
</ul>
<p>OneSignal has also opened both an in-platform AI assistant and a hosted MCP server to all users. The MCP server runs on the open Model Context Protocol with OAuth, so Claude, ChatGPT, Cursor and Copilot can operate your OneSignal account directly across messaging, users, segments, templates, exports and analytics. Practically, that means you can build a segment, draft the copy and schedule the send in one conversation, and orchestrate it alongside whatever else your agent has access to.</p>
<p>None of that is push infrastructure. It is the marketing and retention layer that sits on top of push infrastructure, and it is the reason the choice of provider matters after launch rather than at integration time.</p>
<h2>What happens when you need a feature the runtime has not shipped?</h2>
<p><strong>For push, usually nothing, because the OneSignal REST API is available to you directly.</strong></p>
<p>You are not waiting on a wrapper method to be exposed for each field, and you are not filing a request for something OneSignal already supports.</p>
<p>That is the practical payoff of integrating an established provider instead of inventing one. Your backend calls OneSignal, and the full payload surface is yours:</p>
<pre><code class="language-javascript">// Backend: targeted send with in-app routing, no reload
await fetch('https://onesignal.com/api/v1/notifications', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        Authorization: `Basic ${process.env.ONESIGNAL_REST_API_KEY}`,
    },
    body: JSON.stringify({
        app_id: process.env.ONESIGNAL_APP_ID,
        include_external_user_ids: [userId],
        headings: { en: 'Order shipped' },
        contents: { en: 'Track it now' },
        data: { path: '/orders/1042' },
    }),
})
</code></pre>
<p>Routing is worth calling out because it is the difference between a notification that works and one that annoys people. Sending <code>data.path</code> updates the route with <code>pushState</code> and fires <code>popstate</code>, so the app moves to the right screen without a reload. The legacy <code>url</code> field triggers a full navigation and should be used only when a reload is genuinely wanted.</p>
<p>Two push features do need us, and both for the same reason: they touch the binary rather than the API.</p>
<p><strong>Custom notification sounds.</strong> The audio file has to be compiled into the app, so it cannot be added from a dashboard. Email <a href="mailto:support@despia.com">support@despia.com</a> with a <code>.wav</code> file and we add it to your build, after which you select it from your payload like any other OneSignal option.</p>
<p><strong>Critical alerts.</strong> These bypass Do Not Disturb and silent mode, so Apple gates them behind a dedicated entitlement you request directly from Apple. Approval is a manual review and can take several weeks. Once Apple grants it, contact support with the confirmation, we enable it on your project, you rebuild, and then <code>ios_critical_alert: 1</code> works in your payload. The gate is Apple's rather than ours, which is why the approval has to be yours.</p>
<p>Rich push with images is a third case worth planning for, though it is setup rather than a request: on iOS it needs the notification service extension and a matching App Group configured against your bundle ID.</p>
<p>The same philosophy runs through the rest of the runtime. In-app purchases integrate the official RevenueCat SDK rather than a homegrown billing layer, and the SDK versions are maintained and provisioned on every rebuild, so you are not tracking StoreKit and Google Play Billing releases yourself.</p>
<p>If something reasonable is genuinely missing, <a href="mailto:features@despia.com">features@despia.com</a> goes to the engineering team. There is no promise attached to that address, and a capability only one app in the world needs is not going to jump the queue. A feature that is executable, sensible and useful to more than one builder tends to get prioritized, and it ships quickly, because most of the engineers here have been doing this specific work for 15 years.</p>
<h2>How does the Despia runtime differ?</h2>
<p><strong>Despia is a native runtime rather than a shell around a URL.</strong></p>
<p>Your Base44 app stays the source of truth, and native capabilities are called from that same web codebase through a single JavaScript function, with over 50 available.</p>
<table>
<thead>
<tr>
<th></th>
<th>Base44 mobile app</th>
<th>Despia</th>
</tr>
</thead>
<tbody><tr>
<td>Rendering</td>
<td>Web view around your published URL</td>
<td>WKWebView on iOS, Chromium WebView on Android</td>
</tr>
<tr>
<td>Push notifications</td>
<td>Sent by Base44, via your APNs key and Firebase</td>
<td>Sent by OneSignal, from an account you own</td>
</tr>
<tr>
<td>In-app purchases</td>
<td>Not supported, Stripe not permitted</td>
<td>RevenueCat paywalls, purchases and Customer Center</td>
</tr>
<tr>
<td>Biometrics</td>
<td>Not supported</td>
<td>Face ID and Touch ID, bound to encrypted storage</td>
</tr>
<tr>
<td>Persistent storage</td>
<td>Not supported</td>
<td>Storage Vault, survives uninstall and reinstall</td>
</tr>
<tr>
<td>Offline</td>
<td>Not supported</td>
<td>Your own service worker, enabled by the runtime</td>
</tr>
<tr>
<td>Background audio</td>
<td>Not supported</td>
<td>Native player with lock screen and Control Center controls</td>
</tr>
<tr>
<td>Health data</td>
<td>Not supported</td>
<td>HealthKit and Health Connect</td>
</tr>
<tr>
<td>Haptics</td>
<td>Not supported</td>
<td>Supported</td>
</tr>
<tr>
<td>Bundle ID</td>
<td>Generated, not changeable</td>
<td>Chosen by you at project creation</td>
</tr>
<tr>
<td>Over-the-air updates</td>
<td>Web layer only</td>
<td>Remote hydration, no resubmission</td>
</tr>
<tr>
<td>Native project export</td>
<td>Not offered</td>
<td>Full Xcode and Android Studio projects</td>
</tr>
</tbody></table>
<p>Two of those rows do most of the work in a 4.2 conversation.</p>
<p><strong>Storage Vault</strong> is encrypted key-value storage backed by iCloud Key Value Store on iOS and Android Key/Value Backup on Android. Values survive uninstall and reinstall and sync across devices on the same Apple ID or Google account. Setting <code>locked=true</code> on a value means reading it back requires Face ID or Touch ID, which turns "biometric login" from a UI gesture into an actual credential flow.</p>
<pre><code class="language-javascript">import despia from 'despia-native'

const isDespia = navigator.userAgent.toLowerCase().includes('despia')

if (isDespia) {
    // Store the session token behind biometrics
    await despia('setvault://?key=sessionToken&amp;value=' + token + '&amp;locked=true')

    // Reading it prompts for Face ID or Touch ID
    const data = await despia('readvault://?key=sessionToken', ['sessionToken'])
    resumeSession(data.sessionToken)
}
</code></pre>
<p>That is a native capability with no web equivalent in the relevant sense: a token that outlives a reinstall and is gated by the platform's own authentication. It is also the answer to two product problems at once, since the same persistence is what stops trial abuse by reinstall.</p>
<p><strong>RevenueCat</strong> solves the billing problem without you writing against StoreKit and Google Play Billing separately. Those are two different APIs with two different lifecycles, and maintaining both is the reason most small teams get subscriptions wrong. With Despia you present a paywall configured in the RevenueCat dashboard, check entitlements on load, and open the native Customer Center for restores, subscription management and refund requests.</p>
<pre><code class="language-javascript">if (isDespia) {
    despia('revenuecat://launchPaywall?external_id=' + userId + '&amp;offering=default')
}

window.onRevenueCatPurchase = () =&gt; checkEntitlements()
</code></pre>
<h2>When is the Base44 wrapper the right call?</h2>
<p><strong>When your app does not sell digital content, does not need to work offline, does not need biometrics or persistent credentials, and notifications genuinely are the only device integration your product design calls for.</strong></p>
<p>That is a real category. An internal tool, a members area, a booking front end for a business that already has a website, a content app where the reviewer can see a clear reason to install it. If your app is substantial in its own right and push is a nice addition rather than the whole native story, the built-in path is fewer moving parts and fewer accounts to manage, and it is free of the Stripe constraint if you sell nothing digital in the app.</p>
<p>Where it gets expensive is the case Base44 documents most clearly: you sell subscriptions. There is no compliant route through the built-in wrapper today, and no amount of push configuration changes that.</p>
<h2>What is the next step once the app is review-ready?</h2>
<p><strong>Once the app itself is review-ready, the remaining problem is getting a compliant binary signed and onto both stores.</strong></p>
<p>Base44 can scan your app, generate the IPA and the AAB, and add push notifications, and for apps that need nothing else that may be all you require. Despia is what you reach for when in-app purchases, biometrics, persistent storage, offline or background audio are core to the product rather than optional, from the same Base44 codebase you already have.</p>
<p><a href="https://setup.despia.com">Learn more about Despia in the docs</a> or <a href="https://despia.com">start building at despia.com</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Base44 App Rejected Under Guideline 5.6: The Fix]]></title><description><![CDATA[If a Base44 app receives a 5.6 letter despite hiding nothing, similarity is one of the first things worth checking, and the legacy login screen is where it shows most. Base44 now ships customizable lo]]></description><link>https://blog.despia.com/base44-app-rejected-under-guideline-5-6-the-fix</link><guid isPermaLink="true">https://blog.despia.com/base44-app-rejected-under-guideline-5-6-the-fix</guid><category><![CDATA[base44]]></category><category><![CDATA[webapp]]></category><category><![CDATA[webview]]></category><category><![CDATA[iOS]]></category><category><![CDATA[ios app development]]></category><category><![CDATA[ios app developer]]></category><category><![CDATA[AppReview]]></category><category><![CDATA[appstore]]></category><dc:creator><![CDATA[Despia CEO]]></dc:creator><pubDate>Wed, 19 Aug 2026 13:17:04 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/67f0239ff4f5a0b7b4a852c4/a845b79f-1c68-4214-a3d0-9c6f6c0243bb.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If a Base44 app receives a 5.6 letter despite hiding nothing, similarity is one of the first things worth checking, and the legacy login screen is where it shows most. Base44 now ships customizable login pages. Here is what Apple checks and how to clear it.</p>
<h2>What does a guideline 5.6 letter actually mean?</h2>
<p>It means Apple invoked the Developer Code of Conduct rather than a technical rule. 5.6 covers respectful communication, customer trust, ratings integrity, accurate developer identity, and app quality, and it carries account-level implications rather than applying only to one submission.</p>
<p>The wording is what alarms people. You get told there is a pattern of unusual behavior commonly associated with fraudulent activity, and that features appear to have been hidden during review, when you hid nothing at all.</p>
<p>The weight is real, though. Apple states that a Developer Program account will be terminated for actions not in accordance with the code, and that restoring it means submitting a written statement of the improvements you plan to make, which Apple has to approve. For the full breakdown, see our <a href="https://blog.despia.com/guideline-5-6-rejection-why-ai-built-apps-get-flagged">guideline 5.6 rejection guide</a>.</p>
<p>References throughout are checked against the <a href="https://developer.apple.com/app-store/review/guidelines/">App Review Guidelines</a> as updated on June 8, 2026.</p>
<h2>What actually triggered the rejection?</h2>
<p>Similarity is the first thing worth investigating. Apple's published 2025 numbers show enforcement volume sitting far more in the copycat and spam category than in apps hiding functionality, though aggregate totals cannot diagnose an individual case.</p>
<p>Apple's App Review team evaluated more than 9.1 million submissions in 2025. Over 371,000 were rejected for copying other apps, spam, or misleading users, against roughly 22,000 for hidden or undocumented features. The larger bucket is roughly 17 times the one your letter names. Source: <a href="https://www.apple.com/newsroom/2026/05/the-app-store-stopped-over-2-point-2-billion-usd-in-fraudulent-transactions-in-2025/">Apple's 2025 App Store fraud analysis</a>.</p>
<p>Apple also states that its review systems use AI to analyze app similarity and flag problematic changes in updates, and that AI development tools are driving the submission volume behind that. Guideline <strong>4.3(b)</strong> is the written version: do not submit apps that are indistinguishable from what is already widely available.</p>
<p>Base44 gets you a working app very fast. What it cannot do is make your app look like nobody else's, and that is the part review is measuring.</p>
<h2>Why is the Base44 login screen the problem?</h2>
<p>Because the legacy Base44 login screen appears across submissions from unrelated developers, and it is the first thing a reviewer sees on an app they cannot otherwise verify. Apple does not disclose screen weighting, so treat it as the first similarity hotspot worth eliminating.</p>
<p>You know the screen: centered card, rounded icon tile, "Welcome back", Continue with Google, an OR divider, email and password fields with small leading icons, a full-width button, "Don't have an account? Create one" underneath.</p>
<p>It is a good screen. That is exactly the problem. Nothing on it belongs to your product, and it sits in front of everything a reviewer would otherwise use to tell your app apart.</p>
<h2>How do you switch to custom login pages?</h2>
<p>Base44 rolled out custom login pages to all users on 2 June 2026. New apps ship with editable login, register, forgot password, and reset password pages. Existing apps on the old built-in pages migrate through the App Visibility setting in six clicks.</p>
<ol>
<li><p>Open your app editor and click <strong>Dashboard</strong>.</p>
</li>
<li><p>Click <strong>Overview</strong>.</p>
</li>
<li><p>Open the <strong>App Visibility</strong> dropdown.</p>
</li>
<li><p>Select <strong>Public (no login)</strong>.</p>
</li>
<li><p>Read the modal, then click <strong>Enable custom auth</strong>.</p>
</li>
<li><p>Base44 writes a wiring prompt into the AI chat to connect the new pages to your routes. Let it run.</p>
</li>
</ol>
<p>Apps already on custom login pages show a single Public option and need no change. Once migrated, the pages live inside your app, so you can restyle them, translate them, and brand them like any other screen.</p>
<h2>How do you redesign the auth pages?</h2>
<p>Change the composition, not the palette. Move off the centered card, set a real typeface, rewrite every string in your product's voice, and make the pages visually continuous with your onboarding screen.</p>
<ol>
<li><p><strong>Change the composition.</strong> A split layout with brand imagery on one side, a top-anchored form under a full-bleed header, or a two-step email-then-password flow all read as deliberate.</p>
</li>
<li><p><strong>Set a typeface.</strong> The highest-leverage single change available. The default system stack is the most recognizable tell in a generated app.</p>
</li>
<li><p><strong>Rewrite every string.</strong> "Welcome back" and "Log in to your account" appear on an enormous number of screens.</p>
</li>
<li><p><strong>Match your onboarding.</strong> Most Base44 apps have a strong branded first screen and then drop into an unbranded login. That seam is what a reviewer registers as a shell around a template.</p>
</li>
</ol>
<p>Changing the accent color from purple to teal is not a redesign. The layout is the fingerprint.</p>
<p>Paste this into Base44's AI chat to get a redesign rather than a recolor:</p>
<pre><code class="language-plaintext">CONTEXT
This is a Base44 app. The auth pages are close to the default and Apple
rejected the submission under guideline 5.6 for resembling other template
submissions.

TASK
1. Redesign the login, register, forgot password and reset password pages
   using a composition that is NOT a centered card with a stacked field group.
2. Apply the typeface, spacing scale, radius and background treatment from our
   onboarding screen so the whole app reads as one product.
3. Rewrite every string on these pages in our product's voice.
4. Keep all existing auth behavior, routes and redirects working exactly as
   they do now.

RULES
Never change only the accent color and call it a redesign.
Never leave a default template string on these pages.
Never break sign-in for existing accounts.

DONE WHEN
The auth pages are visually continuous with onboarding, no original template
string remains, and an existing account signs in on a real device.
</code></pre>
<h2>Is building with AI against App Review rules?</h2>
<p>No. Apple regulates what the resulting app does and how it is submitted. Where AI-built apps run into trouble is low-effort similarity, and a default theme shipped untouched is what that looks like from outside.</p>
<p>Set your tokens before you build more screens: palette, radius scale, typeface, spacing rhythm, border weight, shadow treatment. Every component generated afterwards inherits your identity instead of the platform's default.</p>
<p>Do the same with the icon. A model produces one in 5 seconds and it looks like every other icon produced that way. Twenty minutes in a real design tool removes an entire category of similarity signal.</p>
<h2>Do you need a custom domain?</h2>
<p>Not for approval. Base44 states you do not need one to submit to the App Store or Google Play, and can generate the store files from your default Base44 URL. A custom domain is a branding decision, not a review requirement.</p>
<p>The branding case is still worth making. A default platform subdomain turns up in your app, your emails, and your store listing, and it reads as a prototype to everyone who sees it.</p>
<p>What does matter at review time is that the app Apple sees is the app you meant to submit. Base44 builds against your published app, so publish before you generate store files, then open that exact URL on a phone and walk through it.</p>
<p>Apple does not require you to submit against production specifically, but the features you are submitting have to be accessible to App Review under guidelines <strong>2.1(a)</strong> and <strong>2.3.1(a)</strong>. Separately, guideline <strong>2.5.2</strong> prohibits downloading, installing, or executing code that introduces or changes features or functionality after review. Content updating remotely is normal and expected. Materially different functionality switched on after approval is not.</p>
<h2>How do quality problems become a conduct issue?</h2>
<p>Through guideline 5.6.4, which names excessive customer reports, negative reviews, and refund requests as indications of failing to maintain quality, and says that inability to maintain quality may factor into whether a developer abides by the code of conduct.</p>
<p>A broken screen is already a 2.1 completeness problem, since Apple rejects incomplete binaries, crashes, and obvious technical issues. What 5.6.4 adds is that the same failure can also feed a conduct judgment rather than staying purely technical.</p>
<p>That matters because a reviewer who suspects a template spends the rest of their time looking for confirmation. Before submitting, walk the whole app on a real device: open every screen, tap every button, complete every form to the last field. Anything that dead-ends converts a suspicion into a decision.</p>
<h2>Are your store claims accurate?</h2>
<p>Guideline 5.6.2 requires your representation of yourself, your business, and your offerings to be accurate, truthful, relevant, and up to date. Read your listing as a reviewer would, then delete anything you could not evidence.</p>
<p>This catches AI-built apps more than people expect, because generated marketing copy overstates by default. Any claim about who built the app, what qualifications sit behind it, what it is certified to do, or what results it produces has to be true and verifiable. Professional titles and credentials count.</p>
<p>While you are in the metadata, guideline <strong>2.3.3</strong> states screenshots should show the app in use, not merely title art, a login page, or a splash screen. Leading with your auth screen is both a similarity signal and a compliance problem.</p>
<h2>How do you make the app reviewable?</h2>
<p>Give review working demo credentials, keep the backend live, and describe every feature and the path to it in Notes for Review. Apple states outright that generic descriptions will be rejected.</p>
<p>Guideline <strong>2.1(a)</strong> requires final versions with complete metadata, working URLs, no placeholder content, demo account details when there is a login, and your backend live during review. Guideline <strong>2.3.1(a)</strong> goes further, requiring features to be accessible to review and every new feature to be described with specificity.</p>
<p>In App Store Connect:</p>
<ol>
<li><p>Open your app, then the version you are submitting.</p>
</li>
<li><p>Scroll to <strong>App Review Information</strong>.</p>
</li>
<li><p>Tick <strong>Sign-in required</strong> and enter demo credentials.</p>
</li>
<li><p>Verify those exact credentials on a real device the same day you submit.</p>
</li>
<li><p>In <strong>Notes</strong>, name each non-obvious feature and the path to it. Name the tab, the button, the screen. Not "various improvements".</p>
</li>
<li><p>Say how to reach anything gated behind a purchase, a role, or a region.</p>
</li>
<li><p>Confirm your support URL and privacy policy URL both load.</p>
</li>
</ol>
<h2>Does guideline 4.2.6 ban building on Base44?</h2>
<p>No. It requires the app to be submitted directly by the provider of its content, rather than mass-submitted by a platform from its own account. Meeting that ownership condition is necessary rather than sufficient, so the customization work still applies.</p>
<p>The full rule says apps created from a commercialized template or app generation service will be rejected unless submitted directly by the provider of the app's content. It exists to stop agencies pushing near-identical client apps from one account.</p>
<p>Apple then goes further in your favor: it says those services should offer tools that let their clients create customized, innovative apps that provide unique customer experiences. That is the same standard as everything above, written by Apple rather than argued by us.</p>
<h2>What do you do if you already have the letter?</h2>
<p>Do not resubmit the same binary with a bumped version. Make the changes, reply in Resolution Center before submitting the new build, and list what changed screen by screen with screenshots attached.</p>
<p>Apple states that review takes longer when an app is repeatedly rejected for the same guideline violation, so a second identical submission works against you.</p>
<pre><code class="language-plaintext">We have addressed the issues as follows.

Sign-in: moved to custom login pages and redesigned with our own layout,
typography and copy. It is now continuous with our onboarding screen.

Visual identity: replaced the default theme with our own palette, radius and
typography across the app.

App icon and store screenshots: replaced. Screenshots now show the app in use.

Metadata: reviewed the listing and removed claims we cannot evidence.

Review access: demo account added under App Review Information and verified
today. Notes for Review describe each feature and where to find it.

The submitted build points at the same domain our users get. No functionality
is gated, remote-flagged or withheld from review.
</code></pre>
<p>No argument and no emotion. Evidence of change is what moves a 5.6 letter.</p>
<h2>Is Base44 why your app was rejected?</h2>
<p>No. Similarity, quality, accurate representation, and verifiability are what Apple's review rules evaluate, and none of them is a build tool. A poorly differentiated app written in Swift gets flagged on the same signals.</p>
<p>What gets flagged is an app that arrives looking like every other app from the same starter, with claims nobody checked and a reviewer who cannot get in. Custom login pages, your own theme, a real icon, and a filled-in review notes field close that gap, and none of it takes more than an afternoon.</p>
<h2>Do you need more than Base44's own store files?</h2>
<p>Only if you need native capability. Base44 can scan your app and generate the IPA and AAB you submit through your own developer accounts, which is enough for many products. Its docs list push notifications and full offline mode as unsupported in that wrapper.</p>
<p>Base44's own documentation describes the mobile app as a secure web view that opens your app's URL, which is why those two capabilities sit outside it.</p>
<p>If they are core to your product, that is where Despia fits. Same Base44 codebase, still shipped under your own developer account, with 50+ native device features available from it, push notifications and offline behavior among them, plus content updates over the air. Code signing and submission run from the browser, with no Mac and no Xcode required.</p>
<p><a href="https://setup.despia.com">See the setup docs at setup.despia.com</a></p>
]]></content:encoded></item><item><title><![CDATA[Lovable App Rejected Under Guideline 5.6: The Fix]]></title><description><![CDATA[Two things are worth checking first on a Lovable app that got a 5.6 letter: an untouched default design, and a build pointed at the editor preview rather than the published app. Neither is concealment]]></description><link>https://blog.despia.com/lovable-app-rejected-under-guideline-5-6-the-fix</link><guid isPermaLink="true">https://blog.despia.com/lovable-app-rejected-under-guideline-5-6-the-fix</guid><category><![CDATA[lovable]]></category><category><![CDATA[webapp]]></category><category><![CDATA[appstore]]></category><category><![CDATA[AppReview]]></category><category><![CDATA[iOS]]></category><category><![CDATA[ios app development]]></category><category><![CDATA[ios app developer]]></category><dc:creator><![CDATA[Despia CEO]]></dc:creator><pubDate>Wed, 19 Aug 2026 13:06:41 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/67f0239ff4f5a0b7b4a852c4/bf6cb449-de8f-4595-ac96-2d599597e24d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Two things are worth checking first on a Lovable app that got a 5.6 letter: an untouched default design, and a build pointed at the editor preview rather than the published app. Neither is concealment. Here is what Apple checks and how to clear it.</p>
<p>You built in Lovable, wrapped it, submitted it, and got a rejection citing guideline 5.6, a pattern of unusual behavior commonly associated with fraudulent activity, and features that appear to have been hidden during review. Nothing was hidden.</p>
<p>Guideline 5.6 is the Developer Code of Conduct. It covers conduct, customer trust, and accurate representation, and unlike a purely technical rejection it carries account-level implications: Apple states that a Developer Program account will be terminated for actions not in accordance with the code, and that restoring it requires a written statement of improvements Apple has to approve. That is why the wording is so much heavier than a 4.3 or a 2.1.</p>
<p>For the full breakdown of what guideline 5.6 means at the account level, see our <a href="https://blog.despia.com/guideline-5-6-rejection-why-ai-built-apps-get-flagged">guideline 5.6 rejection guide</a>.</p>
<p>Two Lovable-specific things are worth checking first. One is visual, one is procedural. References below are checked against the <a href="https://developer.apple.com/app-store/review/guidelines/">App Review Guidelines</a> as updated on June 8, 2026.</p>
<h2>What actually triggered the rejection?</h2>
<p>Similarity is the first thing worth investigating. Apple's published 2025 numbers show enforcement volume sitting far more in the copycat and spam category than in apps hiding functionality, though aggregate totals cannot diagnose an individual case.</p>
<p>Apple's App Review team evaluated more than 9.1 million submissions in 2025. Over 371,000 were rejected for copying other apps, spam, or misleading users, against roughly 22,000 for hidden or undocumented features. Apple also states that its review systems analyze app similarity as an input, and that AI development tools are driving the volume behind it. Source: <a href="https://www.apple.com/newsroom/2026/05/the-app-store-stopped-over-2-point-2-billion-usd-in-fraudulent-transactions-in-2025/">Apple's 2025 App Store fraud analysis</a>.</p>
<p>Those are global category totals, so they do not tell you why any single app was flagged. What they do tell you is where the enforcement volume sits: the bucket about apps resembling other apps is roughly 17 times the one the letter names. Guideline <strong>4.3(b)</strong> says it plainly: do not submit apps that are indistinguishable from what is already widely available.</p>
<h2>Is the shadcn default theme the problem?</h2>
<p>The theme is, not the library. shadcn/ui lands in your repo as source you own, with the visual identity in CSS variables you are meant to set. Leaving those at their defaults is what makes a thousand projects look alike.</p>
<p>Lovable uses Tailwind for styling, and many Lovable projects use shadcn/ui components. The underlying frontend stack varies by project age: newer apps default to TanStack Start with server-side rendering, while older ones use React with Vite. shadcn/ui is well built and accessible, and it is explicitly designed to be customized: the components land in your repo as source you own, and the visual identity lives in CSS variables you are meant to set.</p>
<p>What produces the generic result is the agent scaffolding it and stopping there. A thousand projects ship the same neutral palette, the same default radius, the same system font stack, the same card-and-border rhythm. The library handed you a solid vanilla base. Leaving it vanilla is the choice that gets noticed.</p>
<p>Being built with AI is not itself an App Review violation. Apple regulates what the resulting app does and how it is submitted. Where AI-built apps run into trouble is low-effort similarity, and an untouched default theme is what that looks like from outside.</p>
<p><strong>Set your tokens before you build any more screens.</strong> In Lovable this lives in your global stylesheet and Tailwind config, and doing it first means every component the agent generates afterwards inherits your identity instead of the default one.</p>
<ol>
<li><p><strong>Palette.</strong> Define your own foreground, background, primary, muted, border and destructive values. Not one hue swap on the default.</p>
</li>
<li><p><strong>Radius.</strong> One of the most recognizable tells. Change it, and change it consistently.</p>
</li>
<li><p><strong>Typeface.</strong> The highest-leverage single change in the list. Load one real display face for headings and check it renders on device, not just in preview.</p>
</li>
<li><p><strong>Density and border weight.</strong> Card padding, gap scale, border thickness, shadow treatment. Together these read as a designed product where a color change does not.</p>
</li>
</ol>
<p>Then the auth screens, which are what a reviewer sees first on an app they cannot otherwise verify:</p>
<pre><code class="language-plaintext">CONTEXT
This project is on the default shadcn theme and Apple rejected the submission
under guideline 5.6 for resembling other template submissions.

TASK
1. Update our CSS variables and Tailwind config with the palette, radius scale,
   typeface and spacing rhythm below, then make every existing component read
   from those tokens rather than hardcoded values.
2. Redesign the sign-in, sign-up and forgot-password screens using a
   composition that is NOT a centered card with a stacked field group.
3. Rewrite every string on the auth screens in our product's voice. No
   "Welcome back", no "Log in to your account".
4. Keep all auth logic, routes and redirects working exactly as they do now.

MY TOKENS
Palette: &lt;fill in&gt;
Radius: &lt;fill in&gt;
Heading typeface: &lt;fill in&gt;
Body typeface: &lt;fill in&gt;

RULES
Never leave a hardcoded default color or radius in a component.
Never change only the accent color and call it a redesign.
Never break sign-in for existing accounts.

DONE WHEN
No screen still shows the default theme, the auth screens are visually
continuous with the rest of the app, and an existing account signs in on a
real device.
</code></pre>
<p>Give the agent a spec, not a vibe. "Build a sign-in screen" returns the default. "Split layout, brand image left, form right, our tokens, no centered card" returns your app.</p>
<h2>Which URL should the build point at?</h2>
<p>The published app or your custom domain, never the editor preview. Publishing is a distinct action from editing, so unpublished changes stay unpublished and the preview you work in is not the app your users get.</p>
<p>This is a particularly important Lovable-specific trap, because it can leave App Review seeing a different state from the one you intended to submit.</p>
<p>Every Lovable project gets a subdomain like <code>yourapp.lovable.app</code> on any plan, with custom domains on paid plans. Publishing is a distinct action from editing, and unpublished changes stay unpublished. So the preview you work in and the app your users get are not the same thing.</p>
<p>Apple does not require production specifically, but the features you are submitting have to be accessible to App Review under guidelines <strong>2.1(a)</strong> and <strong>2.3.1(a)</strong>, and an editor preview that diverged from your published app does not expose them. Separately, guideline <strong>2.5.2</strong> prohibits downloading, installing, or executing code that introduces or changes features or functionality after review. Content and data updating remotely is normal and expected.</p>
<p><strong>Before every submission:</strong></p>
<ol>
<li><p>Publish in Lovable so the live URL reflects your current build.</p>
</li>
<li><p>Connect your custom domain if you have one and confirm it serves the published app over HTTPS.</p>
</li>
<li><p>Set your app's start URL to that domain, or the published <code>.lovable.app</code> URL if you have not connected one. Never the editor preview URL.</p>
</li>
<li><p>Open that exact URL in a phone browser and walk the app before you build the binary.</p>
</li>
</ol>
<p>Lovable's reference for the publish flow and subdomain settings is at <a href="https://docs.lovable.dev/features/publish">docs.lovable.dev/features/publish</a>.</p>
<h2>Can your Lovable credits run out during review?</h2>
<p>Yes, and Lovable documents the failure itself. One credit balance now covers building, hosting and running the app on Cloud, and AI features in deployed apps. When it hits zero, Lovable's backend and AI services may stop serving the deployed app.</p>
<p>This is easy to miss, because everything works the day you submit. Cloud and AI gateway usage show up as Run credits under Settings, Plans and credit usage, Usage details, and they are consumed by your live app rather than by your prompting. Lovable is rolling the unified balance out gradually, so some workspaces still show the older separate Cloud and AI balances.</p>
<p>Guideline <strong>2.1(a)</strong> requires your backend services to be live and accessible during review. If the balance empties while your app sits in the queue, the services behind it may stop serving the deployed app, a reviewer opens it, nothing answers, and you are rejected for something that worked when you hit submit. Your data is safe while services are paused, but you cannot reach it either.</p>
<p>Two things to do before submitting. Check the balance, and enable auto top-up with a monthly spend limit you are comfortable with, which is available on Pro and Business plans. It is the cheapest rejection you will ever avoid.</p>
<h2>How do you make the app reviewable?</h2>
<p>Give review working demo credentials, keep the backend live, and describe every feature and its path in Notes for Review. Apple states outright that generic descriptions will be rejected.</p>
<p>The same section requires demo account details when the app has a login and final metadata with no placeholder content or dead URLs. Guideline <strong>2.3.1(a)</strong> goes further: features must be accessible to review, and every new feature must be described with specificity in Notes for Review, with Apple stating outright that generic descriptions will be rejected.</p>
<p>In App Store Connect:</p>
<ol>
<li><p>Open your app, then the version you are submitting.</p>
</li>
<li><p>Scroll to <strong>App Review Information</strong>.</p>
</li>
<li><p>Tick <strong>Sign-in required</strong> and enter demo credentials.</p>
</li>
<li><p>Verify those exact credentials on a real device the same day you submit.</p>
</li>
<li><p>In <strong>Notes</strong>, name each non-obvious feature and the path to it. Name the tab, the button, the screen.</p>
</li>
<li><p>Say how to reach anything behind a purchase, a role, or a region.</p>
</li>
<li><p>Confirm your support URL and privacy policy URL both load.</p>
</li>
</ol>
<p>Also check <strong>2.3.3</strong>: screenshots should show the app in use, not merely title art, a login page, or a splash screen.</p>
<h2>How do quality problems become a conduct issue?</h2>
<p>Through guideline 5.6.4, which names excessive customer reports, negative reviews, and refund requests as indications of failing to maintain quality, and says that inability to maintain quality may factor into whether a developer abides by the code of conduct.</p>
<p>Guideline <strong>5.6.4</strong> states that indications of failing to maintain quality include excessive customer reports, negative reviews, and excessive refund requests, and that an inability to maintain high quality may be a factor in deciding whether a developer is abiding by the Developer Code of Conduct.</p>
<p>That is why a broken screen can matter here in addition to being a 2.1 completeness problem, since Apple rejects incomplete binaries, crashes, and obvious technical issues under App Completeness. A reviewer who suspects a template spends the rest of their time looking for confirmation. Walk the whole app on a real device before submitting: every screen, every button, every form to the last field. Anything that dead-ends converts a suspicion into a decision.</p>
<h2>Are your store claims accurate?</h2>
<p>Guideline 5.6.2 requires your representation of yourself, your business, and your offerings to be accurate, truthful, relevant, and up to date. Read your listing as a reviewer would and delete anything you could not evidence.</p>
<p>Guideline <strong>5.6.2</strong> requires your representation of yourself, your business, and your offerings to be accurate, truthful, relevant, and up to date. Generated marketing copy overstates by default, so read your name, subtitle, description, and in-app claims as a reviewer would. Any claim about who built the app, what qualifications sit behind it, what it is certified to do, or what results it produces has to be true and verifiable. Delete anything you could not evidence.</p>
<h2>Does guideline 4.2.6 ban building on Lovable?</h2>
<p>No. It requires the app to be submitted directly by the provider of its content, rather than mass-submitted by a platform from its own account. Meeting that ownership condition is necessary rather than sufficient, so the customization work still applies.</p>
<p>Guideline <strong>4.2.6</strong> says apps created from a commercialized template or app generation service will be rejected unless submitted directly by the provider of the app's content. In practice: submit under your own Apple Developer account, as a customized product rather than a rethemed instance. Building in Lovable does not put you on the wrong side of this. Submitting through someone else's shared account, or shipping something indistinguishable from the next generated app, does. Apple also says those services should offer tools that let their clients create customized, innovative apps that provide unique customer experiences, which is the same standard as everything above, stated in Apple's own words.</p>
<h2>What do you do if you already have the letter?</h2>
<p>Do not resubmit the same binary with a bumped version. Make the changes, reply in Resolution Center before submitting the new build, and list what changed screen by screen with screenshots attached.</p>
<p>Apple states that review takes longer when an app is repeatedly rejected for the same guideline violation.</p>
<pre><code class="language-plaintext">We have addressed the issues as follows.

Visual identity: replaced the default component theme with our own palette,
radius, typography and spacing across the app.

Sign-in: redesigned with our own layout and copy, now continuous with
onboarding.

App icon and store screenshots: replaced. Screenshots now show the app in use.

Metadata: reviewed the listing and removed claims we cannot evidence.

Review access: demo account added under App Review Information and verified
today. Notes for Review describe each feature and where to find it.

The submitted build points at our published production domain and the backend
is provisioned for the review period. No functionality is gated, remote-flagged
or withheld from review.
</code></pre>
<p>If you still believe the rejection is wrong after that, the <a href="https://developer.apple.com/contact/app-store/?topic=appeal">appeal path</a> exists, but it is slower than fixing and resubmitting, and evidence of change is what actually moves a 5.6 letter.</p>
<h2>Is Lovable why your app was rejected?</h2>
<p>No. Similarity, quality, accurate representation, and verifiability are what Apple's review rules evaluate, and none of them is a build tool.</p>
<p>Lovable is not why your app was rejected. Apple's stated inputs are app similarity, quality, accurate representation, and whether the app can be verified. None of those is a build tool.</p>
<p>What gets flagged is an app that arrives on the default theme, pointed at a preview URL, with a backend that may not be answering by the time a reviewer opens it. Fix the tokens, publish properly, top up the credits, and fill in the review notes.</p>
<h2>Get it on the stores</h2>
<p>Once the app itself is review-ready, the remaining problem is getting the build signed and onto the stores.</p>
<p>Take the Lovable app you already built and ship it to iOS and Android as a real native binary, under your own developer account, with 50+ native device features available from your existing codebase and content updates over the air. Code signing and submission run from the browser, with no Mac and no Xcode required.</p>
<p><a href="https://setup.despia.com">See the setup docs at setup.despia.com</a></p>
]]></content:encoded></item><item><title><![CDATA[WebView App Rejected Under Guideline 5.6: The Fix]]></title><description><![CDATA[Apple has no rule against WebViews. Three things are worth checking first after a guideline 5.6 rejection on a web-built app: an interface that looks like the tool that generated it, an app that adds ]]></description><link>https://blog.despia.com/webview-app-rejected-under-guideline-5-6-the-fix</link><guid isPermaLink="true">https://blog.despia.com/webview-app-rejected-under-guideline-5-6-the-fix</guid><category><![CDATA[webview]]></category><category><![CDATA[appstore]]></category><category><![CDATA[AppReview]]></category><category><![CDATA[iOS]]></category><category><![CDATA[ios app development]]></category><category><![CDATA[ios app developer]]></category><dc:creator><![CDATA[Despia CEO]]></dc:creator><pubDate>Wed, 19 Aug 2026 09:02:13 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/67f0239ff4f5a0b7b4a852c4/ddd9879c-cdc7-4f5f-8c7f-e549124474b9.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Apple has no rule against WebViews. Three things are worth checking first after a guideline 5.6 rejection on a web-built app: an interface that looks like the tool that generated it, an app that adds nothing over your website, and a build a reviewer could not verify.</p>
<h2>Does Apple reject apps for using a WebView?</h2>
<p>No. There is no guideline against WebViews, and guideline <strong>2.5.6</strong> requires apps that browse the web to use the appropriate WebKit framework and WebKit JavaScript, with an entitlement path for alternative browser engines in the EU and Japan. On iOS, WKWebView is the sanctioned way to render web content.</p>
<p>The advice circulating online is that Apple rejects WebView apps, so you have to rebuild in Swift. That is wrong in a way that costs people months. WKWebView is also the foundation used by mainstream hybrid wrappers such as Capacitor and Cordova.</p>
<p>What does get flagged is different. Guideline <strong>5.6</strong> is the Developer Code of Conduct, and a rejection under it arrives with language about a pattern of unusual behavior associated with fraudulent activity and features that appear to have been hidden during review. Unlike a purely technical rejection, it carries account-level implications, because Apple states that a Developer Program account will be terminated for actions not in accordance with the code, and that restoring it means a written statement of improvements Apple has to approve.</p>
<p>For the full breakdown of what guideline 5.6 means at the account level, see our <a href="https://blog.despia.com/guideline-5-6-rejection-why-ai-built-apps-get-flagged">guideline 5.6 rejection guide</a>.</p>
<p>References below are checked against the <a href="https://developer.apple.com/app-store/review/guidelines/">App Review Guidelines</a> as updated on June 8, 2026.</p>
<h2>What is Apple actually measuring?</h2>
<p>Similarity, utility, quality, accurate representation, and whether the app can be verified. Apple publishes that its systems analyze app similarity, but not how signals are weighted. All of those are things its review rules explicitly evaluate. None is a rendering engine.</p>
<p>Apple's App Review team evaluated more than 9.1 million submissions in 2025. Over 371,000 were rejected for copying other apps, spam, or misleading users, against roughly 22,000 for hidden or undocumented features. Those are global category totals, so they cannot tell you why any single app was flagged, but they do show where enforcement volume sits. Source: <a href="https://www.apple.com/newsroom/2026/05/the-app-store-stopped-over-2-point-2-billion-usd-in-fraudulent-transactions-in-2025/">Apple's 2025 App Store fraud analysis</a>.</p>
<p>Guideline <strong>4.3(b)</strong> puts the similarity point plainly: do not submit apps that are indistinguishable from what is already widely available.</p>
<h2>Why does an AI-built interface look suspicious?</h2>
<p>Because a thousand projects ship the same one. Leaving an AI builder's default theme in place produces the same palette, radius, font stack, and card rhythm across unrelated apps. Apple has not disclosed screen weighting, so treat this as the first similarity signal worth eliminating.</p>
<p>The component libraries are not the problem. shadcn/ui is well built and accessible and is explicitly designed to be customized: the components land in your repo as source you own, and the visual identity lives in CSS variables you are meant to set. What produces the generic result is an agent scaffolding it and stopping there.</p>
<p>Being built with AI is not itself an App Review violation. Apple regulates what the resulting app does and how it is submitted. Where AI-built apps run into trouble is low-effort similarity, and an untouched default theme is what that looks like from outside.</p>
<p>The auth screen is where it shows most, because it is the first thing a reviewer sees on an app they cannot otherwise verify. Centered card, rounded icon tile, "Welcome back", Continue with Google, an OR divider, iconed email and password fields, "Don't have an account? Create one". Every project from that builder has it.</p>
<ol>
<li><p><strong>Set your tokens first.</strong> Palette, radius scale, typeface, spacing rhythm, border weight. Every component generated afterwards inherits your identity instead of the default one.</p>
</li>
<li><p><strong>Change the auth composition.</strong> Move off the centered card. Split layout, top-anchored form under a full-bleed header, or a two-step email-then-password flow.</p>
</li>
<li><p><strong>Set a typeface.</strong> Highest-leverage single change available. The default system stack is the most recognizable tell there is.</p>
</li>
<li><p><strong>Rewrite every string</strong> on the auth screens in your product's voice.</p>
</li>
<li><p><strong>Replace the generated icon.</strong> Ship a flat 1024x1024 PNG with no alpha channel, made deliberately rather than prompted and shipped.</p>
</li>
</ol>
<p>Changing the accent color is not a redesign. The layout is the fingerprint.</p>
<h2>What does your app need to add beyond your website?</h2>
<p>Enough that installing it is worth doing. Guideline <strong>4.2</strong> asks for features, content, and UI that elevate the app beyond a repackaged website, and <strong>4.2.2</strong> says apps should not primarily be marketing materials, web clippings, content aggregators, or a collection of links. The test is utility, not architecture.</p>
<p><strong>The question to answer: what does the installed app experience add over opening your site in Safari?</strong> If you cannot answer it, the guideline is talking to you.</p>
<p>This is not a question about what browsers can technically do. Modern Safari and Home Screen web apps support a great deal, including web push, badging, and sharing, so framing native capability as things a browser cannot do is both wrong and beside the point. What 4.2 asks for is app-level integration that improves your specific product.</p>
<p>Examples of app-level integration:</p>
<ul>
<li><p>StoreKit purchases and subscriptions, and Play Billing on Android</p>
</li>
<li><p>Face ID and Touch ID through the platform's own biometric flow</p>
</li>
<li><p>Native notification handling, including how alerts behave when the app is backgrounded</p>
</li>
<li><p>Native camera and photo picker flows</p>
</li>
<li><p>Platform share integrations, both sharing out and receiving shares</p>
</li>
<li><p>Native haptics</p>
</li>
<li><p>Contacts, calendar, and precise location access</p>
</li>
<li><p>App-specific deep link and universal link behavior</p>
</li>
<li><p>Background capabilities</p>
</li>
<li><p>Offline behavior that survives a cold start</p>
</li>
</ul>
<p>You do not need all of them. You need the ones your product would genuinely use, implemented and reachable in the build you submitted. More importantly, these integrations are how you demonstrate the additional utility and app-like experience that 4.2 asks for.</p>
<p>A submission that opens a URL and stops fails that test. Rewriting the same product in Swift would not, by itself, cure it, because the guideline is about what the app does rather than what it is written in.</p>
<h2>Why could the reviewer not verify your app?</h2>
<p>Two causes, and both land a legitimate app in the hidden-features bucket by accident. Either the build points at an environment that does not expose everything you are asking Apple to approve, or the reviewer could not sign in and could not find features you never described.</p>
<p><strong>The build points at an incomplete environment.</strong> Apple does not require production specifically, but the features you are submitting have to be accessible to App Review under guidelines <strong>2.1(a)</strong> and <strong>2.3.1(a)</strong>, and a staging URL or a preview deployment usually lags the live app. Separately, guideline <strong>2.5.2</strong> prohibits downloading, installing, or executing code that introduces or changes features or functionality after review, which is why over-the-air content updates are fine and post-approval functionality changes are not. Check your start URL before every submission.</p>
<p><strong>Review could not get in or could not find things.</strong> Guideline <strong>2.1(a)</strong> requires demo account details when the app has a login, final metadata with no placeholder content, and your backend live during review. Guideline <strong>2.3.1(a)</strong> goes further: features must be accessible to review, and every new feature must be described with specificity in Notes for Review, with Apple stating outright that generic descriptions will be rejected.</p>
<p>In App Store Connect:</p>
<ol>
<li><p>Open your app, then the version you are submitting.</p>
</li>
<li><p>Scroll to <strong>App Review Information</strong>.</p>
</li>
<li><p>Tick <strong>Sign-in required</strong> and enter demo credentials.</p>
</li>
<li><p>Verify those exact credentials on a real device the same day you submit.</p>
</li>
<li><p>In <strong>Notes</strong>, name each native capability and where to find it. "Face ID unlock: Profile tab, Security. Push notifications: prompted on first launch after onboarding." That field is where you tell a reviewer which parts of your app are native, which is worth doing when the interface runs in a WebView.</p>
</li>
<li><p>Say how to reach anything behind a purchase, a role, or a region.</p>
</li>
<li><p>Confirm your support URL and privacy policy URL both load.</p>
</li>
</ol>
<p>Also: <strong>2.3.3</strong> states screenshots should show the app in use, not merely title art, a login page, or a splash screen.</p>
<h2>How do quality problems become a conduct issue?</h2>
<p>Through guideline <strong>5.6.4</strong>, which names excessive customer reports, negative reviews, and excessive refund requests as indications of failing to maintain quality, and states that an inability to maintain high quality may factor into whether a developer is abiding by the Developer Code of Conduct.</p>
<p>That is the mechanism people miss. Quality problems can therefore become part of a conduct judgment rather than remaining only a technical issue. A reviewer who suspects a template spends the rest of their time looking for confirmation, and a screen that does not work hands it to them. Before submitting, walk the app on a real device: every screen, every button, every form completed to the last field. Anything that dead-ends converts a suspicion into a decision.</p>
<h2>Are your store claims accurate?</h2>
<p>Guideline <strong>5.6.2</strong> requires your representation of yourself, your business, and your offerings to be accurate, truthful, relevant, and up to date. Read your name, subtitle, description, and in-app claims as a reviewer would, then delete anything you could not evidence.</p>
<p>Any claim about who built the app, what qualifications sit behind it, what it is certified to do, or what results it produces has to be true and verifiable. Generated marketing copy overstates by default, which is why this catches more AI-built apps than people expect.</p>
<h2>Does guideline 4.2.6 ban building on a platform?</h2>
<p>No. It says apps from a commercialized template or app generation service are rejected unless submitted directly by the provider of the app's content. Submitting it yourself, rather than having the platform mass-submit client apps from its own account, addresses that ownership requirement.</p>
<p>The rule exists to stop agencies mass-submitting near-identical apps from one account. Meeting the ownership condition is necessary rather than sufficient, so the customization work above still applies. Apple says as much itself: those services should offer tools that let their clients create customized, innovative apps that provide unique customer experiences, which is the same standard as everything above, stated in Apple's own words.</p>
<h2>When is the rejection correct?</h2>
<p>When the app genuinely is thin. If it is a link to your marketing site, a catalog with no interaction, an aggregator of other people's content, or a shell around a page you did not build, guidelines 4.2 and 4.2.2 apply and they apply correctly.</p>
<p>Rewriting it in Swift produces the same rejection, because the guideline is about what the app does. The same goes for an app that only works if another app is installed, which is 4.2.3(i), or one functionally indistinguishable from apps already on the store, which is 4.3(b).</p>
<h2>What do you do if the letter already arrived?</h2>
<p>Do not resubmit the same binary with a bumped version, because Apple states that review takes longer when an app is repeatedly rejected for the same guideline violation. Make the changes, reply in Resolution Center before submitting the new build, and list what changed with screenshots attached.</p>
<pre><code class="language-plaintext">We have addressed the issues as follows.

Visual identity: replaced the default component theme with our own palette,
radius, typography and spacing across the app.

Sign-in: redesigned with our own layout and copy.

App-level integration: Face ID unlock, notification handling, native camera
access and the share sheet are implemented and reachable in this build. Paths
are listed in Notes for Review.

App icon and store screenshots: replaced. Screenshots now show the app in use.

Metadata: reviewed the listing and removed claims we cannot evidence.

Review access: demo account added under App Review Information and verified
today.

The submitted build points at the same environment our users get. No
functionality is gated, remote-flagged or withheld from review.
</code></pre>
<p>No argument and no emotion. Evidence of change is what moves a 5.6 letter. The <a href="https://developer.apple.com/contact/app-store/?topic=appeal">appeal path</a> exists, but it is slower than fixing and resubmitting.</p>
<h2>The summary</h2>
<p>There is no guideline against WebViews, and Apple's own rules point web-browsing apps at WebKit. What gets flagged is an app that looks like every other submission that week, adds nothing over visiting your website, or cannot be verified by the person reviewing it.</p>
<p>Make the interface yours, add the platform integrations your product would actually use, point the build at an environment that shows everything you are asking Apple to approve, and tell the reviewer where it all is. That is what separates an approved app from a rejected one, in any stack.</p>
<h2>Get it on the stores</h2>
<p>Once the app itself is review-ready, the remaining problem is getting the build signed and onto the stores.</p>
<p>Take the web app you already built and ship it to iOS and Android as a real native binary, under your own developer account, with 50+ native device features available from your existing codebase and content updates over the air. Code signing and submission run from the browser, with no Mac and no Xcode required.</p>
<p><a href="https://setup.despia.com">See the setup docs at setup.despia.com</a></p>
]]></content:encoded></item><item><title><![CDATA[Guideline 5.6 Rejection: Why AI-Built Apps Get Flagged]]></title><description><![CDATA[A guideline 5.6 letter accuses you of hiding features and behaving like a fraud. If a legitimate app receives one despite hiding nothing, similarity and reviewability are two of the first things worth]]></description><link>https://blog.despia.com/guideline-5-6-rejection-why-ai-built-apps-get-flagged</link><guid isPermaLink="true">https://blog.despia.com/guideline-5-6-rejection-why-ai-built-apps-get-flagged</guid><category><![CDATA[appstore]]></category><category><![CDATA[AppReview]]></category><category><![CDATA[iOS]]></category><category><![CDATA[ios app development]]></category><category><![CDATA[ios app developer]]></category><dc:creator><![CDATA[Despia CEO]]></dc:creator><pubDate>Wed, 19 Aug 2026 08:40:36 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/67f0239ff4f5a0b7b4a852c4/2c696231-a0d1-46d3-9edd-23f6ab748609.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A guideline 5.6 letter accuses you of hiding features and behaving like a fraud. If a legitimate app receives one despite hiding nothing, similarity and reviewability are two of the first things worth investigating. Here is what the Developer Code of Conduct says, and the steps that clear it.</p>
<p>The letter reads like an accusation. Apple identified a pattern of unusual behavior commonly associated with fraudulent activity, and your app appears to contain features that were intentionally hidden during review. You did not hide anything. There is no kill switch, no screen that unlocks after approval, no remote flag waiting to flip.</p>
<p>Apple does not publish how it decides, or how it weights any individual signal. What it does publish is that its systems use AI to analyze app similarity, identify malicious patterns, and flag potentially problematic changes in updates. For a legitimate developer holding this letter, similarity and reviewability are the two concrete things worth investigating, and neither of them is an engineering problem. Everything below is checked against the <a href="https://developer.apple.com/app-store/review/guidelines/">App Review Guidelines</a> as last updated on June 8, 2026.</p>
<h2>What does guideline 5.6 actually say?</h2>
<p>It is the Developer Code of Conduct. It covers respectful communication, customer trust, ratings integrity, accurate developer identity, and app quality, and it carries account-level consequences rather than applying only to one submission.</p>
<p>Reading the section changes how you respond. It is about conduct rather than features. It asks developers to treat people with respect in App Store review responses, customer support requests, and communication with Apple, and it states that repeated manipulative or misleading behavior or other fraudulent conduct leads to removal from the Apple Developer Program.</p>
<p>It is about customer trust. Apps should not prey on users, rip off customers, trick them into unwanted purchases, force them to share unnecessary data, raise prices in a tricky manner, charge for things that are not delivered, or use other manipulative practices.</p>
<p>And it escalates to the account. Apple states that a Developer Program account will be terminated for actions not in accordance with the code of conduct, and that restoring it means submitting a written statement detailing the improvements you plan to make, which Apple has to approve and confirm before the account comes back.</p>
<p>That is why the letter feels so heavy. A 2.1 rejection wants a demo account. A 4.3 rejection usually starts with the submission, though Apple notes that repeated low-effort submissions under it can also lead to removal from the Developer Program. A 5.6 letter starts at the level 4.3 can escalate to, because it is the code of conduct itself.</p>
<p>Four subsections sit under it:</p>
<ul>
<li><p><strong>5.6.1 Reviews and Ratings</strong> covers how you collect ratings, and requires the API Apple provides rather than gathering them by other means.</p>
</li>
<li><p><strong>5.6.2 Developer Identity</strong> requires that your representation of yourself, your business, and your offerings on the App Store is accurate, truthful, relevant, and up to date, so customers understand who they are engaging with.</p>
</li>
<li><p><strong>5.6.3 Discovery Fraud</strong> states that manipulating any element of the App Store customer experience, such as charts, search, reviews, or referrals to your app, is not permitted.</p>
</li>
<li><p><strong>5.6.4 App Quality</strong> states that indications of failing to maintain quality include excessive customer reports, negative reviews, and excessive refund requests, and that an inability to maintain high quality may be a factor in deciding whether a developer is abiding by the code of conduct.</p>
</li>
</ul>
<p>Note what 5.6.4 does. Quality problems can therefore become part of a conduct judgment rather than remaining only a technical issue.</p>
<h2>Why does the letter mention hidden features?</h2>
<p>Because it is a template covering a broad category. Apple's published 2025 numbers show where enforcement volume actually sits, and it is not in apps concealing functionality.</p>
<p>Apple published its App Store fraud analysis for 2025 in May 2026. App Review evaluated more than 9.1 million submissions and rejected over 2 million, including more than 1.2 million new apps and nearly 800,000 updates. By category:</p>
<table>
<thead>
<tr>
<th>Reason for rejection</th>
<th>Submissions in 2025</th>
</tr>
</thead>
<tbody><tr>
<td>Privacy violations</td>
<td>443,000+</td>
</tr>
<tr>
<td>Copying other apps, spam, or misleading users</td>
<td>371,000+</td>
</tr>
<tr>
<td>Hidden or undocumented features</td>
<td>22,000+</td>
</tr>
</tbody></table>
<p>Source: <a href="https://www.apple.com/newsroom/2026/05/the-app-store-stopped-over-2-point-2-billion-usd-in-fraudulent-transactions-in-2025/">Apple, The App Store stopped over $2.2 billion in potentially fraudulent transactions in 2025</a>.</p>
<p>The copycat and spam bucket is roughly 17 times the hidden-features bucket. Those are global category totals, so they do not tell you why any single app was flagged. What they do tell you is where the enforcement volume sits, and it is not in apps concealing functionality.</p>
<p>Apple is explicit about the mechanism in that same report. Its systems use AI to identify malicious patterns, <strong>analyze app similarity</strong>, and flag problematic changes in updates, so human reviewers can focus their attention. It is equally explicit about the cause: powerful AI development tools are driving a surge in app submissions.</p>
<p>App similarity is not a side detail. It is a stated input to review, and it can turn "my app came from the same starter as thousands of others" into a flag. Guideline <strong>4.3(b)</strong> says the same thing in plain language: do not submit apps that are indistinguishable from what is already widely available.</p>
<h2>Is building with AI against App Review rules?</h2>
<p>No. Apple regulates what the resulting app does and how it is submitted, and guideline 4.2.6 explicitly contemplates app-generation services. Where AI-built apps run into trouble is low-effort similarity, and an untouched default theme is what that looks like from the outside.</p>
<p>The wrong lesson gets drawn from these rejections constantly, so it is worth saying directly. Being built with AI is not itself an App Review violation. Apple regulates what the resulting app does and how it is submitted, and 4.2.6 explicitly contemplates app-generation services. Where AI-built apps run into trouble is low-effort similarity, and an untouched default theme is what that looks like from the outside.</p>
<p>The component libraries are not the problem either. shadcn/ui is the good example. It looks excellent, it is accessible, and it is deliberately built to be customized: the components land in your repo as source you own, and the visual identity lives in CSS variables you are meant to set. What produces the generic result is an agent scaffolding it and stopping there, so a thousand projects ship the same neutral palette, the same default radius, the same system font stack, the same card-and-border rhythm. The library gave you a solid vanilla base. Leaving it vanilla is the choice that gets noticed.</p>
<p>Same with generated assets. A model produces an icon in 5 seconds and it looks like every other icon produced that way. Taking that into a real design tool and making decisions about mark, weight, contrast, and crop is 20 minutes that removes an entire category of similarity signal.</p>
<p><strong>Set your tokens before you build more screens.</strong> Palette, radius scale, typeface, spacing rhythm, border weight, shadow treatment. Do it first and every component generated afterwards inherits your identity instead of the default one. Changing the accent color from purple to teal is not a redesign.</p>
<h2>Where does a generated app get caught first?</h2>
<p>Usually the sign-in screen. It is the screen most likely to be identical across unrelated submissions, and it is the one a reviewer sees before anything else. Apple has not disclosed screen weighting, so treat this as the first place to look rather than a ranking.</p>
<p>Apple has not said which screens weigh most, so treat this as the first place to look rather than a ranking. It is the screen most likely to be identical across unrelated submissions, and it is the one a reviewer sees first.</p>
<p>Most AI builders ship the same authentication screen. Centered card, rounded icon tile at the top, a bold "Welcome back", a grey subtitle, a Continue with Google button, an OR divider, email and password fields with small leading icons, a full-width primary button, and "Don't have an account? Create one" underneath. It is a decent screen. That is the problem. Every project scaffolded from that builder gets it, and it is the first thing a reviewer sees, in front of an app they cannot otherwise verify.</p>
<p><strong>Steps:</strong></p>
<ol>
<li><p><strong>Change the composition.</strong> Move off the centered card entirely. A split layout with brand imagery on one side, a top-anchored form under a full-bleed header, or a two-step email-then-password flow all read as deliberate.</p>
</li>
<li><p><strong>Set a typeface.</strong> Highest-leverage single change on the list. The default system stack is the most recognizable tell in a generated app.</p>
</li>
<li><p><strong>Rewrite every string.</strong> "Welcome back" and "Log in to your account" appear on an enormous number of screens.</p>
</li>
<li><p><strong>Restyle the social button.</strong> Follow Google's branding rules for the mark, but height, radius, border treatment, and placement are yours.</p>
</li>
<li><p><strong>Make it continuous with your onboarding.</strong> Most AI-built apps have a strong branded first screen and then drop into an unbranded login. That seam reads as a shell wrapped around a template.</p>
</li>
<li><p><strong>Compare it against three other apps built with the same tool.</strong> If you cannot tell them apart, neither can a similarity check.</p>
</li>
</ol>
<p>A prompt that produces a redesign rather than a recolor:</p>
<pre><code class="language-plaintext">CONTEXT
This app was scaffolded from a template and the auth screens are still the
default. Apple rejected the submission under guideline 5.6 for resembling
other template submissions.

TASK
1. Redesign sign-in, sign-up and forgot-password using a composition that is
   NOT a centered card with a stacked field group.
2. Apply the typeface, spacing scale, radius and background treatment from our
   onboarding screen so the app reads as one product.
3. Rewrite every string on these screens in our product's voice.
4. Keep all existing auth logic, routes and error handling unchanged.

RULES
Never change only the accent color and call it a redesign.
Never leave a default template string on these screens.
Never break sign-in for existing accounts.

DONE WHEN
The auth screens share the onboarding screen's visual identity, no original
template string remains, and an existing account signs in on a real device.
</code></pre>
<h2>What else in the submission looks generated?</h2>
<p>The icon, the screenshots, leftover placeholder text, and the store description. None is a violation alone. Together they form the profile.</p>
<p>Review looks at the whole submission.</p>
<ol>
<li><p><strong>Replace the generated icon.</strong> A gradient square with a letterform is the most common AI-generated icon in existence. Ship a flat 1024x1024 PNG with no alpha channel.</p>
</li>
<li><p><strong>Fix the screenshots.</strong> Guideline <strong>2.3.3</strong> states screenshots should show the app in use, not merely title art, a login page, or a splash screen. If your first screenshot is the auth wall, you led with the generic part and you are out of compliance at the same time.</p>
</li>
<li><p><strong>Scrub placeholder content.</strong> Guideline <strong>2.1(a)</strong> requires final versions with no placeholder text and no dead URLs. Search for lorem, TODO, and example.com.</p>
</li>
<li><p><strong>Rewrite the store description.</strong> Not a violation on its own, but another similarity signal in a submission that already has several.</p>
</li>
</ol>
<h2>How do quality problems become a conduct issue?</h2>
<p>Through 5.6.4, which names excessive customer reports, negative reviews, and refund requests as indications of failing to maintain quality, and says that inability to maintain quality may factor into whether a developer abides by the code of conduct.</p>
<p>A broken screen is already a 2.1 completeness problem, since Apple rejects incomplete binaries, crashes, and obvious technical issues. 5.6.4 means it can also matter here. A reviewer who suspects a template will spend the rest of their time looking for confirmation, and an app that does not work gives it to them.</p>
<p>Before submitting, walk the app on a real device. Open every screen. Tap every button and confirm none of them dead-ends. Complete every form to the last field. Anything a reviewer cannot finish is a reason for them to stop, and it converts a suspicion into a decision.</p>
<p>The forward-looking half of 5.6.4 matters too. Excessive customer reports, negative reviews, and high refund rates are named as indications, which means quality problems that survive approval can come back as a code of conduct question later.</p>
<h2>Are your store claims accurate?</h2>
<p>Guideline 5.6.2 requires your representation of yourself, your business, and your offerings to be accurate, truthful, relevant, and up to date. Read your listing as a reviewer would and delete anything you could not evidence.</p>
<p>5.6.2 is short and it catches more AI-built apps than people expect, because generated marketing copy overstates by default.</p>
<p>Your app name, subtitle, description, screenshots, and in-app claims all have to be accurate and current. Any claim about who built the app, what qualifications sit behind it, what it is certified to do, or what results it produces has to be true and verifiable. Professional titles and credentials are part of this. So is the pricing you advertise, inside the App Store and outside it.</p>
<p>If the model wrote your listing, read it as a reviewer would and delete anything you could not evidence.</p>
<h2>What do 5.6.1 and 5.6.3 actually ban?</h2>
<p>Ratings and reviews collected outside Apple's API or arranged through incentives, and manipulation of any element of the App Store customer experience, including charts, search, reviews, and referrals.</p>
<p>Worth stating plainly, because growth advice on the internet routinely recommends these.</p>
<ul>
<li><p>Do not buy, incentivize, trade, or otherwise arrange ratings and reviews.</p>
</li>
<li><p>Do not gather ratings outside the API Apple provides for it.</p>
</li>
<li><p>Do not stuff keywords, use competitor names in your metadata, or manipulate search and charts.</p>
</li>
<li><p>Do not run install campaigns that misrepresent your price.</p>
</li>
</ul>
<p>These are the behaviors 5.6 is genuinely written about. If any of them are in your growth plan, remove them before you resubmit, because an account already carrying a 5.6 letter has no margin.</p>
<h2>Does guideline 4.2.6 ban building on a platform?</h2>
<p>No. It requires the app to be submitted directly by the provider of its content, rather than mass-submitted by the platform from its own account. It also tells those services to enable customized, innovative apps that provide unique customer experiences.</p>
<p>Guideline <strong>4.2.6</strong> is worth reading if you built on any app-generation platform, because it says more than people quote.</p>
<p>It states that apps created from a commercialized template or app generation service will be rejected unless they are submitted directly by the provider of the app's content, that those services should not submit apps on behalf of their clients, and that they should offer tools letting their clients create customized, innovative apps that provide unique customer experiences. It also allows template providers a single binary hosting all client content in an aggregated or picker model.</p>
<p>Two things follow. First, the app has to go up under the Apple Developer account of the person or company whose content and business it is. Building on a platform does not put you on the wrong side of 4.2.6. Submitting through a platform's shared account does.</p>
<p>Second, Apple has written down the standard in its own words: customized, innovative, unique customer experiences. That is the same instruction as the section above, coming from the rulebook rather than from us.</p>
<h2>Why could the reviewer not verify your app?</h2>
<p>Either the build points at an environment that does not expose everything you are asking Apple to approve, or the reviewer could not sign in and could not find features you never described in Notes for Review.</p>
<p>This is where a legitimate app accidentally lands in the hidden-features bucket.</p>
<p><strong>Point the submitted build at a complete, representative environment.</strong> Apple does not require you to submit against production specifically, but the features you are submitting have to be accessible to App Review, which is guideline <strong>2.1(a)</strong> and <strong>2.3.1(a)</strong> below. Separately, guideline <strong>2.5.2</strong> prohibits downloading, installing, or executing code that introduces or changes features or functionality after review. Content and data updating remotely is normal and expected. Materially different functionality switched on after approval is not.</p>
<p>For most people the simplest way to satisfy that is to point at production, because a staging URL or a preview deployment usually lags the live app and nobody notices until a reviewer does. Check the start URL before every submission.</p>
<p><strong>Give review a working way in.</strong> Guideline <strong>2.1(a)</strong> requires demo account details when the app has a login, and requires your backend live during review. Guideline <strong>2.3.1(a)</strong> goes further: features must be accessible for review, and every new feature and product change must be described with specificity in Notes for Review, with Apple stating outright that generic descriptions will be rejected.</p>
<p>In App Store Connect:</p>
<ol>
<li><p>Open your app, then the version you are submitting.</p>
</li>
<li><p>Scroll to <strong>App Review Information</strong>.</p>
</li>
<li><p>Tick <strong>Sign-in required</strong> and enter demo credentials.</p>
</li>
<li><p>Verify those exact credentials on a real device the same day you submit.</p>
</li>
<li><p>In <strong>Notes</strong>, name each non-obvious feature and the path to it. Name the tab, the button, the screen. Not "various improvements".</p>
</li>
<li><p>Say how to reach anything gated behind a purchase, a role, or a region.</p>
</li>
<li><p>Confirm your support URL and privacy policy URL both load.</p>
</li>
</ol>
<h2>What do you do if the letter already arrived?</h2>
<p>Do not resubmit the same binary with a version bump. Make the changes, reply in Resolution Center before submitting the new build, list what changed screen by screen, and attach screenshots.</p>
<p>Apple states that review takes longer when an app is repeatedly rejected for the same guideline violation or when a developer has attempted to manipulate the process. A second identical submission is exactly the behavior an account-level guideline is written about.</p>
<pre><code class="language-plaintext">We have addressed the issues as follows.

Visual identity: replaced the default component theme with our own palette,
radius, typography and spacing across the app.

Sign-in: redesigned with our own layout and copy, now continuous with
onboarding. Screenshots attached.

App icon and store screenshots: replaced. Screenshots now show the app in use
rather than the sign-in screen.

Metadata: reviewed the listing and removed claims we cannot evidence.

Review access: demo account added under App Review Information and verified
today. Notes for Review now describe each feature and where to find it.

The submitted build points at the same environment our users get. No
functionality is gated, remote-flagged or withheld from review.
</code></pre>
<p>No argument and no emotion. Evidence of change is what moves a 5.6 letter. If your account was actually terminated rather than the app rejected, the path back is the written improvement statement described in 5.6, and it should read like the note above with more detail. The <a href="https://developer.apple.com/contact/app-store/?topic=appeal">appeal path</a> exists, but it is slower than fixing and resubmitting.</p>
<h2>What should you check before submitting?</h2>
<p>Eleven things, and the first two matter most: that your app does not wear the default theme of the tool that built it, and that your sign-in screen is distinguishable from other apps built with the same tool.</p>
<ol>
<li><p>Your app does not use the default theme of the tool that built it.</p>
</li>
<li><p>Your sign-in screen is distinguishable from other apps built with the same tool.</p>
</li>
<li><p>The icon was made deliberately, not generated and shipped.</p>
</li>
<li><p>Screenshots show the app in use, not the login screen.</p>
</li>
<li><p>Every button does something and every form can be completed.</p>
</li>
<li><p>No placeholder text, no dead links.</p>
</li>
<li><p>Every claim in your listing is true and evidenced.</p>
</li>
<li><p>No incentivized reviews, no keyword manipulation, anywhere in your growth plan.</p>
</li>
<li><p>The build points at an environment that exposes everything you are asking Apple to approve.</p>
</li>
<li><p>Demo credentials verified today, backend live.</p>
</li>
<li><p>Notes for Review name each feature and the path to reach it.</p>
</li>
</ol>
<p>If you only have time for three, do 1, 2 and 11.</p>
<h2>Is this about how your app was built?</h2>
<p>No. A poorly differentiated app written in Swift gets flagged on the same signals. Similarity, quality, accurate representation, and verifiability are all things Apple's review rules explicitly evaluate. None of them is a runtime.</p>
<p>It is easy to read a 5.6 rejection as a verdict on your stack, and it is not.</p>
<p>If you are on a specific platform, the platform-specific versions of this guide go deeper: <a href="/blog/lovable-app-store-rejection">Lovable</a>, <a href="/blog/base44-app-store-rejection">Base44</a>, and <a href="/blog/webview-app-store-rejection">WebView apps generally</a>.</p>
<p>The takeaway for anyone building with AI tools is that the tool gets you a working app quickly, and the last 10 percent, where the app stops looking like its scaffolding, is work you have to ask for. If you ship generic output and it gets rejected, the AI was not the problem. The 20 minutes nobody spent on the theme was.</p>
<h2>Get it on the stores</h2>
<p>Once the app itself is review-ready, the remaining problem is getting the build signed and onto the stores.</p>
<p>Take the app you already built and ship it to iOS and Android as a real native binary, under your own developer account, with 50+ native device features available from your existing web codebase and content updates over the air. Code signing and submission run from the browser, with no Mac and no Xcode required.</p>
<p><a href="https://setup.despia.com">See the setup docs at setup.despia.com</a></p>
]]></content:encoded></item><item><title><![CDATA[Base44 Offline Mode: Service Workers and Local Data]]></title><description><![CDATA[https://youtu.be/ebflykFHCkg

Most people try this once, in the wrong order. They flip Offline Support to PWA in Despia, rebuild, put the phone in airplane mode, and the app opens to the native networ]]></description><link>https://blog.despia.com/base44-offline-mode-service-workers-and-local-data</link><guid isPermaLink="true">https://blog.despia.com/base44-offline-mode-service-workers-and-local-data</guid><category><![CDATA[base44]]></category><category><![CDATA[webapp]]></category><category><![CDATA[PWA]]></category><category><![CDATA[indexeddb]]></category><category><![CDATA[WebAPI]]></category><dc:creator><![CDATA[Despia CEO]]></dc:creator><pubDate>Sun, 09 Aug 2026 09:17:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/67f0239ff4f5a0b7b4a852c4/158d5975-247e-4038-96d3-76a35ddf22d0.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a class="embed-card" href="https://youtu.be/ebflykFHCkg">https://youtu.be/ebflykFHCkg</a></p>

<p>Most people try this once, in the wrong order. They flip Offline Support to PWA in Despia, rebuild, put the phone in airplane mode, and the app opens to the native network error page. Or it opens, shows the shell, and then spins forever on an empty screen. Or worse: it works offline, and now none of their updates reach users ever again.</p>
<p>Two things are going on. Caching the app and running the app offline are separate problems, and a service worker that solves the first one badly will quietly break your deployments. A worker keeps your HTML, JavaScript and CSS available without a network. It does nothing for the entity reads, backend functions and third party APIs your app calls the moment it boots. And if it answers every request from its stored copy, your users are pinned to whatever version they installed first.</p>
<p>The video above is the whole build: prompting Base44, catching the first attempt failing on a real phone, correcting it, and testing the same app as a PWA and as a native build. The code is open source at <a href="https://github.com/despia-native/base44-service-worker-sample">despia-native/base44-service-worker-sample</a>, a small task app with email login, a hand-written worker, an IndexedDB mirror and an outbox. You can hand that repository URL to Base44 as a reference. This post is the reasoning behind it.</p>
<h2>Where the service worker goes</h2>
<p>A service worker only controls the routes inside its own scope, so it has to be served from the root of your domain to control the whole app. Base44 serves files from a <code>public</code> folder at the root of your published app, so the worker goes at <code>public/sw.js</code> and is reachable at <code>https://your-app-domain.com/sw.js</code>. Put <code>offline.html</code> and your web app manifest in the same folder.</p>
<p>Confirm that before you write a line of caching logic. Open <code>https://your-app-domain.com/sw.js</code> directly in a browser. You want your JavaScript, served as JavaScript. If you get your app's HTML instead, the path is falling through to the single page app rewrite, the file is not at the root, and every registration attempt fails with a content type error that reads like a code problem and is not one.</p>
<p>Registering from a blob URL is not an alternative. <code>register()</code> requires an http or https script URL, so a blob is rejected outright.</p>
<h2>What the Despia offline toggle actually does</h2>
<p>In the Despia Editor, go to Settings &gt; Configuration and scroll to Dynamic App Source. That is where your Start URL points at your published Base44 app, and directly underneath it is Offline Support, with three options: none, PWA, and native. Select PWA. Then bump the version code and publish a new build for iOS, Android, or both.</p>
<p>That toggle provisions the Xcode and Android Studio projects to allow service workers in the native binary, without you touching either. Done manually, this is registering app bound domains on iOS and the equivalent on Android, in native project settings, which is not a path a non-native developer wants to walk. Here it is one setting and a rebuild.</p>
<p>What it does not do is write a service worker. If your Base44 app does not ship one, or ships one that never caches anything, the toggle changes nothing and every offline launch fails exactly as before. Despia removes the native obstacle. The caching strategy is yours, in your web codebase, using standard web APIs.</p>
<p>The other thing worth saying out loud, because it costs people an afternoon: offline support is a native runtime setting, so it only exists in binaries produced after the toggle was flipped. If you enable it and keep testing the build already on your phone, service worker registration is silently ignored, caches stay empty, and the Editor still reads as on. Install the new build before you conclude anything.</p>
<h2>A worker that stays fresh online and only falls back offline</h2>
<p>This is the part that decides whether your app can still be updated. Say it as a rule and it becomes obvious:</p>
<p>A bad worker asks "is there a cached copy" and serves it. A good worker asks "am I online", takes the network when the answer is yes, and only reaches for the cache when the network genuinely fails.</p>
<p>Cache-first on navigation is the default in most tutorials and in most AI-generated workers, and it is the single worst choice for an app that updates over the air. You publish in Base44, the content updates on your host, and nobody sees it. Here is the shape that works:</p>
<pre><code class="language-javascript">const VERSION = 'v1'
const CACHE = `app-${VERSION}`
const PRECACHE = ['/', '/offline.html']
const NETWORK_TIMEOUT_MS = 3000

// content-hashed build output, safe to cache forever because the name changes
function isHashedAsset(url) {
    return /^\/assets\//.test(url.pathname)
        &amp;&amp; /\.[0-9a-zA-Z_-]{8,}\.(js|css|woff2?|png|jpg|jpeg|svg|webp|avif|ico)$/.test(url.pathname)
}

self.addEventListener('install', event =&gt; {
    event.waitUntil((async () =&gt; {
        const cache = await caches.open(CACHE)
        await Promise.allSettled(PRECACHE.map(async path =&gt; {
            const res = await fetch(path, { cache: 'no-store' })
            if (res.ok) await cache.put(path, res)
        }))
        await self.skipWaiting()
    })())
})

self.addEventListener('activate', event =&gt; {
    event.waitUntil((async () =&gt; {
        if (self.registration.navigationPreload) {
            await self.registration.navigationPreload.enable().catch(() =&gt; {})
        }
        const names = await caches.keys()
        await Promise.allSettled(
            names.filter(n =&gt; n.startsWith('app-') &amp;&amp; n !== CACHE).map(n =&gt; caches.delete(n))
        )
        await self.clients.claim()
    })())
})

self.addEventListener('fetch', event =&gt; {
    const { request } = event
    if (request.method !== 'GET') return

    const url = new URL(request.url)
    if (url.origin !== self.location.origin) return

    if (isHashedAsset(url)) {
        event.respondWith(cacheFirst(request))
        return
    }

    const isDocument = request.mode === 'navigate'
    event.respondWith((async () =&gt; {
        try {
            return await fromNetwork(request, {
                preload: isDocument ? event.preloadResponse : undefined,
                noStore: isDocument
            })
        } catch {
            return offlineFallback(request)
        }
    })())
})

async function fromNetwork(request, { preload, noStore } = {}) {
    const cache = await caches.open(CACHE)
    const attempt = preload
        ? Promise.resolve(preload).then(res =&gt; res || fetch(request))
        : fetch(noStore ? new Request(request, { cache: 'no-store' }) : request)

    const timeout = new Promise((_, reject) =&gt;
        setTimeout(() =&gt; reject(new Error('network-timeout')), NETWORK_TIMEOUT_MS)
    )

    const res = await Promise.race([attempt, timeout])
    if (res &amp;&amp; res.ok &amp;&amp; res.type !== 'opaque') {
        cache.put(request, res.clone()).catch(() =&gt; {})
    }
    return res
}

async function offlineFallback(request) {
    const cache = await caches.open(CACHE)
    return (await cache.match(request))
        || (request.mode === 'navigate'
            ? (await cache.match('/')) || (await cache.match('/offline.html'))
            : undefined)
        || Response.error()
}
</code></pre>
<p>Four details in there are not optional.</p>
<p>The timeout matters more than it looks. A hanging connection does not reject <code>fetch()</code>, it sits there, so a plain <code>fetch().catch()</code> fallback stalls on a bad train connection instead of falling back. Race it.</p>
<p><code>cache: 'no-store'</code> on document fetches bypasses the browser's own HTTP cache and any CDN copy. Without it, the worker can go to the network, do everything right, and still be handed a stale HTML response, which looks identical to a broken worker and sends you debugging the wrong file.</p>
<p>Navigation preload starts the document request in parallel with worker startup, which removes the roughly one second delay you will otherwise see on the document in the network tab.</p>
<p>And the offline fallback is a chain, not a single file: the last cached copy of that exact page, then the app shell, then <code>/offline.html</code>. Make <code>/offline.html</code> render something useful, because it is the last thing standing between a dropped request and a blank screen.</p>
<p>There is no stack question to answer here. Every Base44 app is React and Vite, built to a static frontend with an <code>index.html</code> rewrite for routes, which makes two things true. Hashed build output lands under <code>/assets/</code>, so the test above is the right one. And every route resolves to the same document, so precaching the shell precaches every screen in the app rather than one page at a time.</p>
<p>Registration matters as much as the worker. <code>updateViaCache: 'none'</code> stops the browser reusing a cached copy of <code>sw.js</code> itself, which is how a broken worker becomes permanent. Then check for updates on load, on focus, on reconnect, and reload when control changes hands:</p>
<pre><code class="language-javascript">const registration = await navigator.serviceWorker.register('/sw.js', {
    scope: '/',
    updateViaCache: 'none'
})

let reloading = false
navigator.serviceWorker.addEventListener('controllerchange', () =&gt; {
    if (reloading) return
    reloading = true
    window.location.reload()
})

const checkForUpdate = () =&gt; void registration.update().catch(() =&gt; {})
window.addEventListener('focus', checkForUpdate)
window.addEventListener('online', checkForUpdate)
document.addEventListener('visibilitychange', () =&gt; {
    if (document.visibilityState === 'visible') checkForUpdate()
})
checkForUpdate()
</code></pre>
<p>Skip registration entirely in the Base44 editor preview, and unregister anything already installed there. The reliable test is not the hostname, it is whether you are running inside a frame:</p>
<pre><code class="language-javascript">function isEditorPreview() {
    try {
        return window.self !== window.top
    } catch {
        return true // cross-origin frame, treat as preview
    }
}
</code></pre>
<p>The preview is not your deployment, and a worker installed against it will fight the editor's live reload and confuse every test you run afterwards.</p>
<p>Do not hand-write a precache list of hashed filenames. The build renames them every time, so a list written today points at files that will not exist next week, <code>install</code> fails, and the worker never activates. Precache the shell and the offline page, and let everything else fill in as it is visited.</p>
<h2>Offline means your database is gone, and that is the part people miss</h2>
<p>Here is what turns a working service worker into a broken app. Your cached shell boots. It renders. Then it does what Base44 wrote it to do, which is call your backend.</p>
<p>Every <code>base44.entities.Todo.list()</code> is an HTTPS request to a server on the other side of the planet. So is <code>base44.auth.me()</code>, so is every backend function you invoke, every AI call, every payment. With no network there is no server to talk to, and a cached page that renders a spinner until a query resolves will spin until the request times out and then show an error, if you wrote one.</p>
<p>The mental model has to change. You are not fetching data, you are reading data that is already on the device and refreshing it when you can.</p>
<table>
<thead>
<tr>
<th>What the user does offline</th>
<th>Works from cache alone</th>
<th>Needs a local data layer</th>
</tr>
</thead>
<tbody><tr>
<td>Opens the app, sees the shell and navigates</td>
<td>Yes</td>
<td>No</td>
</tr>
<tr>
<td>Loads JS, CSS and previously visited images</td>
<td>Yes</td>
<td>No</td>
</tr>
<tr>
<td>Reads their existing records</td>
<td>No</td>
<td>Yes, mirrored into IndexedDB</td>
</tr>
<tr>
<td>Creates or edits a record</td>
<td>No</td>
<td>Yes, optimistic write plus outbox</td>
</tr>
<tr>
<td>Runs a backend function, AI call or payment</td>
<td>No</td>
<td>No, disable and explain</td>
</tr>
<tr>
<td>Stays signed in</td>
<td>Only if you cached the user</td>
<td>Cache the user object locally</td>
</tr>
<tr>
<td>Sees another user's changes</td>
<td>No</td>
<td>No, resync on reconnect</td>
</tr>
</tbody></table>
<p>So: on first load, while online, pull the records this user needs and write them into IndexedDB along with the current user. From then on, read local first, render immediately, then refresh from Base44 in the background and merge. That is one code path that feels instant on a good connection and keeps working on none.</p>
<p>Use IndexedDB for records. <code>localStorage</code> is fine for a token and a small user object, and wrong for lists: synchronous, string-only, and small enough that a few hundred rows hit the wall. Keep entity responses out of the Cache API too. That cache is keyed by URL with no notion of who is signed in. Mirror rows into IndexedDB instead, where you control the shape and the lifetime.</p>
<h2>The write path: optimistic locally, queued, flushed on reconnect</h2>
<p>Every offline write is two operations. Update the local copy so the UI responds immediately, and record the intent so it can be replayed later. Keep the mirrored entity, an <code>outbox</code> of queued jobs, and tombstones for deletes.</p>
<pre><code class="language-javascript">const LOCAL_FIELDS = ['_key', '_entity', '_pending', '_deleted', '_localId']

function stripLocal(row) {
    const clean = { ...row }
    LOCAL_FIELDS.forEach(f =&gt; delete clean[f])
    return clean
}

async function create(values) {
    const id = `local_${Date.now().toString(36)}`
    const now = new Date().toISOString()

    await putRecord(entityName, { ...values, id, created_date: now, updated_date: now, _pending: true })
    await outboxAdd({ jobId: uid(), entity: entityName, op: 'create', id, values: stripLocal(values) })

    flush()
}

async function flush() {
    if (flushing || !navigator.onLine) return
    flushing = true

    try {
        for (const job of await jobsFor(entityName)) {
            try {
                if (job.op === 'create') {
                    const created = await api.create(job.values)
                    await deleteRecord(entityName, job.id)   // drop the temporary local row
                    await putRecord(entityName, created)
                } else if (job.op === 'update') {
                    await putRecord(entityName, await api.update(job.id, job.values))
                } else {
                    await api.delete(job.id)
                    await deleteRecord(entityName, job.id)
                }
                await outboxDelete(job.jobId)
            } catch (err) {
                const rejected = typeof err?.status === 'number' &amp;&amp; err.status &gt;= 400 &amp;&amp; err.status &lt; 500

                if (!rejected) break // network failure, keep the job queued

                await outboxDelete(job.jobId)
                await revert(job)
                emitError(err?.message || 'A change could not be saved and was undone.')
            }
        }
    } finally {
        flushing = false
    }
}

window.addEventListener('online', flushAll)
document.addEventListener('visibilitychange', () =&gt; {
    if (document.visibilityState === 'visible') flushAll()
})
</code></pre>
<p>Six details separate a sync layer from a data loss bug.</p>
<p>Strip your local-only fields before sending. Prefix them so this is mechanical rather than remembered: <code>_pending</code>, <code>_deleted</code>, <code>_localId</code>. A <code>_pending</code> flag that reaches the entity is a field the schema does not have, and the write is rejected. You will spend an hour blaming the queue.</p>
<p>Separate a transport failure from a server rejection, and use the status code rather than a string match on the error. A 4xx is your entity permissions or your schema refusing the write, and it will never succeed on retry, so drop the job, undo the local optimistic change, and tell the user. Anything else is the network, so stop and keep the queue intact for the next attempt. An offline write is a request, not a guarantee, and the UI should say pending until the server has accepted it.</p>
<p>A row created offline has no server id, so give it a local one, and when the create finally lands, delete the temporary row and store the server's version. Skip that and you get the same task twice.</p>
<p>Flush on <code>visibilitychange</code> as well as the <code>online</code> event. A phone that was asleep in a lift comes back through resume, and does not always fire a connectivity event.</p>
<p>Queue deletes explicitly and write a tombstone, a row flagged <code>_deleted</code> that your list filters out, until the delete lands. Without it the row reappears on the next pull.</p>
<p>For conflicts, compare <code>updated_date</code> and let the newer write win, with one exception: a pending or deleted local row always wins over the server copy, because it has not been sent yet and dropping it would lose the user's work. Base44 timestamps entities with <code>created_date</code> and <code>updated_date</code>, so use those field names rather than inventing your own.</p>
<h2>Auth, and why the app can look signed out when it is not</h2>
<p><code>base44.auth.me()</code> is a network call. If your app awaits it before rendering anything, an offline cold start hangs on it: the promise never resolves, the loading state never clears, and the user sees a spinner or a login screen even though they are signed in.</p>
<p>Cache the user object locally on every successful call, render from that copy when you are offline, and revalidate when the network returns.</p>
<pre><code class="language-javascript">function loadUser() {
    try {
        return JSON.parse(localStorage.getItem('user'))
    } catch {
        return null
    }
}

async function boot() {
    const user = loadUser()

    if (!navigator.onLine) {
        return user ? renderApp(user) : renderOfflineNotice()
    }

    const fresh = await base44.auth.me()
    localStorage.setItem('user', JSON.stringify(fresh))
    renderApp(fresh)
}

window.addEventListener('online', boot)
boot()
</code></pre>
<p>Treat that cached user as a rendering hint, not as authorization. Anything sensitive is verified server side on the next successful request, and the local mirror should only ever hold data that user was already allowed to read.</p>
<p>If you want the offline state to look different inside the native app than in a browser tab, the environment check is the user agent:</p>
<pre><code class="language-javascript">const isDespia = navigator.userAgent.toLowerCase().includes('despia')
</code></pre>
<h2>Verify in three steps, and never skip one</h2>
<p>This is the part that saves the money. Service workers, the Cache API and IndexedDB are web platform APIs. The Despia runtime runs the same engine your browser runs, so it does not add behaviour to them and it cannot repair them. Works in the browser, works in the PWA, works in the native build. Fails anywhere earlier and it fails everywhere later, except that in the binary the failure hides behind a native error screen and looks like a runtime bug.</p>
<p>So the order is fixed. Step one, prove updates still reach users. Step two, prove the web app works offline as an installed PWA. Step three, prove the native build works offline. People who jump to step three burn hundreds of dollars in AI tokens debugging something they could have excluded in ten minutes. Worse, they debug the wrong layer: hand an AI a failure from inside a native binary and it will confidently tell you the Xcode project is misconfigured, when the actual fault is a worker that never cached anything.</p>
<p>Before any of that, pick your model. Base44 lets you choose, and a service worker is exactly the kind of task where that matters, since it is one file that has to be right in several ways at once. Use the strongest model available to you and put it in build mode. Tools that decide the model for you are where these implementations usually go wrong.</p>
<p><strong>Step one: prove over-the-air updates still work, online.</strong></p>
<p>Do this before you test offline at all, because a worker that caches beautifully and never updates is worse than no worker.</p>
<p>Publish from Base44 and open the published URL, not the editor preview. Open developer tools, two finger click and choose inspect if you do not have the menu, and enable developer tools in browser settings if the option is missing. Go to the network tab and reload. The gear icon next to a request means it was served by the service worker. Check that your CSS is in there too, not only the HTML and JavaScript, or the app will render as a mess offline. Screenshot that tab and paste it into Base44 with a line like "here is the network tab from the dev tools, make sure the full app experience is cached". Being proactive here is much cheaper than debugging from inside the failure.</p>
<p>Ignore requests injected by browser extensions. They show up in the network tab, they look like your app failing to cache something, and they are not.</p>
<p>Then change something obvious and publish. The theme colour is the crudest and fastest signal: make it pink, publish, reload. If the colour does not change, the worker is corrupt, and no amount of offline testing means anything yet. Escalate through all three kinds of change before you move on: CSS styling, then HTML structure, then JavaScript logic, since a worker can pass one and fail the next.</p>
<p><strong>You will trap yourself, and that is normal.</strong> The first bad worker you ship installs itself on your own machine, and from then on your browser keeps serving the old copy no matter how many times you fix the code and reload. Adding a <code>?cachebust=123</code> query parameter sometimes works. Opening a 404 route sometimes works. Often neither does. The practical answer is a second browser and a private window: Safari when you have been testing in Chrome, then a fresh incognito window each time. Clearing the cache properly is workable on macOS and Windows, awkward on Android, and genuinely painful on iOS. Assume you will need clean sessions and stop fighting it.</p>
<p>When something is wrong, tell the AI exactly what you observed, in plain language, including where you observed it. These two cover the two failures you will actually hit:</p>
<pre><code class="language-text">I made the update to pink and if I reload my app, the update did not go through.
This means the service worker you wrote is corrupted. The service worker needs to load
the latest web code if the user is online and update over the air. If the user is
offline, serve cache, but once online make a new latest copy.
</code></pre>
<pre><code class="language-text">I tested it as a PWA on iOS and I get an error saying Safari cannot load the website
while offline. That means your service worker does not cache the data for offline PWA
usage. I need it to load the latest version if the network is online, and load cache
only if the network is offline. Never show old cache when online, and show the latest
cache when offline.
</code></pre>
<p>Attach the network tab screenshot. Ask for a plan first when you are two rounds in and it is still wrong, so the model reads the whole registration path instead of patching one file. And read the plan before approving it: a common AI escape route is to replace the worker with a cleanup worker that unregisters itself, which does guarantee fresh code and also deletes the offline support you asked for.</p>
<p>Do not take "I tested it and it works" from an AI tool as a result. Their browser testing and your own physical device are not the same thing. Getting a service worker right usually takes several rounds even with good documentation, so plan for the loop rather than a one-shot.</p>
<p><strong>Step two: install it as a PWA and go offline.</strong></p>
<p>You cannot test offline behaviour in a Safari tab on iOS, and testing it inside a native binary is the hardest environment to debug. The installed PWA is the middle rung: no Xcode, no Android Studio, no store review, and it exercises exactly the same worker. If your app is not meant to ship as a PWA, ask Base44 to make it one anyway and strip it later.</p>
<p>Open your published URL on a real phone and add it to the home screen. If it opens with an address bar at the top or a share and reload bar at the bottom, it opened as a browser tab, not a PWA, and you are missing a web app manifest and the Apple specific head tags. Fix that even if you never intend to ship a PWA, because you need it to run this test. If it still opens wrong after the fix, delete it from the home screen, reload the page, and add it again, waiting a couple of seconds before tapping add.</p>
<p>Then use the app online first. Load the screens that matter, sign in, create a record. That is what fills the caches and writes rows into IndexedDB, and a worker with an empty cache is indistinguishable from no worker.</p>
<p>Now turn off Wi-Fi and cellular data separately in system settings. Not airplane mode, since some devices keep Wi-Fi alive in it and the test passes for the wrong reason.</p>
<p>Your offline indicator lighting up proves nothing. It is interface, rendered by an app that is already open and already running. Force quit from the app switcher and launch it cold. That is the only version of this test that means anything, and it is where a worker that looked fine in the network tab will show you an address bar and a message about not being able to load the page.</p>
<p>What you want to see: the app opens, your existing records are on screen, and a new record can be created and shows as pending. Then turn the radios back on, watch the pending state clear, and reload the app on your desktop to confirm the record actually reached the database.</p>
<p><strong>Step three: the native build.</strong></p>
<p>Only now flip Offline Support to PWA in Despia, mark the version bump as a medium update, publish, and install the new build from TestFlight or internal testing. A deployment takes a few minutes, occasionally up to thirty. Run the same sequence: use it online, kill both radios, force quit, cold launch past the native splash screen, read your data, write a record, reconnect and watch it sync. If steps one and two passed, this one passes, because it is the same web app with the same caches inside a native shell.</p>
<p>The one thing that genuinely differs is the very first launch. A hosted app has to be opened once with a working connection before the worker can install and precache anything, so a user who installs from the store on a plane has nothing to serve.</p>
<h2>Wrapping it as a native app, and why the built-in publishing will not do</h2>
<p>Base44's own mobile publishing is a thin wrapper. It puts your web app in a binary and that is the extent of it: no in-app purchases, no StoreKit or Android billing, no push provider, no iCloud key value storage or Android backup for data that survives an uninstall. Most relevant here, it does not register app bound domains on iOS, which is what a service worker needs in order to register inside a native binary at all. Offline support is an advanced feature, and a minimal wrapper is the wrong tool for it.</p>
<p>Two options actually work.</p>
<p>Capacitor is free and open source. You will need a Mac, Xcode, and Android Studio, and you will be archiving projects, editing plist values and the Android manifest by hand. If that is comfortable, it costs nothing but time.</p>
<p>Despia is the paid one, and it is ours, so weigh that accordingly. It exposes the same native capabilities without a Mac or a CLI: copy your Base44 project link into Settings &gt; Configuration &gt; Dynamic App Source as the app start URL, set Offline Support to PWA, then under Versioning choose a medium build version update, confirm, and publish. That is the app bound domain registration and the native offline provisioning done as a setting rather than an Xcode session.</p>
<p>One thing you will have to fix by hand either way: safe areas. Wrapped web content runs edge to edge, so your header slides under the status bar and the notch. Pull the safe area documentation, paste it into Base44, and let it apply the padding utilities. That is a web change, so it ships over the air with your next publish and needs no resubmission.</p>
<h2>The pitfalls that generate the most support tickets</h2>
<p><strong>A worker that is not really at the root.</strong> If <code>/sw.js</code> returns your app's HTML rather than JavaScript, the file is not being served from <code>/public</code> the way you think it is. Registration fails on the content type, and the error message points at your code rather than your hosting.</p>
<p><strong>A stale worker that outlives your fix.</strong> A cache-first worker keeps serving the old app to everyone who already installed it, including after you fix the worker, because <code>sw.js</code> is itself a file the old worker is caching. <code>updateViaCache: 'none'</code> prevents it. If you are already in that hole with real users, ship a kill switch, serve the worker path with no-cache headers, and let it run once.</p>
<pre><code class="language-javascript">self.addEventListener('install', () =&gt; self.skipWaiting())
self.addEventListener('activate', event =&gt; {
    event.waitUntil(
        caches.keys()
            .then(keys =&gt; Promise.all(keys.map(k =&gt; caches.delete(k))))
            .then(() =&gt; self.registration.unregister())
            .then(() =&gt; self.clients.claim())
    )
})
</code></pre>
<p><strong>A cached document pointing at deleted assets.</strong> If the HTML is cached and the hashed JavaScript it references is not, the app opens to a white page. Network-first documents plus a versioned cache prevents it.</p>
<p><strong>iOS holding on to an old registration.</strong> Safari caches service worker registrations aggressively. If you installed an earlier version to the home screen, a new worker may not take, and deleting the icon and adding it again is faster than any amount of debugging.</p>
<p><strong>Testing in the Base44 editor preview.</strong> The preview is not your deployment. Scope, headers and worker behaviour only mean anything on the published URL that Despia actually loads.</p>
<p><strong>Debugging the native build first.</strong> Almost every reported offline failure is a worker that never cached anything, and the binary is the worst place to find that out.</p>
<h2>A prompt that sets this up in one shot</h2>
<p>Paste this into Base44. It covers the worker, the local mirror and the queue together, because asking for offline support alone reliably produces a cache-first worker and nothing else. Expect to iterate anyway.</p>
<pre><code class="language-text">Add offline support to this app. Read https://setup.despia.com/native-features/service-workers first.
This app is shipped as a native iOS and Android build with Despia, and the runtime allows a standard
service worker.

Service worker, always fresh online, cache only as an offline fallback:
- Put the worker at public/sw.js so it is served from the root of the published app, and register it
  with scope '/' and updateViaCache: 'none'. Put offline.html and the web app manifest in public/ too.
- Documents and non-hashed files: network-first with a 3 second timeout and cache: 'no-store' on the
  document request, so no browser or CDN copy shadows a new build. Enable navigation preload.
- Content-hashed build output under /assets/: cache-first, safe because the file name changes on every
  build.
- Offline fallback chain: last cached copy of that page, then the app shell, then /offline.html.
- Do NOT use cache-first for navigation. It pins users to an old build and breaks over-the-air updates.
- Do NOT hand-write a precache list of hashed filenames. Precache only '/' and '/offline.html'.
- Version the cache name, delete every non-matching cache on activate, skipWaiting on install,
  clients.claim on activate.
- Call registration.update() on load, focus, online and visibilitychange, and reload the page on
  controllerchange so a new worker takes effect immediately.
- Do not register the worker in the Base44 editor preview (detect it with window.self !== window.top),
  and unregister any worker found there.
- Add a web app manifest and Apple specific head tags so the app installs as a real PWA.

Local data layer, because the backend is unreachable offline:
- Mirror the entities this app reads into IndexedDB. Read from IndexedDB first and render from it, then
  refresh from Base44 in the background and merge.
- Never block first render on a network call. No await on an entity read or base44.auth.me() before
  something is on screen. Cache the user object locally and render from it when offline.
- Writes are optimistic: update the local row with a _pending flag and queue a job in an IndexedDB outbox.
- Give rows created offline a temporary local id, and when the create lands, delete the temporary row and
  store the server's version so the record does not appear twice.
- Flush the outbox on the window 'online' event and on visibilitychange. Strip local-only fields (_pending,
  _deleted, _localId) before sending. On a 4xx, drop the job, revert the local change and surface an error,
  since a permissions or schema failure never succeeds on retry. On anything else, stop and keep the queue.
- Queue deletes explicitly and write a _deleted tombstone so deleted rows do not come back on the next pull.
- Resolve conflicts on updated_date, newest write wins, except that a pending local row always wins.
- Show a clear pending or offline indicator in the UI.
- Disable online-only features (backend functions, AI calls, payments, email) with an explanation rather
  than letting them fail silently.

Do not add a PWA plugin that generates a cache-first worker.
</code></pre>
<p>Then enable it on the native side: Settings &gt; Configuration &gt; Dynamic App Source &gt; Offline Support &gt; PWA, bump the version code, and publish a new build.</p>
<h2>Get it on the stores</h2>
<p>Take the app you just built and ship it to iOS and Android without a CLI or a Mac. Code signing and submission run from the browser.</p>
<p><a href="https://setup.despia.com">See the setup docs at setup.despia.com</a></p>
]]></content:encoded></item><item><title><![CDATA[Lovable Offline Mode: Service Workers and Local Data]]></title><description><![CDATA[https://youtu.be/dSqgoAYM6qw

Most people try this once, in the wrong order. They flip Offline Support to PWA in Despia, rebuild, put the phone in airplane mode, and the app opens to the native networ]]></description><link>https://blog.despia.com/lovable-offline-mode-service-workers-and-local-data</link><guid isPermaLink="true">https://blog.despia.com/lovable-offline-mode-service-workers-and-local-data</guid><category><![CDATA[lovable]]></category><category><![CDATA[webapp]]></category><category><![CDATA[PWA]]></category><category><![CDATA[indexeddb]]></category><category><![CDATA[Web API]]></category><dc:creator><![CDATA[Despia CEO]]></dc:creator><pubDate>Sun, 09 Aug 2026 05:03:28 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/67f0239ff4f5a0b7b4a852c4/4f613d86-144f-4143-9ace-aae93f2bddab.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a class="embed-card" href="https://youtu.be/dSqgoAYM6qw">https://youtu.be/dSqgoAYM6qw</a></p>

<p>Most people try this once, in the wrong order. They flip Offline Support to PWA in Despia, rebuild, put the phone in airplane mode, and the app opens to the native network error page. Or it opens, shows the shell, and then spins forever on an empty screen. Or worse: it works offline, and now none of their updates reach users ever again.</p>
<p>Two things are going on. Caching the app and running the app offline are separate problems, and a service worker that solves the first one badly will quietly break your deployments. A worker keeps your HTML, JavaScript and CSS available without a network. It does nothing for the Lovable Cloud queries, edge functions and third party APIs your app calls the moment it boots. And if it answers every request from its stored copy, your users are pinned to whatever version they installed first.</p>
<p>The video above is the whole build, unedited: prompting Lovable, catching the worker serving stale code, arguing it back into shape, and testing it on a real phone. The code is open source at <a href="https://github.com/despia-native/lovable-service-worker-sample">despia-native/lovable-service-worker-sample</a>, a small Lovable todo app with email login, a hand-written worker, an IndexedDB mirror and an outbox, under Apache 2.0. Getting it right took about 42 minutes even with good documentation, and you can hand that repository URL to Lovable as a reference instead of repeating the exercise. This post is the reasoning behind it.</p>
<h2>What the Despia offline toggle actually does</h2>
<p>In the Despia Editor, go to Settings &gt; Configuration and scroll to Dynamic App Source. That is where your Start URL points at your published Lovable project, and directly underneath it is Offline Support, with three options: none, PWA, and native. Select PWA. Then bump the version code and publish a new build for iOS, Android, or both.</p>
<p>That toggle provisions the Xcode and Android Studio projects to allow service workers in the native binary, without you touching either. Done manually, this is registering app bound domains on iOS and the equivalent on Android, in native project settings, which is not a path a non-native developer wants to walk. Here it is one setting and a rebuild.</p>
<p>What it does not do is write a service worker. If your Lovable app does not ship one, or ships one that never caches anything, the toggle changes nothing and every offline launch fails exactly as before. Despia removes the native obstacle. The caching strategy is yours, in your web codebase, using standard web APIs.</p>
<p>The other thing worth saying out loud, because it costs people an afternoon: offline support is a native runtime setting, so it only exists in binaries produced after the toggle was flipped. If you enable it and keep testing the build already on your phone, service worker registration is silently ignored, caches stay empty, and the Editor still reads as on. Install the new build before you conclude anything.</p>
<h2>Which Lovable stack are you on</h2>
<p>TanStack Start became the default for new Lovable projects on May 13, 2026, and for new Enterprise projects on June 22, 2026. Anything created before that is the older React plus Vite stack, a client-rendered single page app deployed as static files. The stack is locked at creation and a prompt will not change it, so this is a fact about your project, not a choice you get to make now. Check your exported code for <code>vite.config.ts</code>, or open your published URL with JavaScript disabled and see whether content arrives in the HTML.</p>
<p>Both stacks work. The sample repository is TanStack Start on Lovable Cloud, so server rendering is not a blocker. Two things differ. Hashed build output lives under <code>/assets/</code> on the Vite stack and <code>/_build/</code> on TanStack Start, and your worker needs to recognise both if you want a rule that survives a stack change. And on the SSR stack a cached route document is a snapshot of a server render taken when it was cached, so it is an offline fallback rather than a source of truth, which is exactly how the strategy below treats it.</p>
<h2>A worker that stays fresh online and only falls back offline</h2>
<p>This is the part that decides whether your app can still be updated. Say it as a rule and it becomes obvious:</p>
<p>A bad worker asks "is there a cached copy" and serves it. A good worker asks "am I online", takes the network when the answer is yes, and only reaches for the cache when the network genuinely fails.</p>
<p>Cache-first on navigation is the default in most tutorials and in most AI-generated workers, and it is the single worst choice for an app that updates over the air. You publish in Lovable, the content updates on your host, and nobody sees it. Here is the shape that works, condensed from <code>public/sw.js</code> in the sample:</p>
<pre><code class="language-javascript">const VERSION = 'v7'
const CACHE = `app-${VERSION}`
const PRECACHE = ['/', '/offline.html']
const NETWORK_TIMEOUT_MS = 3000

// content-hashed build output, safe to cache forever because the name changes
function isHashedAsset(url) {
    return /^\/(assets|_build)\//.test(url.pathname)
        &amp;&amp; /\.[0-9a-zA-Z_-]{8,}\.(js|css|woff2?|png|jpg|jpeg|svg|webp|avif|ico)$/.test(url.pathname)
}

self.addEventListener('install', event =&gt; {
    event.waitUntil((async () =&gt; {
        const cache = await caches.open(CACHE)
        await Promise.allSettled(PRECACHE.map(async path =&gt; {
            const res = await fetch(path, { cache: 'no-store' })
            if (res.ok) await cache.put(path, res)
        }))
        await self.skipWaiting()
    })())
})

self.addEventListener('activate', event =&gt; {
    event.waitUntil((async () =&gt; {
        if (self.registration.navigationPreload) {
            await self.registration.navigationPreload.enable().catch(() =&gt; {})
        }
        const names = await caches.keys()
        await Promise.allSettled(
            names.filter(n =&gt; n.startsWith('app-') &amp;&amp; n !== CACHE).map(n =&gt; caches.delete(n))
        )
        await self.clients.claim()
    })())
})

self.addEventListener('fetch', event =&gt; {
    const { request } = event
    if (request.method !== 'GET') return

    const url = new URL(request.url)
    if (url.origin !== self.location.origin) return

    if (isHashedAsset(url)) {
        event.respondWith(cacheFirst(request))
        return
    }

    const isDocument = request.mode === 'navigate'
    event.respondWith((async () =&gt; {
        try {
            return await fromNetwork(request, {
                preload: isDocument ? event.preloadResponse : undefined,
                noStore: isDocument
            })
        } catch {
            return offlineFallback(request)
        }
    })())
})

async function fromNetwork(request, { preload, noStore } = {}) {
    const cache = await caches.open(CACHE)
    const attempt = preload
        ? Promise.resolve(preload).then(res =&gt; res || fetch(request))
        : fetch(noStore ? new Request(request, { cache: 'no-store' }) : request)

    const timeout = new Promise((_, reject) =&gt;
        setTimeout(() =&gt; reject(new Error('network-timeout')), NETWORK_TIMEOUT_MS)
    )

    const res = await Promise.race([attempt, timeout])
    if (res &amp;&amp; res.ok &amp;&amp; res.type !== 'opaque') {
        cache.put(request, res.clone()).catch(() =&gt; {})
    }
    return res
}

async function offlineFallback(request) {
    const cache = await caches.open(CACHE)
    return (await cache.match(request))
        || (request.mode === 'navigate'
            ? (await cache.match('/')) || (await cache.match('/offline.html'))
            : undefined)
        || Response.error()
}
</code></pre>
<p>Four details in there are not optional.</p>
<p>The timeout matters more than it looks. A hanging connection does not reject <code>fetch()</code>, it sits there, so a plain <code>fetch().catch()</code> fallback stalls on a bad train connection instead of falling back. Race it.</p>
<p><code>cache: 'no-store'</code> on document fetches bypasses the browser's own HTTP cache and any CDN copy. Without it, the worker can go to the network, do everything right, and still be handed a stale HTML response, which looks identical to a broken worker and sends you debugging the wrong file.</p>
<p>Navigation preload starts the document request in parallel with worker startup, which removes the roughly one second delay you will otherwise see on the document in the network tab.</p>
<p>And the offline fallback is a chain, not a single file: the last cached copy of that exact page, then the app shell, then <code>/offline.html</code>. Make <code>/offline.html</code> render something useful, because it is the last thing standing between a dropped request and a blank screen.</p>
<p>Registration matters as much as the worker. <code>updateViaCache: 'none'</code> stops the browser reusing a cached copy of <code>sw.js</code> itself, which is how a broken worker becomes permanent. Then check for updates on load, on focus, on reconnect, and reload when control changes hands:</p>
<pre><code class="language-javascript">const registration = await navigator.serviceWorker.register('/sw.js', {
    scope: '/',
    updateViaCache: 'none'
})

let reloading = false
navigator.serviceWorker.addEventListener('controllerchange', () =&gt; {
    if (reloading) return
    reloading = true
    window.location.reload()
})

const checkForUpdate = () =&gt; void registration.update().catch(() =&gt; {})
window.addEventListener('focus', checkForUpdate)
window.addEventListener('online', checkForUpdate)
document.addEventListener('visibilitychange', () =&gt; {
    if (document.visibilityState === 'visible') checkForUpdate()
})
checkForUpdate()
</code></pre>
<p>One more thing the sample does that is easy to miss: it refuses to register at all on Lovable preview and dev hostnames, and unregisters anything already there. The editor preview is not your deployment, and a worker installed against it will fight hot reload and confuse every test you run afterwards.</p>
<p>Do not hand-write a precache list of hashed filenames. Vite renames them on every build, so a list written today points at files that will not exist next week, <code>install</code> fails, and the worker never activates. Precache the shell and the offline page, and let everything else fill in as it is visited.</p>
<h2>Offline means your database is gone, and that is the part people miss</h2>
<p>Here is what turns a working service worker into a broken app. Your cached shell boots. It renders. Then it does what Lovable wrote it to do, which is call your backend.</p>
<p>Lovable Cloud is a managed Supabase instance, and connecting your own Supabase project is the alternative. Either way, every <code>supabase.from(...)</code> read is an HTTPS request to a server on the other side of the planet. So is every edge function, every AI call, every Stripe request. With no network there is no server to talk to, and a cached page that renders a spinner until a query resolves will spin until the request times out and then show an error, if you wrote one.</p>
<p>The mental model has to change. You are not fetching data, you are reading data that is already on the device and refreshing it when you can.</p>
<table>
<thead>
<tr>
<th>What the user does offline</th>
<th>Works from cache alone</th>
<th>Needs a local data layer</th>
</tr>
</thead>
<tbody><tr>
<td>Opens the app, sees the shell and navigates</td>
<td>Yes</td>
<td>No</td>
</tr>
<tr>
<td>Loads JS, CSS and previously visited images</td>
<td>Yes</td>
<td>No</td>
</tr>
<tr>
<td>Reads their existing records</td>
<td>No</td>
<td>Yes, mirrored into IndexedDB</td>
</tr>
<tr>
<td>Creates or edits a record</td>
<td>No</td>
<td>Yes, optimistic write plus outbox</td>
</tr>
<tr>
<td>Runs an edge function, AI call or payment</td>
<td>No</td>
<td>No, disable and explain</td>
</tr>
<tr>
<td>Stays signed in</td>
<td>Session persists locally</td>
<td>Token refresh needs network</td>
</tr>
<tr>
<td>Receives a realtime update</td>
<td>No</td>
<td>No, resync on reconnect</td>
</tr>
</tbody></table>
<p>So: on first load, while online, pull the records this user needs and write them into IndexedDB along with the current user. From then on, read local first, render immediately, then refresh from the backend in the background and merge. That is one code path that feels instant on a good connection and keeps working on none.</p>
<p>Use IndexedDB for records. <code>localStorage</code> is fine for a token and a small user object, and wrong for lists: synchronous, string-only, and small enough that a few hundred rows hit the wall. Keep authenticated backend responses out of the Cache API too. That cache is keyed by URL with no notion of who is signed in. Mirror rows into IndexedDB instead, where you control the shape and the lifetime.</p>
<h2>The write path: optimistic locally, queued, flushed on reconnect</h2>
<p>Every offline write is two operations. Update the local copy so the UI responds immediately, and record the intent so it can be replayed later. The sample keeps three IndexedDB stores for this: the mirrored table, an <code>outbox</code> of queued jobs, and <code>tombstones</code> for deletes.</p>
<pre><code class="language-javascript">async function saveTodo(todo) {
    const row = { ...todo, updated_at: new Date().toISOString(), pending: true }

    await localTodos.put(row)
    await outbox.add({ type: 'insert', row })

    flushOutbox()
    return row
}

async function flushOutbox() {
    if (!navigator.onLine) return

    for (const job of await outbox.all()) {
        const error = await runJob(job)

        if (error) {
            // never reached the server, keep it queued and try again later
            if (isNetworkFailure(error)) return

            // server said no, retrying will not change that
            await outbox.remove(job.key)
            await revertJob(job)
            reportSyncError(error)
            continue
        }

        await outbox.remove(job.key)
        await settleJob(job)
    }
}

window.addEventListener('online', flushOutbox)
document.addEventListener('visibilitychange', () =&gt; {
    if (document.visibilityState === 'visible') flushOutbox()
})
</code></pre>
<p>Five details separate a sync layer from a data loss bug.</p>
<p>Strip your local-only fields before sending. A <code>pending</code> flag that reaches Postgres is an unknown column and the write is rejected, and you will spend an hour blaming the queue.</p>
<p>Separate a transport failure from a server rejection. A dropped connection should keep the job queued. A row your Row Level Security policies refuse will never succeed, so drop it, undo the local optimistic change, and tell the user. An offline write is a request, not a guarantee, and the UI should say pending until the server has accepted it.</p>
<p>Flush on <code>visibilitychange</code> as well as the <code>online</code> event. A phone that was asleep in a lift comes back through resume, and does not always fire a connectivity event.</p>
<p>Queue deletes explicitly and keep a tombstone until the delete lands, or the row reappears on the next pull.</p>
<p>For conflicts, compare <code>updated_at</code> and let the newer write win, with one exception the sample makes explicit: a pending local write always wins over the server copy, because it has not been sent yet and dropping it would lose the user's work.</p>
<h2>Auth, and why the app can look signed out when it is not</h2>
<p>The Supabase client persists the session in local storage by default, so a cold start with no network still has a session to read. The app appears signed out anyway when first render is blocked on a call that cannot complete: a session refresh, a profile fetch, a role lookup. The await never resolves, the loading state never clears, and the user sees a spinner or a login screen.</p>
<p>Read the stored session, render from it, then revalidate when the network returns.</p>
<pre><code class="language-javascript">function loadSession() {
    try {
        return JSON.parse(localStorage.getItem('session'))
    } catch {
        return null
    }
}

async function boot() {
    const session = loadSession()

    if (!navigator.onLine) {
        return session ? renderApp(session) : renderOfflineNotice()
    }

    const fresh = await refreshSession()
    localStorage.setItem('session', JSON.stringify(fresh))
    renderApp(fresh)
}

window.addEventListener('online', boot)
boot()
</code></pre>
<p>Treat that cached session as a rendering hint, not as authorization. Anything sensitive is verified server side on the next successful request, and the local mirror should only ever hold data that user was already allowed to read.</p>
<p>If you want the offline state to look different inside the native app than in a browser tab, the environment check is the user agent:</p>
<pre><code class="language-javascript">const isDespia = navigator.userAgent.toLowerCase().includes('despia')
</code></pre>
<h2>Verify in three steps, and never skip one</h2>
<p>This is the part that saves the money. Service workers, the Cache API and IndexedDB are web platform APIs. The Despia runtime runs the same engine your browser runs, so it does not add behaviour to them and it cannot repair them. Works in the browser, works in the PWA, works in the native build. Fails anywhere earlier and it fails everywhere later, except that in the binary the failure hides behind a native error screen and looks like a runtime bug.</p>
<p>So the order is fixed. Step one, prove updates still reach users. Step two, prove the web app works offline as an installed PWA. Step three, prove the native build works offline. People who jump to step three burn hundreds of dollars in AI tokens debugging something they could have excluded in ten minutes.</p>
<p>If you want to watch this happen rather than read it, this is most of the video above, including the parts where it does not work.</p>
<p><strong>Step one: prove over-the-air updates still work, online.</strong></p>
<p>Do this before you test offline at all, because a worker that caches beautifully and never updates is worse than no worker.</p>
<p>Publish from Lovable and open the published URL, not the editor preview. Open developer tools, two finger click and choose inspect if you do not have the menu, and enable developer tools in browser settings if the option is missing. Go to the network tab and reload. The gear icon next to a request means it was served by the service worker. Check that your CSS is in there too, not only the HTML and JavaScript, or the app will render as a mess offline. Screenshot that tab and paste it into Lovable with a line like "here is the network tab from the dev tools, make sure the full app experience is cached". Being proactive here is much cheaper than debugging from inside the failure.</p>
<p>Ignore requests injected by browser extensions. They show up in the network tab, they look like your app failing to cache something, and they are not.</p>
<p>Then change something obvious and publish. The theme colour is the crudest and fastest signal: make it pink, publish, reload. If the colour does not change, the worker is corrupt, and no amount of offline testing means anything yet. Escalate through all three kinds of change before you move on: CSS styling, then HTML structure, then JavaScript logic, since a worker can pass one and fail the next.</p>
<p><strong>You will trap yourself, and that is normal.</strong> The first bad worker you ship installs itself on your own machine, and from then on your browser keeps serving the old copy no matter how many times you fix the code and reload. Adding a <code>?cachebust=123</code> query parameter sometimes works. Opening a 404 route sometimes works. Often neither does. The practical answer is a second browser and a private window: Safari when you have been testing in Chrome, then a fresh incognito window each time. Clearing the cache properly is workable on macOS and Windows, awkward on Android, and genuinely painful on iOS. Assume you will need clean sessions and stop fighting it.</p>
<p>When the worker is wrong, tell the AI exactly what you observed. These two prompts came out of the sample build and both moved it forward:</p>
<pre><code class="language-text">I made the update to pink and if I reload my app, the update did not go through.
This means the service worker you wrote is corrupted. The service worker needs to load
the latest web code if the user is online and update over the air. If the user is
offline, serve cache, but once online make a new latest copy.
</code></pre>
<pre><code class="language-text">I opened a private Safari window and tested the colour change. The app is still green
and does not show the updates I made, so it is infinitely stuck on old page code, which
means the service worker is corrupted. Please fix it.
</code></pre>
<p>Attach the network tab screenshot. Ask for a plan first when you are two rounds in and it is still wrong, so the model reads the whole registration path instead of patching one file. And read the plan before approving it: at one point in the sample build the AI proposed replacing the worker with a cleanup worker that unregisters itself, which does guarantee fresh code and also deletes the offline support you asked for.</p>
<p>Do not take "I tested it and it works" from an AI tool as a result. Their browser testing and your own physical device are not the same thing.</p>
<p><strong>Step two: install it as a PWA and go offline.</strong></p>
<p>Open your published URL on a real phone and add it to the home screen. If it opens with an address bar at the top or a share and reload bar at the bottom, it opened as a browser tab, not a PWA, and you are missing a web app manifest and the Apple specific head tags. Fix that even if you never intend to ship a PWA, because you need it to run this test. If it still opens wrong after the fix, delete it from the home screen, reload the page, and add it again, waiting a couple of seconds before tapping add.</p>
<p>Then use the app online first. Load the screens that matter, sign in, create a record. That is what fills the caches and writes rows into IndexedDB, and a worker with an empty cache is indistinguishable from no worker.</p>
<p>Now turn off Wi-Fi and cellular data separately in system settings. Not airplane mode, since some devices keep Wi-Fi alive in it and the test passes for the wrong reason. Force quit the installed app and open it cold, because a warm relaunch proves nothing. You want to see: the app opens, your offline indicator appears, your existing records are on screen, a new record can be created and shows as pending. Turn the radios back on and watch that record sync.</p>
<p><strong>Step three: the native build.</strong></p>
<p>Only now flip Offline Support to PWA in Despia, bump the version code, publish, and install the new build. Run the same sequence: use it online, kill both radios, force quit, cold launch past the native splash screen, read your data, write a record, reconnect and watch it sync. If steps one and two passed, this one passes, because it is the same web app with the same caches inside a native shell.</p>
<p>The one thing that genuinely differs is the very first launch. A hosted app has to be opened once with a working connection before the worker can install and precache anything, so a user who installs from the store on a plane has nothing to serve.</p>
<h2>The pitfalls that generate the most support tickets</h2>
<p><strong>A stale worker that outlives your fix.</strong> A cache-first worker keeps serving the old app to everyone who already installed it, including after you fix the worker, because <code>sw.js</code> is itself a file the old worker is caching. <code>updateViaCache: 'none'</code> prevents it. If you are already in that hole with real users, ship a kill switch, serve the worker path with no-cache headers, and let it run once.</p>
<pre><code class="language-javascript">self.addEventListener('install', () =&gt; self.skipWaiting())
self.addEventListener('activate', event =&gt; {
    event.waitUntil(
        caches.keys()
            .then(keys =&gt; Promise.all(keys.map(k =&gt; caches.delete(k))))
            .then(() =&gt; self.registration.unregister())
            .then(() =&gt; self.clients.claim())
    )
})
</code></pre>
<p><strong>A cached document pointing at deleted assets.</strong> If the HTML is cached and the hashed JavaScript it references is not, the app opens to a white page. Network-first documents plus a versioned cache prevents it.</p>
<p><strong>Testing in the Lovable editor preview.</strong> The preview is not your deployment. Scope, headers and worker behaviour only mean anything on the published URL that Despia actually loads.</p>
<p><strong>Debugging the native build first.</strong> Almost every reported offline failure is a worker that never cached anything, and the binary is the worst place to find that out.</p>
<h2>A prompt that sets this up in one shot</h2>
<p>Paste this into Lovable. It covers the worker, the local mirror and the queue together, because asking for offline support alone reliably produces a cache-first worker and nothing else. Expect to iterate anyway. Getting the sample right took roughly 42 minutes of the loop above, and that is with the same instructions.</p>
<pre><code class="language-text">Add offline support to this app. Read https://setup.despia.com/native-features/service-workers first,
and use https://github.com/despia-native/lovable-service-worker-sample as a reference implementation
(public/sw.js, src/lib/sw-register.ts, src/lib/local-db.ts, src/lib/sync.ts). This app is shipped as a
native iOS and Android build with Despia, and the runtime allows a standard service worker.

Service worker, always fresh online, cache only as an offline fallback:
- Serve the worker from the root of the site, registered with scope '/' and updateViaCache: 'none'.
- Documents and non-hashed files: network-first with a 3 second timeout and cache: 'no-store' on the
  document request, so no browser or CDN copy shadows a new build. Enable navigation preload.
- Content-hashed build output under /assets/ or /_build/: cache-first, safe because the name changes.
- Offline fallback chain: last cached copy of that page, then the app shell, then /offline.html.
- Do NOT use cache-first for navigation. It pins users to an old build and breaks over-the-air updates.
- Do NOT hand-write a precache list of hashed filenames. Precache only '/' and '/offline.html'.
- Version the cache name, delete every non-matching cache on activate, skipWaiting on install,
  clients.claim on activate.
- Call registration.update() on load, focus, online and visibilitychange, and reload the page on
  controllerchange so a new worker takes effect immediately.
- Do not register the worker on Lovable preview or dev hostnames, and unregister any worker found there.
- Add a web app manifest and Apple specific head tags so the app installs as a real PWA.

Local data layer, because the database is unreachable offline:
- Mirror the tables this app reads into IndexedDB. Read from IndexedDB first and render from it, then
  refresh from the backend in the background and merge.
- Never block first render on a network call. No await on a query, session refresh or profile fetch
  before something is on screen.
- Writes are optimistic: update the local row with a pending flag and queue a job in an IndexedDB outbox.
- Flush the outbox on the window 'online' event and on visibilitychange. Strip local-only fields such as
  pending before sending. Keep a job queued on a network failure. On a server rejection, drop the job,
  revert the local change and surface an error, since RLS failures never succeed on retry.
- Queue deletes explicitly with a local tombstone so deleted rows do not come back on the next pull.
- Resolve conflicts on updated_at, newest write wins, except that a pending local write always wins.
- Show a clear pending or offline indicator in the UI.
- Disable online-only features (AI calls, payments, edge functions, email) with an explanation rather
  than letting them fail silently.

Do not add a PWA plugin that generates a cache-first worker.
</code></pre>
<p>Then enable it on the native side: Settings &gt; Configuration &gt; Dynamic App Source &gt; Offline Support &gt; PWA, bump the version code, and publish a new build.</p>
<h2>Get it on the stores</h2>
<p>Take the app you just built and ship it to iOS and Android without a CLI or a Mac. Code signing and submission run from the browser.</p>
<p><a href="https://setup.despia.com">See the setup docs at setup.despia.com</a></p>
]]></content:encoded></item><item><title><![CDATA[Lovable Mobile App Slow? Turn Off SSR in TanStack Start]]></title><description><![CDATA[Every tap flashes white. The screen you were on tears down, the spinner comes back, the data you already had is fetched again. On a laptop you would barely register it. On a phone, inside your own app]]></description><link>https://blog.despia.com/lovable-mobile-app-slow-turn-off-ssr-in-tanstack-start</link><guid isPermaLink="true">https://blog.despia.com/lovable-mobile-app-slow-turn-off-ssr-in-tanstack-start</guid><category><![CDATA[lovable]]></category><category><![CDATA[React]]></category><category><![CDATA[PWA]]></category><category><![CDATA[webapp]]></category><category><![CDATA[tanstack]]></category><dc:creator><![CDATA[Despia CEO]]></dc:creator><pubDate>Sat, 08 Aug 2026 06:50:58 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/67f0239ff4f5a0b7b4a852c4/4b2c271a-84d4-4d98-9794-18fce7dadd17.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Every tap flashes white. The screen you were on tears down, the spinner comes back, the data you already had is fetched again. On a laptop you would barely register it. On a phone, inside your own app, it reads as a website in a frame, and the conclusion most people reach is that the WebView is the problem and the fix is to rebuild the whole thing natively.</p>
<p>It usually is not. Look at what your Lovable project is rendering before you rewrite it.</p>
<p><strong>Short answer:</strong> new Lovable projects ship on TanStack Start with server-side rendering turned on. SSR puts your server in the middle of page transitions. In a browser tab on wifi that costs almost nothing. In a WebView on cellular it is the whole difference between an app and a website. Turn SSR off for the routes that make up the app, keep it on the routes that need to rank, and navigation gets fast without changing your stack.</p>
<h2>What changed in Lovable</h2>
<p>Lovable used to generate React plus Vite single page apps, deployed as static files and rendered entirely on the client. New projects moved to TanStack Start with SSR on by default on May 13, 2026, and new Enterprise projects on June 22, 2026. Older projects stayed on the Vite stack unless you took the upgrade.</p>
<p>That means there are two populations of Lovable app, and only one of them has this problem. If your project predates May 2026 and you never upgraded, your slowness is something else: bundle size, unindexed Supabase queries, or fetch waterfalls. If you started after that date, or you took the SSR upgrade because someone told you it would fix your Google ranking, keep reading.</p>
<p>The reason Lovable made the change is sound. A client-rendered app hands crawlers a mostly empty page, and SSR hands everyone the finished HTML: visitors, search engines, AI crawlers, link preview bots. That is a real win for a public website.</p>
<p>Your mobile app has no crawlers in it.</p>
<h2>Why SSR is the wrong trade inside a WebView</h2>
<p>On the web, SSR trades a round trip for a faster first paint and readable HTML. The round trip is short because your user is on a laptop on wifi, and they land on one page from Google and mostly stay there.</p>
<p>A mobile app inverts every one of those assumptions. Your user is on cellular with 80 to 200ms of latency before anything moves. They do not land on one page, they tap through twenty screens in a session. And there is no crawler to serve, because the App Store listing is what gets indexed, not your routes.</p>
<p>So you are paying the round trip twenty times to buy something the app cannot use.</p>
<h2>The two things producing the full reload</h2>
<p><strong>Navigation that is not router navigation.</strong> Generated code reaches for <code>&lt;a href="/dashboard"&gt;</code> and <code>window.location.href = '/settings'</code> more often than you would expect, and both of those are full document loads. The WebView throws away the page, asks your server for HTML, waits for the server render, downloads and re-evaluates the bundle, hydrates, and re-runs every fetch on the new route. Your in-memory state is gone. So is anything you had registered on <code>window</code>, which matters more in a native runtime than in a browser tab, because your native callbacks get torn down and re-registered on every screen change.</p>
<p>Under the old Vite stack this was already wasteful. Under SSR you added a server render in front of it.</p>
<p><strong>Redirects in route guards.</strong> Auth-gated apps redirect on entry. When that redirect is thrown during a server render, it comes back as an HTTP redirect, and an HTTP redirect is another document load. Sign-in flows and protected sections are exactly the places people report the worst lag, and this is why.</p>
<p>Even when navigation is correct and the router handles it, SSR still costs you. Route loaders run on the server, so a screen change waits for your server, which is waiting for Supabase. Two hops instead of one, on every tap.</p>
<h2>Check this before you change anything</h2>
<p>Open the deployed app in a desktop browser, open the Network panel, tick preserve log, and tap through five screens.</p>
<p>If the log clears itself or you see a new <code>document</code> request per navigation, those are real page loads and you have the first problem. If you only see fetch and XHR entries and the screens are still slow, routing is fine and you are looking at loader latency, which is the second problem. Both are fixed by the same change, but knowing which one you have tells you what to expect.</p>
<p>Test the deployed URL, not the Lovable preview. The preview runs under different conditions and will lie to you about this specific thing.</p>
<h2>Turn SSR off for the app routes</h2>
<p>You do not have to write any of this. Open the Lovable chat and paste one of the two prompts below. Which one depends on whether your Lovable project contains a public marketing site as well as the app.</p>
<p><strong>If the Lovable project is only the app</strong> (a login screen and everything behind it, with your marketing site somewhere else or not existing yet), paste this:</p>
<pre><code class="language-text">My project is a mobile app. It does not need SEO and nothing in it is
crawled by search engines, so server-side rendering is costing me speed
for no benefit. Please make it a fully client-rendered app and make
in-app navigation instant.

Do all of the following:

1. Disable SSR globally in TanStack Start by setting defaultSsr to false
   in the start instance.
2. Find every place that navigates by changing the URL directly, such as
   window.location.href, window.location.assign, window.location.replace,
   or a plain &lt;a href&gt; pointing at an internal route. Replace all of them
   with the router's Link component or the navigate function so no
   navigation triggers a full page load. Leave external links alone.
3. Move route data loading to the client so route changes do not wait on
   a server round trip.
4. Make sure any redirects in auth guards happen through the router on
   the client, not as server redirects.
5. Enable link preloading on intent so a screen starts loading when the
   user touches the link.
6. Add a lightweight loading state for routes that fetch data, so the
   screen changes immediately and the content fills in, instead of the
   app freezing until the data arrives.

Do not change my UI, my styling, or my Supabase schema. Tell me which
files you changed and list every navigation you converted.
</code></pre>
<p><strong>If the same project holds your landing page and your app,</strong> paste this instead:</p>
<pre><code class="language-text">My project has a public marketing site and a logged-in app. The marketing
pages need SEO. The app does not, and server-side rendering is making
in-app navigation slow on mobile.

Do all of the following:

1. Keep the public marketing routes server-side rendered exactly as they
   are. Do not set defaultSsr to false, because that would disable SSR on
   the root route and cascade to everything.
2. Set ssr: false on the layout route that all of the logged-in app
   routes sit under, so it cascades down to its children only.
3. Inside the app section, replace every window.location navigation and
   every plain &lt;a href&gt; pointing at an internal route with the router's
   Link component or the navigate function.
4. Move data loading for the app routes to the client so route changes do
   not wait on a server round trip.
5. Make sure auth guard redirects inside the app happen through the
   router on the client, not as server redirects.
6. Enable link preloading on intent, and add a loading state for app
   routes that fetch data.

Do not change my UI, my styling, or my Supabase schema. Tell me which
routes are still server-rendered when you are done.
</code></pre>
<p>Ask for the list of changed files at the end because that is your receipt. If Lovable comes back having touched your marketing routes, say so and have it put them back.</p>
<p>The code below is here so you can check what you got, which is a different skill from writing it and a more useful one. The global switch lives in the start instance:</p>
<pre><code class="language-ts">// src/start.ts
import { createStart } from '@tanstack/react-start'

export const startInstance = createStart(() =&gt; ({
  defaultSsr: false,
}))
</code></pre>
<p>And the per-route version, which is the one you usually want:</p>
<pre><code class="language-ts">export const Route = createFileRoute('/_app')({
  ssr: false,
  component: AppLayout,
})
</code></pre>
<p>The inheritance rule matters and it catches people. A parent route's <code>ssr</code> value cascades to its children. Setting <code>defaultSsr: false</code> turns SSR off at the root, which means a route-level <code>ssr: true</code> further down will not bring it back. So split downward: leave the default alone, and switch SSR off on the layout route that everything in the app sits under.</p>
<h2>It will look like nothing happened</h2>
<p>Give it a few minutes. Edge caches and the WebView both hold onto the old build, so the first test after the change often shows you exactly what you saw before and sends you off chasing the wrong thing.</p>
<p>Wait, force quit the app, and reopen it. Despia's default deployment fetches the current web build on launch, so there is no rebuild and no store submission for a change like this, but the launch has to actually happen. Reopening from the app switcher is not the same as reopening the app.</p>
<p>The reports we get back after this land around 5x on in-app navigation. That number is not a benchmark, it is what happens when a screen change stops being a document load and starts being a state change.</p>
<h2>The split that solves it properly</h2>
<p>The version of this that scales is the one large apps already use: the marketing site and the product are two different things with two different jobs.</p>
<p>Your landing page, pricing page, and blog need SSR. They exist to be found. Rendering them on the server is the entire point, and you should leave that alone.</p>
<p>Your app does not need SSR at all. Nobody is crawling a screen behind a login. It needs to change screens instantly and hold state between them, which is what a client-rendered app is good at.</p>
<p>You can express that split inside one Lovable project with the route tree, which is what the <code>ssr: false</code> layout route above is doing. Or you can go further and deploy them separately, the site on its own domain and the app on <code>app.yourdomain.com</code>, which is cleaner, decouples your release cadence, and means a marketing change can never slow down the product. Either shape works. The route-level split is a prompt away and worth doing first.</p>
<h2>When to leave SSR on</h2>
<p>If the app itself contains public content that has to rank, keep SSR on those routes specifically. Public profiles, marketplace listings, event pages, anything with a shareable URL that a stranger might land on from search. Those pages benefit from server rendering for the same reason your landing page does.</p>
<p>Set <code>ssr: false</code> on the authenticated layout and leave the public branch of the route tree rendering on the server. Selective SSR exists precisely for this, and a per-route decision beats a global one on both sides.</p>
<h2>Turning the Lovable app into a mobile app</h2>
<p>If you are reading this because your app already runs on a phone, skip ahead. If you are still at the "I have a Lovable app and I want it in the App Store" stage, the short version:</p>
<p>Publish the Lovable app so it has a live URL. In Despia, create an app and point it at that URL. Install the <code>despia-native</code> package by asking Lovable to add it, which is how you get the device features. Connect your Apple and Google accounts, build, and submit. There is no Mac, no Xcode, and no CLI in that path.</p>
<p>What you end up with is one codebase. The Lovable project stays the thing you edit, it ships as a real signed binary on both stores, and web changes keep going out over the air without a new store submission. That is also why the SSR fix above is a five minute change rather than a release: you fix it in Lovable, the app picks it up on next launch.</p>
<h2>Then make it feel native</h2>
<p>Fixing navigation speed is the prerequisite, not the finish line. Once a screen change is instant, the things that read as native start landing, because there is no lag underneath them to fight.</p>
<pre><code class="language-javascript">import despia from 'despia-native'

const isDespia = navigator.userAgent.toLowerCase().includes('despia')

function go(path) {
  if (isDespia) despia('lighthaptic://')
  navigate({ to: path })
}
</code></pre>
<p>A light haptic on tap, correct safe area handling, native share and biometrics where they belong. None of that helps while every navigation costs a second, which is why the order matters: make it fast, then make it feel native.</p>
<p>That one is a prompt too:</p>
<pre><code class="language-text">Install the despia-native package. Detect whether the app is running
inside Despia by checking for "despia" in the lowercased user agent, and
only call despia() when it is. Fire a light haptic on tab bar taps and
primary button presses. Do not add haptics to scrolling or to every
element.
</code></pre>
<h2>FAQ</h2>
<p><strong>Do I need to rebuild the app natively?</strong> Not for this. A rewrite gives you a second codebase, a second release process, and store review on every change. What you are looking at is a rendering setting.</p>
<p><strong>How do I know which stack my project is on?</strong> Look for TanStack Start in your dependencies. New Lovable projects created after May 13, 2026 use it. Older ones are React plus Vite unless you ran the upgrade.</p>
<p><strong>Will turning off SSR hurt my SEO?</strong> Only on the routes you turn it off for, and those are the routes behind your login. Leave the public pages server rendered.</p>
<p><strong>Why does it still feel slow after the change?</strong> Either the old build is still cached, or your navigation is still doing document loads. Check the Network panel for <code>document</code> requests per screen change before assuming SSR was not the cause.</p>
<p><strong>Does this need a new App Store build?</strong> No. Web changes go out over the air. The binary picks up the current build on next launch.</p>
<h2>Get it on the stores</h2>
<p>Take the app you just made fast and ship it to iOS and Android without a CLI or a Mac. Code signing and submission run from the browser, and web changes keep going out over the air.</p>
<p><a href="https://setup.despia.com">See the setup docs at setup.despia.com</a></p>
]]></content:encoded></item><item><title><![CDATA[Convert a Base44 App to iOS and Publish It]]></title><description><![CDATA[Apple does not care that your app was built in Base44. It cares whether the binary does something Safari does not, whether the listing is honest, and whether a reviewer can get inside it. Almost every]]></description><link>https://blog.despia.com/convert-a-base44-app-to-ios-and-publish-it</link><guid isPermaLink="true">https://blog.despia.com/convert-a-base44-app-to-ios-and-publish-it</guid><category><![CDATA[base44]]></category><category><![CDATA[mobile app]]></category><category><![CDATA[appstore]]></category><category><![CDATA[iOS]]></category><category><![CDATA[ios app development]]></category><category><![CDATA[ios app developer]]></category><dc:creator><![CDATA[Despia CEO]]></dc:creator><pubDate>Fri, 07 Aug 2026 16:36:04 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/67f0239ff4f5a0b7b4a852c4/1e201083-fef7-4475-95e7-98a29a14ff1e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Apple does not care that your app was built in Base44. It cares whether the binary does something Safari does not, whether the listing is honest, and whether a reviewer can get inside it. Almost every rejection a Base44 builder gets is one of those three.</p>
<p><strong>Short answer.</strong> Base44 can generate store files for the App Store, but what it generates is a secure WebView wrapper that opens only your app's URL, with no native push, no full offline mode, no HealthKit and no built-in StoreKit yet. If you need those while keeping Base44 as the source of truth, converting means running the same Base44 frontend inside a native runtime that ships a signed binary with the capabilities added underneath. Either way you need an Apple Developer Program membership in your own name at $99 a year, a bundle ID with the right capabilities, a listing with device screenshots and accurate purpose strings, App Privacy answers that match reality, and a working demo account. Review is usually 24 to 48 hours.</p>
<p>This guide covers both halves: the conversion, then the submission.</p>
<h2>What Base44 gives you and what it does not</h2>
<p>Base44 does generate store files. From the Mobile app tab you can scan your app against Apple's guidelines and produce the files you submit from your own Apple developer account. So the question is not whether you can get a build. It is what is inside it.</p>
<p>Per <a href="https://docs.base44.com/documentation/building-your-app/uploading-to-app-stores">Base44's own documentation</a>, that build runs your published app inside a secure web view, a lightweight native wrapper that opens only your app's URL, and it does not currently support native-only features such as push notifications or full offline mode. HealthKit and built-in StoreKit are not there yet either. The upside their docs also name is real: publish a content or design change in Base44 and it appears in the app without a new store version.</p>
<p>If your app takes no payments, sends no notifications and touches no hardware, use that and stop reading. It is free and already in your dashboard.</p>
<p>Everything below is for the other case: a real signed binary with native capability, built from the same Base44 project through a native runtime, which is what Despia does.</p>
<p><strong>What converting actually means here.</strong> Nothing is regenerated and nothing is rewritten. Your Base44 project stays exactly where it is and keeps being the thing you edit. The runtime ships it as a signed iOS binary and hands the web layer the device, so push, biometrics and purchases become available from the code you already have. You end up with one codebase, not two, and web changes keep going out over the air with no resubmission.</p>
<p>The alternative, having an AI regenerate your frontend as a React Native project, is a real option and it is also a second codebase to maintain alongside Base44. That trade is covered in <a href="https://blog.despia.com/convert-base44-to-a-mobile-app-the-full-guide">the full conversion guide</a>.</p>
<h2>Enrollment, and why to start it today</h2>
<p>$99 a year, the same price as an individual or an organization.</p>
<p>Individual enrollment usually clears in 24 to 48 hours. Organization enrollment can take around two weeks, because Apple requires a D-U-N-S number and cross-checks it against your form and your company website, sometimes by phone. D-U-N-S registration itself is free and takes one to five business days.</p>
<p>It must be your account. Guideline 4.2.6 rejects apps built with a commercialized template or app generation service unless they are submitted directly by the provider of the app's content, which is a rule about whose developer account ships the app. Any service offering to publish on your behalf creates that problem for you.</p>
<h2>Identifiers, before you build</h2>
<p><strong>Bundle ID.</strong> Reverse-domain format, permanent once an app record exists. Enable the capabilities your app uses. Push Notifications is the one people forget, and registration fails at runtime without it even with a valid key.</p>
<p><strong>Team ID.</strong> Ten characters, top right of developer.apple.com/account.</p>
<p><strong>A second bundle ID for push.</strong> OneSignal's notification service extension needs <code>com.yourcompany.yourapp.OneSignalNotificationServiceExtension</code>, spelled exactly, with Push and Associated Domains enabled. The runtime provisions that target at build time.</p>
<p><strong>Three keys that look identical.</strong> All <code>.p8</code>, all download once:</p>
<table>
<thead>
<tr>
<th>Key</th>
<th>Created under</th>
<th>Goes to</th>
</tr>
</thead>
<tbody><tr>
<td>APNs auth key</td>
<td>Keys, with APNs enabled</td>
<td>OneSignal</td>
</tr>
<tr>
<td>App Store Connect API key</td>
<td>Users and Access, Integrations</td>
<td>Despia</td>
</tr>
<tr>
<td>In-App Purchase key</td>
<td>App Store Connect, In-App Purchase</td>
<td>RevenueCat</td>
</tr>
</tbody></table>
<p>Not interchangeable, and the in-app purchase page has its own Issuer ID that differs from the main one. Label them at download or you will be regenerating keys.</p>
<p>You do not generate certificates. The pipeline creates Apple Development and Apple Distribution for you. If you see "Developer ID" anywhere, that is a macOS certificate for distributing outside the Mac App Store and has nothing to do with iOS, which is the top confusion for anyone searching that phrase.</p>
<h2>Make the Base44 app reachable</h2>
<p>The runtime loads your published Base44 URL, so it has to resolve publicly. Subdomain or custom domain, either works.</p>
<p>A login wall is fine, because that is your app behaving normally. What is not fine is submitting without a demo account. Put real, working credentials in the App Information notes in App Store Connect. Reviewers who cannot get past your login screen reject the build, and that rejection has nothing to do with how it was built.</p>
<h2>What makes a Base44 app pass 4.2</h2>
<p>Guideline 4.2 asks that your app include features, content and UI that elevate it beyond a repackaged website. The subject of that sentence is your features, not your rendering engine. A binary that only loads your Base44 URL fails it correctly.</p>
<p>Push is the strongest single answer, because reaching a user between sessions is visibly not a website behaviour. Base44 has no terminal, so you add the SDK by asking the Builder AI to install <code>despia-native</code>, approving the prompt, and publishing.</p>
<pre><code class="language-javascript">import despia from 'despia-native'

const isDespia = navigator.userAgent.toLowerCase().includes('despia')

// the bridge has no guaranteed callback, so a read must never block the UI
const capped = (p, ms = 2000) =&gt;
  Promise.race([p, new Promise(r =&gt; setTimeout(() =&gt; r(null), ms))])

async function onAuthenticatedLoad() {
  const user = await base44.auth.me()
  if (!isDespia) return

  despia(`setonesignalplayerid://?user_id=${user.id}`)

  const push = await capped(despia('checkNativePushPermissions://', ['nativePushEnabled']))
  if (push &amp;&amp; !push.nativePushEnabled) despia('settingsapp://')
}
</code></pre>
<p>After push: Face ID on sensitive screens, StoreKit purchases, deep links that land on the right screen, camera and photo access used in a real flow. Pick the ones your product uses. Reviewers can tell a feature from a feature bolted on for review.</p>
<p>Two Base44-specific traps here. Never gate on <code>iPhone</code> alone, because iPadOS Safari reports a Macintosh user agent with no iPad token, and if your listing supports iPad then Apple reviews on one. And never leave an awaited bridge call uncapped: with no guaranteed callback it can hang for 15 to 30 seconds, and a reviewer cannot tell a hung bridge from a broken app.</p>
<p>Full setup: <a href="https://blog.despia.com/base44-push-notifications-with-onesignal-native">the Base44 push guide</a>.</p>
<h2>Selling anything digital</h2>
<p>Digital goods and subscriptions unlocked inside the app go through StoreKit. That is 3.1.1 and no architecture changes it. Physical goods and real-world services do not.</p>
<pre><code class="language-javascript">if (isDespia) {
  despia(`revenuecat://launchPaywall?external_id=${user.id}&amp;offering=default`)
} else {
  window.location.href = `https://pay.rev.cat/&lt;your_token&gt;/${encodeURIComponent(user.id)}`
}

window.onRevenueCatPurchase = checkEntitlements
</code></pre>
<p>Apple also requires a restore path, which is the same entitlement check you use to unlock the feature. The Base44 setup, including the version that needs no webhooks, is <a href="https://blog.despia.com/base44-in-app-purchases-without-webhooks-revenuecat">here</a>.</p>
<h2>Login, and the two rules attached to it</h2>
<p>Base44's built-in Google login is browser-based. On a phone that opens a web page inside your own app asking for a password, which works and looks wrong. Apple also expects an equivalent privacy-focused option next to any third-party login, meaning Sign in with Apple.</p>
<p>Two related review requirements catch Base44 builders at the same time. Apple rejects apps that put a signup wall in front of content that does not need an account. And any app with accounts must allow deletion inside the app, not by emailing support.</p>
<p>Native login means owning your auth, which is the hardest thing to build correctly on Base44, so start from <a href="https://github.com/despia-native/base44-native-oauth">the open source boilerplate</a>. It ships native Google Sign-In, Sign in with Apple, guest device accounts and in-app deletion, which is that rejection list closed. The <a href="https://blog.despia.com/convert-base44-to-mobile-app">full guide</a> covers it.</p>
<h2>The listing is part of the review</h2>
<p>Screenshots from a device at the required sizes, showing your app rather than your marketing site. iPad screenshots if your listing supports iPad.</p>
<p>The description should describe the app. "Our Base44 site, now as an app" is an argument for rejecting it.</p>
<p>Purpose strings are the quiet killer. Every permission has a string the user sees, and it must say what your app does with that access. A placeholder left from a template is a 5.1.1 rejection, issued before anyone evaluates your features. Write "Used to attach photos to your reports", not "This app requires camera access". <a href="https://blog.despia.com/base44-app-permissions-fix-the-5-1-1-rejection">The fix</a>.</p>
<p>App Privacy answers must cover everything your app and its SDKs collect, including analytics, attribution and push.</p>
<h2>Build, test, submit</h2>
<p>In the Despia editor you set name, bundle ID, icon, splash and addons, then build. Signing, provisioning and the App Store Connect upload run from the browser, so there is no Mac and no Xcode.</p>
<p>One rule causes more confusion than the rest: web changes ship over the air, native configuration does not. Saving your OneSignal App ID or RevenueCat keys does nothing until you rebuild, and the failure is silent. Calls resolve normally, backend sends return success from the API and never reach the device, entitlement checks come back empty. If something breaks right after a settings change, rebuild before debugging.</p>
<p>Use TestFlight even if you are alone. Installing your own build surfaces what a browser preview never will. Then test push on a physical device, test a purchase in sandbox, test on an iPad if your listing supports one, and open your Base44 app in a desktop browser to confirm every native call is gated.</p>
<p>Review is usually 24 to 48 hours, longer for a first submission.</p>
<h2>When you get rejected</h2>
<p>Read the guideline number, because it tells you which conversation you are in. Guessing costs a cycle.</p>
<p>Reply in Resolution Center rather than resubmitting silently. If the reviewer missed native features because they never created an account or never reached that screen, say where they are and how to get to them. That reply often works.</p>
<p>Fix the metadata in the same pass. A 4.2 rejection usually arrives alongside desktop screenshots and a website-shaped description, and fixing only the app leaves the impression intact.</p>
<p>The full pre-submission list is in <a href="https://blog.despia.com/base44-app-store-approval-the-rejection-checklist">the Base44 rejection checklist</a>.</p>
<h2>After you are live</h2>
<p>Base44 changes ship over the air with no resubmission. Edit, publish, users get it on next launch.</p>
<p>Native configuration still needs a build and a review. Apple also runs an annual SDK clock: since 28 April 2026 every upload must be built with Xcode 26 and the iOS 26 SDK, and that moves every year. If the runtime is maintained upstream, that is a rebuild you trigger rather than a project you maintain.</p>
<h2>Common questions</h2>
<p><strong>Can Base44 publish to the App Store on its own?</strong> Yes. It generates the files you submit from your own Apple developer account. What it does not give you, per Base44's own docs, is native push, full offline mode, HealthKit or built-in StoreKit, so the question is whether your app needs any of those.</p>
<p><strong>Do I need a Mac or Xcode?</strong> No. Builds, signing and submission run from the browser. You need your own Apple Developer account because the app ships under your name.</p>
<p><strong>Will Apple reject my Base44 app for using a WebView?</strong> Not for the WebView. Guideline 4.2 asks whether the app does enough beyond a repackaged website, which is a capability question.</p>
<p><strong>Does my Base44 backend change?</strong> No. Entities, auth and functions stay where they are and the iOS app calls them over HTTPS exactly as the web app does.</p>
<p><strong>How long does it take?</strong> Enrollment 24 to 48 hours as an individual. A build on your own phone the same afternoon. Review usually a day or two.</p>
<p><strong>Can I ship iOS only?</strong> Yes. Licensing is per app per OS, so iOS alone is a normal choice.</p>
<h2>Get it on the App Store</h2>
<p>Take the Base44 app you already built and ship it to iOS with push, biometrics and in-app purchases, from one codebase. Code signing and submission run from the browser.</p>
<p><a href="https://setup.despia.com">See the setup docs at setup.despia.com</a></p>
]]></content:encoded></item><item><title><![CDATA[Convert a Base44 App to Android and Publish It]]></title><description><![CDATA[Google Play is usually described as the easier store. For a Base44 builder shipping their first app it now takes longer than iOS, and not for technical reasons. A rule for new personal accounts adds a]]></description><link>https://blog.despia.com/convert-a-base44-app-to-android-and-publish-it</link><guid isPermaLink="true">https://blog.despia.com/convert-a-base44-app-to-android-and-publish-it</guid><category><![CDATA[base44]]></category><category><![CDATA[Mobile Development]]></category><category><![CDATA[mobile app development]]></category><category><![CDATA[Android]]></category><category><![CDATA[android app development]]></category><category><![CDATA[google play]]></category><dc:creator><![CDATA[Despia CEO]]></dc:creator><pubDate>Fri, 07 Aug 2026 16:33:55 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/67f0239ff4f5a0b7b4a852c4/83599564-3a11-4840-bb0d-6f1f60dacc00.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Google Play is usually described as the easier store. For a Base44 builder shipping their first app it now takes longer than iOS, and not for technical reasons. A rule for new personal accounts adds a fixed two-week wait that no amount of preparation removes, and almost nobody finds out until they are ready to launch.</p>
<p><strong>Short answer.</strong> Base44 can generate a Google Play ready AAB from the Mobile app tab, but what it generates is a secure WebView wrapper that opens only your app's URL, with no native push, no full offline mode, no HealthKit and no built-in Play Billing yet. If you need those while keeping Base44 as the source of truth, converting means running the same Base44 frontend inside a native runtime that ships a signed bundle with the capabilities added underneath. Either way a Play Console account is $25 once, you complete a store listing, content rating, target audience and Data safety declarations, and you run a closed test. If your account is a personal one created after 13 November 2023, you need 12 testers opted in for 14 consecutive days before you can even apply for production access.</p>
<p>This guide covers both halves: the conversion, then the submission. Start the tester clock first and finish the app while it runs.</p>
<h2>Start the tester clock before anything else</h2>
<p>This is the scheduling insight the whole guide turns on, so it goes first.</p>
<p>You need 12 testers opted in and running a closed test for 14 consecutive days. The count was 20 until December 2024. Internal testing does not count. The clock only starts once your closed testing release has cleared review and 12 people have actually opted in, so the day you upload is not day one.</p>
<p>Since spring 2026 Google also rejects production access for insufficient engagement even when the count is right, so testers who install and never open the app are not enough. Unresolved crashes and ANRs in the pre-launch report trigger rejections too.</p>
<p>Sources disagree on whether dropping below 12 resets the clock. The wording is continuous coverage and the published guidance conflicts, so recruit 15 to 25 and the question never arises. If you do not have that many willing testers, <a href="https://www.testerscommunity.com/">Testers Community</a> exists for this and runs 16 days rather than 14 for buffer.</p>
<p>So: get any working build of your Base44 app into closed testing, start the clock, and spend the two weeks adding the features below.</p>
<h2>Account setup</h2>
<p>$25 once, for the lifetime of the account.</p>
<p>The account type decides whether the tester rule applies to you. Personal accounts created after 13 November 2023 must complete closed testing. Organization accounts verified with a D-U-N-S number are exempt, so if this is a company, register the D-U-N-S and open an organization account rather than starting personal and migrating.</p>
<p>Every account completes identity verification with a name and address that Google displays on your store listing. Worth knowing before you sign up as an individual.</p>
<p>Your package name is permanent once an app exists under it, and it must match what you use in Firebase, because a mismatch there produces push failures that look like anything except a naming problem.</p>
<h2>What Base44 gives you and what it does not</h2>
<p>Base44 does generate store files. From the Mobile app tab you can produce a Google Play ready AAB, download it, and upload it to a Play Console release. So the question is not whether you can get a bundle. It is what is inside it.</p>
<p>Per <a href="https://docs.base44.com/documentation/building-your-app/uploading-to-app-stores">Base44's own documentation</a>, that bundle runs your published app inside a secure web view, a lightweight native wrapper that opens only your app's URL, and it does not currently support native-only features such as push notifications or full offline mode. HealthKit and built-in Play Billing are not there yet either. The upside their docs also name is real: publish a content or design change in Base44 and it appears in the app without a new store version.</p>
<p>If your app takes no payments, sends no notifications and touches no hardware, that is free and already in your dashboard. Everything below is for the other case: a signed app bundle with native capability, built from the same Base44 project through a native runtime.</p>
<p><strong>What converting actually means here.</strong> Nothing is regenerated and nothing is rewritten. Your Base44 project stays exactly where it is and keeps being the thing you edit. The runtime ships it as a signed Android bundle and hands the web layer the device, so push, fingerprint unlock and Play Billing become available from the code you already have. One codebase, not two, and web changes keep going out over the air with no new upload.</p>
<p>The alternative, having an AI regenerate your frontend as a React Native project, is a real option and it is also a second codebase to maintain alongside Base44. That trade is covered in <a href="https://blog.despia.com/convert-base44-to-a-mobile-app-the-full-guide">the full conversion guide</a>.</p>
<h2>Add the features worth testing</h2>
<p>Play's relevant policy is Spam and Minimum Functionality, plus a webviews rule that turns on ownership: you must own the site your app loads. That is a lower bar than Apple's, but the app still has to be worth installing, and shipping something worth testing is also what gets you real engagement during the 14 days.</p>
<p>Base44 has no terminal, so ask the Builder AI to install <code>despia-native</code>, approve the prompt, and publish.</p>
<pre><code class="language-javascript">import despia from 'despia-native'

const isDespia = navigator.userAgent.toLowerCase().includes('despia')

// the bridge has no guaranteed callback, so a read must never block the UI
const capped = (p, ms = 2000) =&gt;
  Promise.race([p, new Promise(r =&gt; setTimeout(() =&gt; r(null), ms))])

async function onAuthenticatedLoad() {
  const user = await base44.auth.me()
  if (!isDespia) return

  despia(`setonesignalplayerid://?user_id=${user.id}`)

  const push = await capped(despia('checkNativePushPermissions://', ['nativePushEnabled']))
  if (push &amp;&amp; !push.nativePushEnabled) despia('settingsapp://')
}
</code></pre>
<p>The Android half of push setup is Firebase: an Android app with the same package name, then the Server Key and Sender ID pasted into OneSignal. Full sequence in <a href="https://blog.despia.com/base44-push-notifications-with-onesignal-native">the Base44 push guide</a>.</p>
<p>Never leave an awaited bridge call uncapped. With no guaranteed callback it can hang for 15 to 30 seconds, which reads as a frozen app and shows up in your pre-launch report as an ANR.</p>
<h2>The bundle and signing</h2>
<p>Play takes an Android App Bundle, not an APK, for new apps.</p>
<p>Play App Signing holds the release key: you upload with an upload key, Google re-signs with the app signing key it holds, and that key never sits on your laptop waiting to be lost. Losing an upload key is recoverable. Losing a self-managed app signing key is not.</p>
<p>If your build runs in the browser, the bundle is produced and uploaded for you and there is no Android Studio in your path.</p>
<h2>The declarations, and the one people underestimate</h2>
<p>Play front-loads paperwork that Apple asks for later.</p>
<p><strong>Store listing.</strong> Title, short and full description, screenshots from a device at the required sizes, feature graphic, icon.</p>
<p><strong>Content rating.</strong> A questionnaire generating per-region ratings. Answer honestly, since a wrong answer found later can pull the listing.</p>
<p><strong>Target audience and content.</strong> Whether the app targets children. If it does, Families policy applies and it is extensive.</p>
<p><strong>Ads declaration.</strong> Including ads served by any SDK you added.</p>
<p><strong>Data safety.</strong> The underestimated one. You declare what you collect, what you share, whether it is encrypted in transit, and whether users can request deletion. It must cover everything your app and every SDK inside it collects, including your Base44 backend, analytics, attribution and push. Google compares the declaration to observed behaviour, and a mismatch is a policy violation rather than a form error.</p>
<p><strong>Privacy policy.</strong> A real, reachable URL that matches those answers.</p>
<h2>Selling anything digital</h2>
<p>Digital goods go through Google Play's billing system. On Base44 that is the same RevenueCat call your iOS build uses, with one entitlement across both catalogs:</p>
<pre><code class="language-javascript">if (isDespia) {
  despia(`revenuecat://launchPaywall?external_id=${user.id}&amp;offering=default`)
} else {
  window.location.href = `https://pay.rev.cat/&lt;your_token&gt;/${encodeURIComponent(user.id)}`
}
</code></pre>
<p>Three Android differences that cause real bugs. A purchase not acknowledged within three days is automatically refunded by Google and access revoked, silently. Subscriptions nest as product, then base plan, then offer, so your catalog is not a copy of the App Store one. And testing requires an upload to a track signed by Play plus license testers, because a browser preview or an unsigned build never resolves products, which is the usual reason behind "my products are not showing up".</p>
<p>The Base44 setup, including the version that needs no webhooks, is <a href="https://blog.despia.com/base44-in-app-purchases-without-webhooks-revenuecat">here</a>.</p>
<p>Commission rates are in flux following the Epic litigation and Google's external-link programs, so check current terms rather than a number from a blog post.</p>
<h2>Two deadlines that block uploads</h2>
<p>Both are enforced at upload, not at runtime, so nothing breaks until the day you try to ship.</p>
<p>From 31 August 2026, new apps and updates must target Android 16, API level 36, with an extension available to 1 November. Apps below the threshold stop being discoverable to new users on newer devices, and this repeats annually.</p>
<p>From the same date, anything selling digital goods must use Play Billing Library 8 or later, same extension. PBL 9 shipped in May 2026.</p>
<p>If you own the native project, both are your job every year. If the runtime is maintained upstream, both are a rebuild you trigger from the browser.</p>
<h2>Native Google Sign-In</h2>
<p>Base44's built-in Google login is browser-based. It works, but on a phone users expect the system sheet, and a web sign-in page inside your own app is where your product stops feeling like an app.</p>
<p>Native sign-in means owning your auth: your own <code>Account</code> entity, your own JWTs signed in a Base44 backend function, and the system browser handling consent with a single-use code coming back over your deep link. Start from <a href="https://github.com/despia-native/base44-native-oauth">the open source boilerplate</a>, which ships it along with guest device accounts, in-app account deletion and deny-all row-level security. The <a href="https://blog.despia.com/convert-base44-to-mobile-app">full guide</a> covers it.</p>
<h2>Timeline and rollout</h2>
<p>Signup and identity verification: a day or two, longer for organization verification. First closed testing release through review: hours to a few days. Closed testing: 14 consecutive days minimum, realistically 16 to 20 with the opt-in ramp. Production access questionnaire: about ten questions, reviewed in days.</p>
<p>Three to four weeks end to end for a new personal account, and the app is the small part.</p>
<p>Use staged rollout when you get there. A crash that only appears on one manufacturer's Android build reaches 5% of your users instead of all of them, and you can halt a Play rollout, which you cannot do on the App Store.</p>
<p>That matters more on Android than people expect: Samsung, Huawei, Xiaomi and OnePlus apply aggressive background restrictions by default, which is why a hand-rolled background feature works on a Pixel and nowhere else. Despia handles delivery across those manufacturers with no extra configuration, but test on a Samsung before widening a rollout regardless.</p>
<h2>Common questions</h2>
<p><strong>Can Base44 publish to Google Play on its own?</strong> Yes. The Mobile app tab generates a Google Play ready AAB. What it does not give you, per Base44's own docs, is native push, full offline mode, HealthKit or built-in Play Billing, so the question is whether your app needs any of those.</p>
<p><strong>Do I really need 12 testers?</strong> Only for personal accounts created after 13 November 2023. D-U-N-S-verified organization accounts are exempt, and internal testing does not count.</p>
<p><strong>Do I need Android Studio?</strong> No. The bundle is built, signed and uploaded from the browser. You need your own Play Console account because the app ships under your name.</p>
<p><strong>Will Google reject a Base44 app for being web-based?</strong> Not for that. The webview rule turns on whether you own the site being loaded, and minimum functionality asks whether the app is worth installing.</p>
<p><strong>Does my Base44 backend change?</strong> No. Entities, auth and functions stay where they are and the Android app calls them over HTTPS exactly as the web app does.</p>
<p><strong>Can I ship Android only?</strong> Yes. Licensing is per app per OS. Most people add iOS because the same Base44 codebase already serves it.</p>
<h2>Get it on Google Play</h2>
<p>Take the Base44 app you already built and ship it to Android with push, purchases and device access, from one codebase. Signing and submission run from the browser.</p>
<p><a href="https://setup.despia.com">See the setup docs at setup.despia.com</a></p>
]]></content:encoded></item><item><title><![CDATA[Convert Base44 to a Mobile App: The Full Guide]]></title><description><![CDATA[Base44 will generate store files for you. What it generates is a secure WebView wrapper that opens only your app's URL, which is enough for some apps and nowhere near enough for others. The real decis]]></description><link>https://blog.despia.com/convert-base44-to-a-mobile-app-the-full-guide</link><guid isPermaLink="true">https://blog.despia.com/convert-base44-to-a-mobile-app-the-full-guide</guid><category><![CDATA[base44]]></category><category><![CDATA[#ai-tools]]></category><category><![CDATA[ai agents]]></category><category><![CDATA[AI Development Services]]></category><category><![CDATA[iOS]]></category><category><![CDATA[ios app development]]></category><category><![CDATA[ios app developer]]></category><category><![CDATA[Android]]></category><category><![CDATA[android app development]]></category><category><![CDATA[android apps]]></category><dc:creator><![CDATA[Despia CEO]]></dc:creator><pubDate>Fri, 07 Aug 2026 13:06:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/67f0239ff4f5a0b7b4a852c4/0cc45e37-d6a8-4c0e-a9fb-0bb2d6b3fc1e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Base44 will generate store files for you. What it generates is a secure WebView wrapper that opens only your app's URL, which is enough for some apps and nowhere near enough for others. The real decision is what you do when it is not enough.</p>
<p><strong>Short answer.</strong> Base44's built-in mobile app is a WebView wrapper with no native push, no full offline mode, no HealthKit and no built-in StoreKit or Play Billing yet. If your app needs none of those, use it. If it does, you have two options: regenerate your frontend as a React Native project, which leaves you maintaining two codebases, or keep Base44 as the source of truth and run that frontend inside a native runtime, which ships a signed binary with those capabilities added underneath and pushes web changes over the air with no resubmission.</p>
<h2>What Base44 actually gives you</h2>
<p>A Vite React frontend and a hosted backend: entities, auth, Deno functions, file storage, realtime, reached through <code>@base44/sdk</code>.</p>
<p>Two things worth knowing before you build on top of it. The code export is frontend only, so entities and backend logic stay on Base44's servers and the exported <code>.env</code> still points back at them. And the CLI (<code>npx base44</code>) has an <code>eject</code> command and no mobile build target.</p>
<p>Store files are a different matter, and this is where most posts on this topic are out of date. Base44's Mobile app tab scans your app against Apple and Google guidelines and generates what you submit, including a Google Play ready AAB. Per <a href="https://docs.base44.com/documentation/building-your-app/uploading-to-app-stores">their documentation</a>, that build runs your published app inside a secure web view that opens only your app's URL, and it does not currently support native-only features such as push notifications or full offline mode. HealthKit and built-in billing are not there yet either. In exchange, content and design changes you publish in Base44 appear in the app without a new store version.</p>
<p>So if your app takes no payments, sends no notifications and touches no hardware, use it. It is already in your dashboard and it is the right tool for that app.</p>
<h2>The three routes</h2>
<p><strong>Base44's built-in mobile app.</strong> Generates the store files, including a Play ready AAB, and updates content over the air. A WebView wrapper, so no native push, no full offline, no HealthKit, no built-in billing. Right choice for a simple app.</p>
<p><strong>Rebuild in React Native.</strong> An agent regenerates your frontend as native views. Real native rendering, and a second codebase: your Base44 app still exists, you are still prompting it, and the generated project never hears about it. Right call if rendering is your product, or if you have a developer who wants to own a native codebase.</p>
<p><strong>Run the app you have in a native runtime.</strong> Base44 stays the source of truth. A native engine underneath holds the SDKs, so web code calls APNs and FCM push, StoreKit and Play Billing, Face ID, background GPS and the rest. One codebase, a signed binary on both stores, web changes over the air. That is Despia, and the rest of this walks it end to end.</p>
<h2>Step 1: Publish the Base44 app</h2>
<p>The runtime loads your published URL, so it has to be publicly reachable. Base44 subdomain or custom domain, either is fine.</p>
<p>A login wall is fine too, since that is your app behaving normally. What matters is that the URL resolves, and that Apple gets a working demo account at submission. Reviewers who cannot get past your login screen reject the build.</p>
<h2>Step 2: Install the SDK through the Builder chat</h2>
<p>Base44 has no terminal. Ask the Builder AI to install <code>despia-native</code>, approve the prompt, then publish so the change takes effect. Packages persist once installed, and an unused one calls nothing by itself, so there is nothing to undo if you change direction.</p>
<h2>Step 3: Detect the runtime</h2>
<p>Everything native goes behind one check.</p>
<pre><code class="language-javascript">const ua = navigator.userAgent.toLowerCase()

const isDespia = ua.includes('despia')
const isDespiaIOS = isDespia &amp;&amp; (ua.includes('iphone') || ua.includes('ipad'))
const isDespiaAndroid = isDespia &amp;&amp; ua.includes('android')
</code></pre>
<p>Put it in one shared helper. Do not check for <code>iPhone</code> alone, do not use <code>innerWidth</code>, do not invent a <code>window.despia</code> flag. iPadOS Safari reports a Macintosh user agent with no iPad token, and an iPhone-only check is the most common reason a Base44 app works on a phone and shows a broken paywall on an iPad.</p>
<h2>Step 4: Add the features that make it an app</h2>
<p>This decides whether you get approved and whether anyone keeps the app.</p>
<p>If you do not write code, you do not have to type any of this. Paste it into the Builder chat, or connect the Despia MCP server at <code>https://setup.despia.com/mcp</code> and describe what you want. The code is here so you can check what you got back, which is a different skill from writing it and a more useful one.</p>
<p>Native calls go inside the gate. Callbacks the runtime invokes are assigned outside it, at the top level, or they vanish when a component unmounts.</p>
<pre><code class="language-javascript">import despia from 'despia-native'

const isDespia = navigator.userAgent.toLowerCase().includes('despia')

// the bridge has no guaranteed callback, so a read must never block the UI
const capped = (p, ms = 2000) =&gt;
  Promise.race([p, new Promise(r =&gt; setTimeout(() =&gt; r(null), ms))])

async function onAuthenticatedLoad() {
  const user = await base44.auth.me()
  if (!isDespia) return

  despia(`setonesignalplayerid://?user_id=${user.id}`)   // link device to user

  const push = await capped(despia('checkNativePushPermissions://', ['nativePushEnabled']))
  if (push &amp;&amp; !push.nativePushEnabled) despia('settingsapp://')
}
</code></pre>
<p>Digital goods have to go through the stores. The paywall is one call, with a web fallback for desktop:</p>
<pre><code class="language-javascript">if (isDespia) {
  despia(`revenuecat://launchPaywall?external_id=${user.id}&amp;offering=default`)
} else {
  window.location.href = `https://pay.rev.cat/&lt;your_token&gt;/${encodeURIComponent(user.id)}`
}

window.onRevenueCatPurchase = checkEntitlements

async function checkEntitlements() {
  const data = await capped(despia('getpurchasehistory://', ['restoredData']))
  const active = (data?.restoredData ?? []).filter(p =&gt; p.isActive)
  if (active.some(p =&gt; p.entitlementId === 'premium')) unlockPremium()
}
</code></pre>
<p>Full setup for each: <a href="https://blog.despia.com/base44-in-app-purchases-without-webhooks-revenuecat">the RevenueCat guide</a> and <a href="https://blog.despia.com/base44-push-notifications-with-onesignal-native">the Base44 push guide</a>.</p>
<p>Deep links need no client code. Send a <code>path</code> in the notification's <code>data</code> object and the runtime updates the URL and fires <code>popstate</code>, so your router navigates on its own. Add <code>window.onNotificationEvent</code> only if you also send <code>metadata</code> to restore state, and do not navigate from it as well or you navigate twice.</p>
<h2>The rule to apply to every awaited call</h2>
<p>Notice the two second cap above, because it is the one production rule to learn before anything else.</p>
<p>The bridge has no guaranteed callback. If the native side does not answer, because the feature is off, the binary is older than the code, or you are in a web preview, an awaited call can sit for 15 to 30 seconds. On screen that is a frozen app, and a reviewer who hits it files a rejection rather than a bug report.</p>
<p>Cap every awaited read and let the work finish in the background. Fire-and-forget calls like haptics, the paywall launch and <code>setonesignalplayerid://</code> need none of this.</p>
<h2>If you need native Google or Apple sign-in</h2>
<p>Base44's built-in Google login is browser-based. Ship it to a phone and tapping Sign in with Google opens a web page inside your own app asking for a password. It works, it looks wrong, and it is the moment your product stops feeling like an app.</p>
<p>Only the first of three problems here is cosmetic. Users expect the system sign-in sheet. Apple expects an equivalent privacy-focused option next to any third-party login, which means Sign in with Apple. And web storage clears on delete, so a reinstall signs everyone out. Two related review requirements catch people at the same time: no signup wall in front of content that does not need an account, and account deletion inside the app rather than by support email.</p>
<p><strong>This is the one part you should not hand to an agent.</strong> Native sign-in means the system browser handles consent, the provider redirects to a page you host, and a single-use code comes back over your deep link. Your backend exchanges that code server-side with a secret that must never reach the device, checks the returned <code>id_token</code> audience against your own client id, mints your session token, and re-verifies it on every request against the live user record rather than trusting the token. Then most of it again for Apple.</p>
<p>Every one of those steps has a version that appears to work and is quietly insecure: an unchecked token audience, a secret that reaches the client, a role read from the token instead of the record. The broken version and the correct version both log you in, so you find out later.</p>
<p>Which is why we wrote it and open sourced it:</p>
<pre><code class="language-bash">git clone https://github.com/despia-native/base44-native-oauth
</code></pre>
<p>It is a store-review-ready starter, not an auth snippet. Native Google Sign-In and Sign in with Apple, guest device accounts so nobody is forced to register before seeing the product, in-app account deletion, deny-all row-level security on every entity, push and RevenueCat wired in. That is not a feature list picked for a README. It is the rejection list, closed. The session lives in the Storage Vault, so it survives a reinstall.</p>
<p>Three things to change and nothing else is hardcoded: your deep-link scheme in <code>src/config/app-config.js</code>, your secrets in the Base44 dashboard under Settings, Environment Variables (<code>JWT_SECRET</code> long, random and stable, since changing it signs everyone out, plus your Google client id and secret, <code>RESEND_API_KEY</code> and <code>RESEND_FROM</code>, and <code>APP_BASE_URL</code>), and the Google redirect URI, which is <code>APP_BASE_URL</code> plus <code>/native-callback.html</code>.</p>
<p>The clone is the one terminal moment in this whole guide. Hand the repository URL to your AI agent, or borrow a developer for an hour. After that you are back in dashboards.</p>
<p>One gap to close yourself: the auth endpoints ship without app-level rate limiting, so add per-IP or per-email throttling before you open signups publicly. And if you are only ever shipping on the web, skip all of this. Base44's built-in login is simpler and perfectly fine.</p>
<h2>Step 5: Get the store accounts</h2>
<p>Apple Developer Program, \(99 a year. Individual approval is usually 24 to 48 hours, organization enrollment around two weeks because of the D-U-N-S check, so start it early. Google Play Console, \)25 once.</p>
<p>Both in your own name. Apple's guideline 4.2.6 requires apps built with an app generation service to be submitted by the provider of the content, which means your account, not a vendor's.</p>
<h2>Step 6: Configure and build</h2>
<p>Set the app name, bundle ID, icon, splash and addons in the Despia editor, then build. Signing, provisioning and the store upload run from the browser, so there is no Mac, no Xcode and no CLI.</p>
<p>One rule causes more confusion than anything else: web changes ship over the air, native configuration does not. Saving your OneSignal App ID or RevenueCat keys does nothing until you rebuild, and the failure is silent. <code>setonesignalplayerid://</code> resolves normally, backend sends return success from the API and never reach the device, entitlement checks come back empty. If either breaks right after a settings change, rebuild before debugging anything.</p>
<h2>Step 7: Test on a real device</h2>
<p>A screen rendering proves nothing about whether a native call reached the operating system. Test push on a physical device, test a purchase with a sandbox account, test on an iPad if your listing supports iPad, and open your web app in a desktop browser to confirm every native call is gated and nothing throws outside the runtime.</p>
<h2>Step 8: Submit</h2>
<p>iOS goes through App Store Connect: metadata, screenshots from a phone rather than a desktop browser, purpose strings describing your actual use, App Privacy answers, and a demo account. Review is typically 24 to 48 hours.</p>
<p>Android has an extra gate. New personal Play Console accounts need 12 testers opted in for 14 consecutive days on a closed track before you can request production access, and since 2026 Google also rejects requests for insufficient engagement even when the count is right. Start that clock early.</p>
<h2>The things that go wrong</h2>
<p><strong>Placeholder purpose strings.</strong> <code>NSCameraUsageDescription</code> and friends still saying what the template shipped with. A 5.1.1 rejection, handed out before anyone evaluates your app. <a href="https://blog.despia.com/base44-app-permissions-fix-the-5-1-1-rejection">The fix</a>.</p>
<p><strong>Testing yesterday's build.</strong> Enable an addon, test the binary you already have, see nothing, conclude it is broken. Entitlements compile in. Rebuild first.</p>
<p><strong>Hallucinated APIs.</strong> Your agent writes a real method from RevenueCat's native SDK into a scheme-based runtime. The name is real and the shape is idiomatic, so review passes it, and the first evidence is a button that does nothing on a device. Ask for the documentation URL for that exact symbol and open it.</p>
<p><strong>A frozen screen on an awaited call.</strong> The one that becomes a rejection rather than a bug report, because a reviewer cannot tell a hung bridge from a broken app.</p>
<p><strong>Shipping thin.</strong> A binary that only loads your URL fails guideline 4.2 on the merits. <a href="https://blog.despia.com/base44-app-store-approval-the-rejection-checklist">The checklist</a>.</p>
<h2>How the routes compare</h2>
<table>
<thead>
<tr>
<th></th>
<th>Base44 built-in</th>
<th>React Native rebuild</th>
<th>Native runtime</th>
</tr>
</thead>
<tbody><tr>
<td>Codebases</td>
<td>One</td>
<td>Two</td>
<td>One</td>
</tr>
<tr>
<td>Push notifications</td>
<td>No</td>
<td>Yes</td>
<td>Yes, APNs and FCM</td>
</tr>
<tr>
<td>In-app purchases</td>
<td>No</td>
<td>Yes</td>
<td>Yes, StoreKit and Play Billing</td>
</tr>
<tr>
<td>Device access</td>
<td>No</td>
<td>Full</td>
<td>50+ native features</td>
</tr>
<tr>
<td>Web updates</td>
<td>Over the air</td>
<td>Rebuild, or JS-only OTA</td>
<td>Over the air, no review</td>
</tr>
<tr>
<td>Annual OS requirements</td>
<td>Their problem</td>
<td>Yours</td>
<td>Handled upstream</td>
</tr>
<tr>
<td>Needs a developer</td>
<td>No</td>
<td>Eventually</td>
<td>No</td>
</tr>
<tr>
<td>Cost</td>
<td>Free</td>
<td>Subscription plus maintenance</td>
<td>$249 per app per OS, one time</td>
</tr>
</tbody></table>
<p>Where a native runtime is not the answer: a game loop, real-time 3D, AR, per-frame camera processing, Live Activities, or a watch companion. And if deep offline is core to your product rather than a nice-to-have, weigh that honestly before you start.</p>
<h2>Common questions</h2>
<p><strong>Does Base44 build native mobile apps?</strong> It generates store files, including a Play ready AAB, but what runs inside them is a secure web view of your published app. Its own docs list the gaps: no native push, no full offline mode. HealthKit and built-in billing are not there yet either.</p>
<p><strong>Can I convert a Base44 app to iOS only?</strong> Yes. Licensing is per app per OS. Most people ship both, because the same web codebase already serves both.</p>
<p><strong>Will Apple reject my app for using a WebView?</strong> Not for the WebView. Guideline 4.2 asks whether your app does enough beyond a repackaged website. Push, purchases, biometrics and deep links answer that.</p>
<p><strong>Do I keep my Base44 backend?</strong> Yes, unchanged. Your native app calls entities, auth and functions over HTTPS exactly as the web app does.</p>
<p><strong>How long does it take?</strong> A build on your own phone is an afternoon. Apple review is a day or two. Google's 14-day testing window is the long pole, so recruit testers first, not last.</p>
<p><strong>Do I need a Mac or Xcode?</strong> No. You need your own Apple and Google accounts, because the app ships under your name.</p>
<p><strong>Does Base44's Google login work in the native app?</strong> As a web sign-in flow, not the system sheet, and with no Sign in with Apple. Native login means owning your auth, which is what the boilerplate above is for.</p>
<p><strong>What if I need something Despia does not support?</strong> First ask whether you need it. Most requests have a web-first version that is good enough, or turn out to be a feature the product does not need. That is the cheapest answer and it is right more often than people expect.</p>
<p>If you do need it, two paths, neither instant. Request it at <a href="mailto:features@despia.com">features@despia.com</a>, where it goes on the roadmap and a human builds it, with no guaranteed date. Or export the project and write the Swift or Kotlin yourself, which is immediate, but is code, and is not the easy option.</p>
<h2>Get it on the stores</h2>
<p>Take the Base44 app you already built and ship it to iOS and Android without a CLI or a Mac. Code signing and submission run from the browser.</p>
<p><a href="https://setup.despia.com">See the setup docs at setup.despia.com</a> or <a href="https://despia.com">start building at despia.com</a>.</p>
]]></content:encoded></item><item><title><![CDATA[App Works on iPhone but Not iPad? Here Is the Fix]]></title><description><![CDATA[Apple reviews on iPad. That is where most builders find out that half their app is gated behind a device name. The paywall opens on iPhone, the reviewer taps the same button on an iPad, and gets a scr]]></description><link>https://blog.despia.com/app-works-on-iphone-but-not-ipad-here-is-the-fix</link><guid isPermaLink="true">https://blog.despia.com/app-works-on-iphone-but-not-ipad-here-is-the-fix</guid><category><![CDATA[iOS]]></category><category><![CDATA[ios app development]]></category><category><![CDATA[iosdevx]]></category><category><![CDATA[iOS development]]></category><dc:creator><![CDATA[Despia CEO]]></dc:creator><pubDate>Fri, 07 Aug 2026 10:14:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/67f0239ff4f5a0b7b4a852c4/7126b4f5-1646-4206-806b-88c5ac4d3257.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Apple reviews on iPad. That is where most builders find out that half their app is gated behind a device name. The paywall opens on iPhone, the reviewer taps the same button on an iPad, and gets a screen telling them to open the app on their phone. Same binary, same build, same account. The difference is one substring check written months earlier by someone (or something) that assumed "iPhone" meant "phone".</p>
<p>This post is about that class of bug: platform checks that pass on the device you tested and fail on the device Apple tests.</p>
<h2>The line that does it</h2>
<p>Somewhere in the codebase there is a check like this:</p>
<pre><code class="language-javascript">const isNativeApp = navigator.userAgent.includes('iPhone')
</code></pre>
<p>On an iPhone it returns true and the native purchase flow runs. On an iPad it returns false, so the app decides it is running in a desktop web browser and renders the fallback: a message asking the user to open the mobile app, or an "Open in App" button that goes nowhere because they are already in the app.</p>
<p>Nothing is wrong with the build. Nothing is wrong with the store configuration. The app told itself it was not the app.</p>
<h2>Why the check exists in the first place</h2>
<p>The pattern is legitimate. A web app that ships to the App Store and to the web needs two payment paths, because Apple requires its own billing for digital goods inside the app while the browser version can charge through a web checkout. So the code branches: native purchase inside the app, web checkout outside it.</p>
<p>The branch is correct. The condition is wrong. The question the code needs to answer is "am I running inside the native runtime", and instead it asks "is this device an iPhone". Those were the same question for exactly one device family, and iPad is the counterexample sitting on the reviewer's desk.</p>
<p>This is also why AI-generated code produces it so reliably. Almost every user agent snippet in public training data is browser sniffing: mobile versus desktop, phone versus tablet layout, <code>/iPhone|Android/i</code> regexes from a decade of responsive design work. Ask an agent to detect a mobile app and it reaches for the pattern it has seen ten thousand times. The output is idiomatic, readable, and answers a different question than the one you asked.</p>
<h2>The correct check</h2>
<p>The Despia runtime injects <code>despia</code> into the user agent string alongside the platform identifier. So the environment check is the one substring that is true on every device the app ships to:</p>
<pre><code class="language-javascript">const isDespia = navigator.userAgent.toLowerCase().includes('despia')
</code></pre>
<p>iPhone, iPad, Android, all true. Safari, Chrome, desktop, all false. When you need the platform as well, ask for it separately and include both iOS device names:</p>
<pre><code class="language-javascript">const ua = navigator.userAgent.toLowerCase()
const isDespia = ua.includes('despia')
const isIOS = isDespia &amp;&amp; (ua.includes('iphone') || ua.includes('ipad'))
const isAndroid = isDespia &amp;&amp; ua.includes('android')
</code></pre>
<p>Under server-side rendering, <code>navigator</code> does not exist during the render pass, so guard it or read it inside an effect:</p>
<pre><code class="language-javascript">const isDespia = typeof navigator !== 'undefined' &amp;&amp;
    navigator.userAgent.toLowerCase().includes('despia')
</code></pre>
<p>Full reference: <a href="https://setup.despia.com/native-features/user-agent">User Agent</a>.</p>
<h2>The paywall, written correctly</h2>
<p>Here is the same branch with the condition fixed, using the current RevenueCat scheme:</p>
<pre><code class="language-javascript">import despia from 'despia-native'

const isDespia = navigator.userAgent.toLowerCase().includes('despia')

function openPaywall(userId) {
    if (isDespia) {
        despia(`revenuecat://launchPaywall?external_id=${userId}&amp;offering=default`)
        return
    }

    window.location.href = `https://pay.rev.cat/&lt;token&gt;/${userId}`
}

// assigned outside the gate, fires when the native sheet completes
window.onRevenueCatPurchase = () =&gt; {
    refreshEntitlements()
}
</code></pre>
<p>Two details worth keeping. The <code>despia()</code> call sits inside the gate, because it only means something inside the runtime. The global callback is assigned outside it, because assigning a property on <code>window</code> in a browser costs nothing and keeping it unconditional avoids a class of ordering bug. The full scheme list, including direct purchase and the Customer Center, is in the <a href="https://setup.despia.com/native-features/revenuecat/reference">RevenueCat reference</a>.</p>
<h2>The rest of the family</h2>
<p>Once you find one of these, grep for the others. They fail the same way and for the same reason.</p>
<p><strong>Width checks standing in for platform.</strong> <code>window.innerWidth &lt; 768</code> returns false on an iPad in full screen, so anything gated behind it disappears. It also returns true on an iPad in Split View, so a phone-only layout shows up on a tablet. Screen width describes a viewport, not a runtime.</p>
<p><strong>Touch detection standing in for mobile.</strong> <code>'ontouchstart' in window</code> is true on touchscreen laptops and false in some remote testing setups. It tells you about the input device and nothing about where your code is running.</p>
<p><code>/Mobi/</code> <strong>regexes.</strong> Common, and iPad Safari does not include <code>Mobi</code> in desktop-class mode.</p>
<p><strong>An</strong> <code>isMobile</code> <strong>flag that ends up meaning three different things.</strong> Layout in one component, native feature gating in another, analytics in a third. Once one boolean carries all three meanings, fixing the paywall breaks the tablet layout. Split them.</p>
<p><strong>iPad Safari reporting itself as a Mac.</strong> Since iPadOS 13, Safari on iPad requests desktop sites by default and its user agent can read as Macintosh with no <code>iPad</code> token in it at all. This is the deeper reason not to key native behaviour off device names: the device names are not stable. <code>despia</code> is, because the runtime puts it there.</p>
<h2>How to find every instance in ten minutes</h2>
<ol>
<li><p>Search the codebase, case-insensitively, for <code>iPhone</code>, <code>Android</code>, <code>iPad</code>, <code>Mobi</code>, <code>isMobile</code>, <code>isNative</code>, <code>innerWidth</code>, and <code>ontouchstart</code>.</p>
</li>
<li><p>For each hit, ask what the code is actually deciding. Layout decisions can keep using width. Anything that decides whether to call a native feature, show a purchase button, or render a "download the app" prompt must use the <code>despia</code> check.</p>
</li>
<li><p>Replace those conditions with the shared <code>isDespia</code> value. Export it from one module so there is one definition, not eight.</p>
</li>
<li><p>Rebuild and open the app on an iPad, or on the iPad simulator. Walk the purchase path, the login path, and anything that shows a "get the app" prompt. Those three cover most rejections.</p>
</li>
</ol>
<p>Step four is the one people skip. If your listing supports iPad, Apple will test on iPad, and a purchase path that dead-ends there is a rejection under <a href="https://developer.apple.com/app-store/review/guidelines/#minimum-functionality">minimum functionality</a>. Shipping iPhone-only is the alternative, but it is a device family setting on the binary and it costs you the tablet audience permanently. Fixing the substring is cheaper.</p>
<h2>Common questions</h2>
<p><strong>Does the iPad need a separate build?</strong> No. Despia ships a universal binary. The same JavaScript runs on both, which is why a device-name check is the only thing that can make them behave differently.</p>
<p><strong>Why did this pass on my iPhone during testing?</strong> Because your check happened to be true there. Testing on the device the condition was written for confirms the condition, not the behaviour.</p>
<p><strong>Apple only rejected the paywall. Is anything else affected?</strong> Probably. Anything gated behind the same flag is also off on iPad: push prompts, biometrics, camera access, deep links. The reviewer only reported the first dead end they hit.</p>
<p><strong>Can I check</strong> <code>window.despia</code> <strong>instead?</strong> No. The supported detection is the user agent substring. Anything else is an invented API and will break.</p>
<p><strong>My AI assistant wrote the original check. How do I stop it doing this again?</strong> Give it the docs page for the exact API before it writes, and ask it what token it is matching on and why. If the answer is a device name, the answer is wrong.</p>
<h2>Add this to your app</h2>
<p>Every native capability in this post is one JavaScript call away, inside your existing web codebase. No Xcode, no native project to maintain.</p>
<p><a href="https://setup.despia.com">Read the full reference at setup.despia.com</a></p>
]]></content:encoded></item><item><title><![CDATA[Lovable Mobile App Cost: The Real 10-Year Math]]></title><description><![CDATA[Lovable gives you two-way GitHub sync, so you already hold the repo. That makes the mobile step feel close to free: clone it, run npx cap add ios, done. The repo is the cheap part. The expensive part ]]></description><link>https://blog.despia.com/lovable-mobile-app-cost-the-real-10-year-math</link><guid isPermaLink="true">https://blog.despia.com/lovable-mobile-app-cost-the-real-10-year-math</guid><category><![CDATA[capacitor]]></category><category><![CDATA[lovable]]></category><category><![CDATA[webapp]]></category><category><![CDATA[PWA]]></category><category><![CDATA[webview]]></category><category><![CDATA[iOS]]></category><category><![CDATA[ios app development]]></category><category><![CDATA[Android]]></category><category><![CDATA[android app development]]></category><dc:creator><![CDATA[Despia CEO]]></dc:creator><pubDate>Thu, 06 Aug 2026 07:28:16 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/67f0239ff4f5a0b7b4a852c4/6561088f-6641-46f3-86de-0607a506346a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Lovable gives you two-way GitHub sync, so you already hold the repo. That makes the mobile step feel close to free: clone it, run <code>npx cap add ios</code>, done. The repo is the cheap part. The expensive part is the ten years afterwards, when someone has to keep a native project compiling against Apple's and Google's moving targets.</p>
<p>Here is the full arithmetic, with every number sourced and every assumption stated.</p>
<h2>The scenario</h2>
<p>Fixing the scenario matters more than the pricing pages, because both routes get cheap or expensive depending on who you are.</p>
<ul>
<li><p>You built the app in Lovable. It works, it is deployed, and the repo is synced to GitHub.</p>
</li>
<li><p>You ship it to both the App Store and Google Play.</p>
</li>
<li><p>It has 1,000 monthly active users.</p>
</li>
<li><p>You keep prompting Lovable to change the web app, maybe six meaningful releases a year.</p>
</li>
<li><p>You are not an iOS or Android developer, and you do not want to become one.</p>
</li>
</ul>
<p>That last line is the one that moves the total by thousands of dollars, and it is the line most cost comparisons leave out.</p>
<h2>There is no free built-in route</h2>
<p>Worth stating plainly, because Lovable users often assume there is one. Lovable has no built-in App Store or Google Play publishing. Your free fallback is a PWA, which means no store listing, no push on iOS unless the user manually adds it to their Home Screen, and no App Store presence at all.</p>
<p>If a store listing is the point, you are choosing between two paid routes, and the cost question becomes real.</p>
<p>One reassurance first, because it is a common fear: your Supabase backend does not need rebuilding for either route. It is an HTTPS API, and it behaves identically from a native client.</p>
<h2>Costs both routes pay</h2>
<p>These are identical either way, so they are not the argument. Listing them anyway, because a total that hides them is not a total.</p>
<table>
<thead>
<tr>
<th>Line item</th>
<th>Cost</th>
</tr>
</thead>
<tbody><tr>
<td>Apple Developer Program</td>
<td>$99 per year</td>
</tr>
<tr>
<td>Google Play Console</td>
<td>$25 one time</td>
</tr>
<tr>
<td>Lovable plan, hosting, Supabase</td>
<td>Whatever you already pay</td>
</tr>
</tbody></table>
<p>Both routes ship under your own developer accounts. Neither tool holds your listing.</p>
<h2>What the Capacitor route actually costs</h2>
<p>Capacitor itself is MIT licensed and free forever. That is real and worth saying plainly. What is not free is everything Capacitor deliberately leaves to you.</p>
<p>Open your repo and look. There is no <code>ios/</code> folder, no <code>android/</code> folder, and no <code>capacitor.config.ts</code>. Lovable builds with Vite, so the path is real and short: point <code>webDir</code> at <code>dist</code> and add the platforms. Ten minutes of work. Then the project is yours forever.</p>
<p><strong>Capacitor forks your Lovable workflow.</strong> This is the Lovable-specific cost and it is not on any pricing page. You keep prompting, Lovable keeps changing the web code, and the native shell becomes a second thing you own and have to keep in step. Every plugin you add, every config change, every permission string now lives outside the tool you actually build in.</p>
<p><strong>Over-the-air updates.</strong> Capacitor has no update service. The standard answer is <a href="https://capgo.app/pricing/">Capgo</a>, and it is a good product. At 1,000 MAU you need the Solo plan, which is $12 a month billed annually at $146, and includes 2,000 MAU, 100 GiB of bandwidth, unlimited live updates, and one build hour a month. There is also a credits-only pay-as-you-go option at $0.003 per MAU plus $0.06 per GiB, which for a small app lands closer to $40 to $60 a year if you are willing to manage prepaid credits instead of a plan. Either way it is a subscription, and it is metered on how many people use your app.</p>
<p><strong>A build machine.</strong> Historically this meant buying a Mac. It no longer has to. Capgo Native Build runs macOS M4 machines and includes App Store publishing on every plan, so the Mac requirement is gone if you are already paying for Capgo. Solo includes one build hour a month, with overage at $0.08 per build minute.</p>
<p><strong>The person who maintains the native project.</strong> This is the line item that does not go away, and it is where the money is.</p>
<p>Capacitor generates <code>ios/</code> and <code>android/</code> folders that are yours to keep. That is the whole design. It means you own the Podfile, the Gradle config, the Info.plist, the Android manifest, the plugin versions, and the target SDK. Capgo's own documentation is explicit about the boundary: changes to <code>capacitor.config.ts</code>, native plugin configuration, native package installs or upgrades, and anything requiring <code>npx cap sync</code> all need a native app release, not a live update.</p>
<p>Two of those releases are mandatory every year whether your product changes or not:</p>
<ul>
<li><p>Since <a href="https://developer.apple.com/news/upcoming-requirements/">April 28, 2026</a>, every upload to App Store Connect must be built with Xcode 26 and the iOS 26 SDK. Apple resets this baseline annually.</p>
</li>
<li><p>From <a href="https://developer.android.com/google/play/requirements/target-sdk">August 31, 2026</a>, new Google Play submissions and updates must target Android 16 (API 36), and apps that fall below API 35 stop being discoverable to new users on newer devices. Google runs this on an annual clock too.</p>
</li>
</ul>
<p>If you cannot do that work, you hire it. Rates for Capacitor finalization and store submission run roughly like this:</p>
<table>
<thead>
<tr>
<th>Who you hire</th>
<th>Typical range</th>
<th>What it covers</th>
</tr>
</thead>
<tbody><tr>
<td>Offshore freelancer</td>
<td>$150 to $350</td>
<td>Native folders generated, icons and splash screens, base store config, binary upload</td>
</tr>
<tr>
<td>Experienced Capacitor freelancer</td>
<td>$400 to $800</td>
<td>Native plugin setup, provisioning profiles, certificates, keystores, review guidance</td>
</tr>
<tr>
<td>Boutique agency or senior US/EU dev</td>
<td>$900 to $1,500+</td>
<td>CI/CD pipeline, OTA service wiring, dedicated support through rejections</td>
</tr>
</tbody></table>
<p>Mobile developers generally bill $30 to $100 an hour, and a first store deployment is 4 to 12 hours of work. That is the setup fee. The part nobody budgets for is that you come back to the same developer for two specific reasons, indefinitely.</p>
<p><strong>Native plugin changes.</strong> Adding Bluetooth, swapping push providers, wiring in a new SDK, or even changing the app icon all modify the native project and require a rebuilt binary. Expect $50 to $200 per update.</p>
<p><strong>Annual OS breakers.</strong> Every year Apple and Google raise the submission floor and change privacy rules, and every year some plugin in your dependency tree stops compiling against the new SDK. Keeping a Capacitor app compiling and compliant runs $150 to $500 a year in general maintenance, before you have shipped a single new feature.</p>
<h2>What Despia actually costs</h2>
<p>$249 per app per OS, one time. One app on both stores is $498, paid once, for the life of the app.</p>
<p>Each license includes 200 cloud credits, and a build costs 5 credits, so that is 40 native builds per app per OS. A two-platform app starts with 80. At the release cadence in this scenario, two mandatory compliance rebuilds a year across both platforms plus the occasional icon or permission change, that is roughly a decade of releases before you buy a single build. If you do run out, builds come in packs of 10 for $25.</p>
<p>Some Despia users do burn through the whole allowance inside a year. That is almost always a workflow problem rather than a pricing one, because a build is for native changes and a web change that gets rebuilt instead of deployed over the air is a build spent for nothing. The table below still assumes a $25 pack every two years, which is deliberately pessimistic against an allowance that should cover the decade.</p>
<p>Builds are also flat rate. A build is a build: it is not metered by the minute, so a slow one costs the same as a fast one and there is no per-minute overage to model. Over-the-air updates are unlimited with no MAU metering. There is no plan, no renewal, and no line that scales with how many people open your app.</p>
<p>Your workflow does not fork. You keep prompting Lovable, you deploy, and the deployed app is the app in the stores. Web changes ship over the air the moment you deploy.</p>
<p>Native changes still need a rebuild, exactly like Capacitor, because that is a store rule and not a tool limitation. The difference is what the two re-hire scenarios cost you here. A native plugin change is a toggle in the dashboard and one build, which spends 5 of the 200 credits your license came with, against $50 to $200. The annual OS breaker is not your line item at all, because there is no <code>ios/</code> folder in your repo to migrate to Xcode 26 and no <code>targetSdk</code> in a Gradle file you own.</p>
<p>Adding a native capability does not touch a native project either:</p>
<pre><code class="language-javascript">const isDespia = navigator.userAgent.toLowerCase().includes('despia')

if (isDespia) despia('successhaptic://')
</code></pre>
<p>No <code>npm install</code>, no <code>npx cap sync</code>, no native release to add a feature that was already compiled into the runtime.</p>
<h2>The ten-year totals</h2>
<p>Assumptions: one app on both stores, 1,000 MAU, six web releases a year, two mandatory native compliance rebuilds a year, Capgo Solo at the annual rate. Freelancer pricing taken at the middle of each range above: $600 for initial setup, $300 a year for OS compliance maintenance, and one native plugin change a year at $125. The Despia column assumes a $25 build pack every two years, which is conservative given two licenses already cover 80 builds.</p>
<table>
<thead>
<tr>
<th></th>
<th>Despia</th>
<th>Capacitor, you own the native project</th>
<th>Capacitor, you hire it out</th>
</tr>
</thead>
<tbody><tr>
<td>Year 1</td>
<td>$622</td>
<td>$270</td>
<td>$870</td>
</tr>
<tr>
<td>5 years</td>
<td>$1,068</td>
<td>$1,250</td>
<td>$3,550</td>
</tr>
<tr>
<td>10 years</td>
<td>$1,638</td>
<td>$2,475</td>
<td>$6,900</td>
</tr>
<tr>
<td>Recurring per year after setup</td>
<td>$99 store fee, plus about $13 a year in builds</td>
<td>$245</td>
<td>$670</td>
</tr>
<tr>
<td>Your hours, year one</td>
<td>Under 2</td>
<td>20 to 30</td>
<td>0</td>
</tr>
<tr>
<td>Your hours, per year after</td>
<td>Under 2</td>
<td>8 to 12</td>
<td>0</td>
</tr>
</tbody></table>
<p>Three things fall out of that table.</p>
<p><strong>The Capacitor DIY column is genuinely cheap in cash and expensive in hours.</strong> If you already write code and do not mind a yearly Xcode migration, $245 a year is a defensible number. Price your own 8 to 12 hours a year at anything above $30 an hour and the cash advantage disappears, but that is your call to make.</p>
<p><strong>The hired column is where most Lovable builders actually land</strong>, because the whole reason you chose Lovable is that you do not write Swift. At $6,900 over ten years, the free library is the most expensive option on the table, and it stays expensive because the maintenance never ends.</p>
<p><strong>Despia's cost is front-loaded and then nearly flat.</strong> After the license, the recurring lines are the $99 Apple fee that every route pays and about $13 a year in builds, and that build figure is a pessimistic placeholder rather than a bill you should expect.</p>
<h2>The line that actually diverges: growth</h2>
<p>The 1,000 MAU comparison is the friendly one for a metered service. Watch what happens when the app works.</p>
<table>
<thead>
<tr>
<th>Monthly active users</th>
<th>Capgo annual</th>
<th>Despia annual</th>
</tr>
</thead>
<tbody><tr>
<td>1,000</td>
<td>$146 (Solo)</td>
<td>$0</td>
</tr>
<tr>
<td>10,000</td>
<td>$396 (Maker)</td>
<td>$0</td>
</tr>
<tr>
<td>100,000</td>
<td>$998 (Team)</td>
<td>$0</td>
</tr>
<tr>
<td>1,000,000</td>
<td>$2,490+ (Enterprise)</td>
<td>$0</td>
</tr>
</tbody></table>
<p>Capgo counts MAU as distinct devices contacting Capgo in a rolling 30-day window, and the same device counts separately per native app ID. Despia does not meter update delivery at all. Over ten years, that is the difference between a cost that grows with your success and a cost that was fixed the day you bought it.</p>
<h2>Why Despia costs more on day one</h2>
<p>Two reasons, and neither is margin.</p>
<p>The first is that you are buying a runtime, not a library. Despia ships a compiled native layer with 50-plus device capabilities already in it, so the price includes the work that Capacitor pushes to plugin selection, native project maintenance, and your yearly SDK migration.</p>
<p>The second is that it is a license, not a subscription. The $249 is the whole software cost, decided once, at a price that does not move when your user count does. That is a worse deal for someone who ships an app, gets 40 users, and abandons it. It is a much better deal for anyone whose app survives.</p>
<p>The obvious objection stands: a Despia app runs your web code in WKWebView and the Android WebView, and that is not SwiftUI. True. The answer is the 50-plus native capabilities exposed through one JavaScript call, and the fact that the full Xcode and Android Studio projects export whenever you want them.</p>
<h2>When Capacitor is the right call</h2>
<p>Plainly, because a comparison that never concedes anything is an ad.</p>
<ul>
<li><p>You have an in-house mobile developer, or you are one. The native project is an asset in that case, not a liability, and the cash cost is low.</p>
</li>
<li><p>You need custom native code or a specific plugin that is not in Despia's feature set. Capacitor's plugin ecosystem is large and open.</p>
</li>
<li><p>You have stopped prompting. If the Lovable phase is over and the repo is now a normal codebase you maintain by hand, the forked-workflow argument mostly evaporates.</p>
</li>
<li><p>You want the entire stack auditable and self-hostable. Capacitor is MIT, and Capgo's plugin and backend are open source under MIT and MPL-2.0.</p>
</li>
</ul>
<h2>Common questions</h2>
<p><strong>Does my Supabase backend need to change?</strong> No. It is an HTTPS API and a native client talks to it exactly as the web app does. Nothing about the database, auth, or edge functions moves.</p>
<p><strong>Can I just ship a PWA instead?</strong> You can, and it costs nothing, but there is no App Store listing at the end of it and iOS push requires the user to manually add the app to their Home Screen first.</p>
<p><strong>Does Despia charge for each store update?</strong> No. Web changes ship over the air with no store review and no per-update fee. Native changes need a rebuild on any route, and on Despia it spends 5 credits from the 200 each license includes, which is 40 builds per app per OS. Beyond that, builds are $25 per pack of 10, flat rate with no per-minute metering.</p>
<p><strong>Is Capgo required with Capacitor?</strong> No, but something is. Capacitor has no update mechanism of its own, so without an OTA service every web change becomes a store release with a review wait.</p>
<p><strong>Can I leave Despia later?</strong> Yes. The Xcode project and Android Studio project export at any time, and the app is registered under your own Apple and Google accounts throughout.</p>
<h2>Before you budget, read these</h2>
<ul>
<li><p><a href="https://blog.despia.com/how-to-add-a-revenuecat-paywall-to-a-webview-app">How to add a RevenueCat paywall to a web view app</a></p>
</li>
<li><p><a href="https://blog.despia.com/lovable-app-store-approval-the-rejection-checklist">Lovable App Store approval: the rejection checklist</a></p>
</li>
<li><p><a href="https://blog.despia.com/lovable-app-permissions-fix-the-5-1-1-rejection">Lovable app permissions: fix the 5.1.1 rejection</a></p>
</li>
</ul>
<h2>Ship one app, not two</h2>
<p>If you are going to keep building in Lovable, keep building there. Despia ships that app to the App Store and Google Play, gives it native device access, and updates it over the air, from the one codebase you already have.</p>
<p><a href="https://setup.despia.com">Learn more about Despia in the docs</a> or <a href="https://despia.com">start building at despia.com</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Base44 App Store Cost: The Real 10-Year Math]]></title><description><![CDATA[Every Base44 user pricing out a mobile launch hits the same three numbers: Base44's built-in publishing is included, Capacitor is free, and Despia is $249 per app per OS. Two of those three are true a]]></description><link>https://blog.despia.com/base44-app-store-cost-the-real-10-year-math</link><guid isPermaLink="true">https://blog.despia.com/base44-app-store-cost-the-real-10-year-math</guid><category><![CDATA[capacitor]]></category><category><![CDATA[base44]]></category><category><![CDATA[webapp]]></category><category><![CDATA[PWA]]></category><category><![CDATA[iOS]]></category><category><![CDATA[ios app development]]></category><category><![CDATA[Android]]></category><category><![CDATA[android app development]]></category><dc:creator><![CDATA[Despia CEO]]></dc:creator><pubDate>Thu, 06 Aug 2026 07:23:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/67f0239ff4f5a0b7b4a852c4/00307de9-9fae-4487-8b9f-cea7968bd61f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Every Base44 user pricing out a mobile launch hits the same three numbers: Base44's built-in publishing is included, Capacitor is free, and Despia is $249 per app per OS. Two of those three are true and misleading, because none of them is the cost of keeping an app in the stores for ten years.</p>
<p>Here is the full arithmetic, with every number sourced and every assumption stated.</p>
<h2>The scenario</h2>
<p>Fixing the scenario matters more than the pricing pages, because every route gets cheap or expensive depending on who you are.</p>
<ul>
<li><p>You built the app in Base44. It works, it is hosted, and you are done prompting for now.</p>
</li>
<li><p>You ship it to both the App Store and Google Play.</p>
</li>
<li><p>It has 1,000 monthly active users.</p>
</li>
<li><p>You release web changes maybe six times a year.</p>
</li>
<li><p>You are not an iOS or Android developer, and you do not want to become one.</p>
</li>
</ul>
<p>That last line is the one that moves the total by thousands of dollars, and it is the line most cost comparisons leave out.</p>
<h2>Start with the route that costs nothing</h2>
<p>Base44 has built-in store publishing, and for some apps it is the correct answer. Do not skip past it because it is free.</p>
<p>The limits are stated in <a href="https://docs.base44.com/documentation/building-your-app/uploading-to-app-stores">Base44's own documentation</a>: the mobile output is a web view, with no push notifications and no full offline support. If your app has no payments, no push, and no device access, that is a genuinely fine place to stop, and everything below this line is money you do not need to spend.</p>
<p>If your app needs push, in-app purchases, biometrics, background location, or anything else Apple looks for under <a href="https://developer.apple.com/app-store/review/guidelines/#minimum-functionality">Guideline 4.2</a>, you are choosing between the other two routes, and the cost question becomes real.</p>
<h2>Costs every route pays</h2>
<p>These are identical whichever way you go, so they are not the argument. Listing them anyway, because a total that hides them is not a total.</p>
<table>
<thead>
<tr>
<th>Line item</th>
<th>Cost</th>
</tr>
</thead>
<tbody><tr>
<td>Apple Developer Program</td>
<td>$99 per year</td>
</tr>
<tr>
<td>Google Play Console</td>
<td>$25 one time</td>
</tr>
<tr>
<td>Base44 hosting and plan</td>
<td>Whatever you already pay</td>
</tr>
</tbody></table>
<p>Both paid routes ship under your own developer accounts. Neither tool holds your listing.</p>
<h2>What the Capacitor route actually costs</h2>
<p>Capacitor itself is MIT licensed and free forever. That is real and worth saying plainly. What is not free is everything around it.</p>
<p><strong>The export is not free and not complete.</strong> Both ZIP and GitHub export require a paid Base44 plan, so the free library starts behind a paywall. What you get is a Vite React project, which means <code>npm run build</code> to <code>dist</code> is the right entry point for Capacitor. What you do not get is the backend: entity schemas, live records, and backend logic stay on Base44, and your local <code>.env</code> still needs <code>VITE_BASE44_APP_ID</code> and <code>VITE_BASE44_APP_BASE_URL</code> pointing back at Base44's servers. Base44's own CLI has <code>login</code>, <code>create</code>, entity and function sync, deploy to Base44 hosting, and an <code>eject</code> command. None of those has a mobile build target.</p>
<p><strong>Over-the-air updates.</strong> Capacitor has no update service. The standard answer is <a href="https://capgo.app/pricing/">Capgo</a>, and it is a good product. At 1,000 MAU you need the Solo plan, which is $12 a month billed annually at $146, and includes 2,000 MAU, 100 GiB of bandwidth, unlimited live updates, and one build hour a month. There is also a credits-only pay-as-you-go option at $0.003 per MAU plus $0.06 per GiB, which for a small app lands closer to $40 to $60 a year if you are willing to manage prepaid credits instead of a plan. Either way it is a subscription, and it is metered on how many people use your app.</p>
<p><strong>A build machine.</strong> Historically this meant buying a Mac. It no longer has to. Capgo Native Build runs macOS M4 machines and includes App Store publishing on every plan, so the Mac requirement is gone if you are already paying for Capgo. Solo includes one build hour a month, with overage at $0.08 per build minute.</p>
<p><strong>The person who maintains the native project.</strong> This is the line item that does not go away, and it is where the money is.</p>
<p>Capacitor generates <code>ios/</code> and <code>android/</code> folders that are yours to keep. That is the whole design. It means you own the Podfile, the Gradle config, the Info.plist, the Android manifest, the plugin versions, and the target SDK. Capgo's own documentation is explicit about the boundary: changes to <code>capacitor.config.ts</code>, native plugin configuration, native package installs or upgrades, and anything requiring <code>npx cap sync</code> all need a native app release, not a live update.</p>
<p>Two of those releases are mandatory every year whether your product changes or not:</p>
<ul>
<li><p>Since <a href="https://developer.apple.com/news/upcoming-requirements/">April 28, 2026</a>, every upload to App Store Connect must be built with Xcode 26 and the iOS 26 SDK. Apple resets this baseline annually.</p>
</li>
<li><p>From <a href="https://developer.android.com/google/play/requirements/target-sdk">August 31, 2026</a>, new Google Play submissions and updates must target Android 16 (API 36), and apps that fall below API 35 stop being discoverable to new users on newer devices. Google runs this on an annual clock too.</p>
</li>
</ul>
<p>If you cannot do that work, you hire it. Rates for Capacitor finalization and store submission run roughly like this:</p>
<table>
<thead>
<tr>
<th>Who you hire</th>
<th>Typical range</th>
<th>What it covers</th>
</tr>
</thead>
<tbody><tr>
<td>Offshore freelancer</td>
<td>$150 to $350</td>
<td>Native folders generated, icons and splash screens, base store config, binary upload</td>
</tr>
<tr>
<td>Experienced Capacitor freelancer</td>
<td>$400 to $800</td>
<td>Native plugin setup, provisioning profiles, certificates, keystores, review guidance</td>
</tr>
<tr>
<td>Boutique agency or senior US/EU dev</td>
<td>$900 to $1,500+</td>
<td>CI/CD pipeline, OTA service wiring, dedicated support through rejections</td>
</tr>
</tbody></table>
<p>Mobile developers generally bill $30 to $100 an hour, and a first store deployment is 4 to 12 hours of work. That is the setup fee. The part nobody budgets for is that you come back to the same developer for two specific reasons, indefinitely.</p>
<p><strong>Native plugin changes.</strong> Adding Bluetooth, swapping push providers, wiring in a new SDK, or even changing the app icon all modify the native project and require a rebuilt binary. Expect $50 to $200 per update.</p>
<p><strong>Annual OS breakers.</strong> Every year Apple and Google raise the submission floor and change privacy rules, and every year some plugin in your dependency tree stops compiling against the new SDK. Keeping a Capacitor app compiling and compliant runs $150 to $500 a year in general maintenance, before you have shipped a single new feature.</p>
<h2>What Despia actually costs</h2>
<p>$249 per app per OS, one time. One app on both stores is $498, paid once, for the life of the app.</p>
<p>Each license includes 200 cloud credits, and a build costs 5 credits, so that is 40 native builds per app per OS. A two-platform app starts with 80. At the release cadence in this scenario, two mandatory compliance rebuilds a year across both platforms plus the occasional icon or permission change, that is roughly a decade of releases before you buy a single build. If you do run out, builds come in packs of 10 for $25.</p>
<p>Some Despia users do burn through the whole allowance inside a year. That is almost always a workflow problem rather than a pricing one, because a build is for native changes and a web change that gets rebuilt instead of deployed over the air is a build spent for nothing. The table below still assumes a $25 pack every two years, which is deliberately pessimistic against an allowance that should cover the decade.</p>
<p>Builds are also flat rate. A build is a build: it is not metered by the minute, so a slow one costs the same as a fast one and there is no per-minute overage to model. Over-the-air updates are unlimited with no MAU metering. There is no plan, no renewal, and no line that scales with how many people open your app.</p>
<p>Your Base44 backend does not change. It is an HTTPS API, and it behaves identically from a native client.</p>
<p>Web changes ship over the air the moment you deploy. Native changes still need a rebuild, exactly like Capacitor, because that is a store rule and not a tool limitation. The difference is what the two re-hire scenarios cost you here. A native plugin change is a toggle in the dashboard and one build, which spends 5 of the 200 credits your license came with, against $50 to $200. The annual OS breaker is not your line item at all, because there is no <code>ios/</code> folder in your repo to migrate to Xcode 26 and no <code>targetSdk</code> in a Gradle file you own.</p>
<p>Adding a native capability does not touch a native project either:</p>
<pre><code class="language-javascript">const isDespia = navigator.userAgent.toLowerCase().includes('despia')

if (isDespia) despia('successhaptic://')
</code></pre>
<p>No <code>npm install</code>, no <code>npx cap sync</code>, no native release to add a feature that was already compiled into the runtime.</p>
<h2>The ten-year totals</h2>
<p>Assumptions: one app on both stores, 1,000 MAU, six web releases a year, two mandatory native compliance rebuilds a year, Capgo Solo at the annual rate. Freelancer pricing taken at the middle of each range above: $600 for initial setup, $300 a year for OS compliance maintenance, and one native plugin change a year at $125. The Despia column assumes a $25 build pack every two years, which is conservative given two licenses already cover 80 builds.</p>
<table>
<thead>
<tr>
<th></th>
<th>Despia</th>
<th>Capacitor, you own the native project</th>
<th>Capacitor, you hire it out</th>
</tr>
</thead>
<tbody><tr>
<td>Year 1</td>
<td>$622</td>
<td>$270</td>
<td>$870</td>
</tr>
<tr>
<td>5 years</td>
<td>$1,068</td>
<td>$1,250</td>
<td>$3,550</td>
</tr>
<tr>
<td>10 years</td>
<td>$1,638</td>
<td>$2,475</td>
<td>$6,900</td>
</tr>
<tr>
<td>Recurring per year after setup</td>
<td>$99 store fee, plus about $13 a year in builds</td>
<td>$245</td>
<td>$670</td>
</tr>
<tr>
<td>Your hours, year one</td>
<td>Under 2</td>
<td>20 to 30</td>
<td>0</td>
</tr>
<tr>
<td>Your hours, per year after</td>
<td>Under 2</td>
<td>8 to 12</td>
<td>0</td>
</tr>
</tbody></table>
<p>Three things fall out of that table.</p>
<p><strong>The Capacitor DIY column is genuinely cheap in cash and expensive in hours.</strong> If you already write code and do not mind a yearly Xcode migration, $245 a year is a defensible number. Price your own 8 to 12 hours a year at anything above $30 an hour and the cash advantage disappears, but that is your call to make.</p>
<p><strong>The hired column is where most Base44 builders actually land</strong>, because the whole reason you chose Base44 is that you do not write Swift. At $6,900 over ten years, the free library is the most expensive option on the table, and it stays expensive because the maintenance never ends.</p>
<p><strong>Despia's cost is front-loaded and then nearly flat.</strong> After the license, the recurring lines are the $99 Apple fee that every route pays and about $13 a year in builds, and that build figure is a pessimistic placeholder rather than a bill you should expect.</p>
<h2>The line that actually diverges: growth</h2>
<p>The 1,000 MAU comparison is the friendly one for a metered service. Watch what happens when the app works.</p>
<table>
<thead>
<tr>
<th>Monthly active users</th>
<th>Capgo annual</th>
<th>Despia annual</th>
</tr>
</thead>
<tbody><tr>
<td>1,000</td>
<td>$146 (Solo)</td>
<td>$0</td>
</tr>
<tr>
<td>10,000</td>
<td>$396 (Maker)</td>
<td>$0</td>
</tr>
<tr>
<td>100,000</td>
<td>$998 (Team)</td>
<td>$0</td>
</tr>
<tr>
<td>1,000,000</td>
<td>$2,490+ (Enterprise)</td>
<td>$0</td>
</tr>
</tbody></table>
<p>Capgo counts MAU as distinct devices contacting Capgo in a rolling 30-day window, and the same device counts separately per native app ID. Despia does not meter update delivery at all. Over ten years, that is the difference between a cost that grows with your success and a cost that was fixed the day you bought it.</p>
<h2>Why Despia costs more on day one</h2>
<p>Two reasons, and neither is margin.</p>
<p>The first is that you are buying a runtime, not a library. Despia ships a compiled native layer with 50-plus device capabilities already in it, so the price includes the work that Capacitor pushes to plugin selection, native project maintenance, and your yearly SDK migration.</p>
<p>The second is that it is a license, not a subscription. The $249 is the whole software cost, decided once, at a price that does not move when your user count does. That is a worse deal for someone who ships an app, gets 40 users, and abandons it. It is a much better deal for anyone whose app survives.</p>
<p>The obvious objection stands: a Despia app runs your web code in WKWebView and the Android WebView, and that is not SwiftUI. True. The answer is the 50-plus native capabilities exposed through one JavaScript call, and the fact that the full Xcode and Android Studio projects export whenever you want them.</p>
<h2>When each route is the right call</h2>
<p>Plainly, because a comparison that never concedes anything is an ad.</p>
<ul>
<li><p><strong>Base44 built-in publishing</strong> if your app has no payments, no push, and no device access. It is included and it works within the limits Base44 states.</p>
</li>
<li><p><strong>Export plus Capacitor</strong> if you have a developer, or you are one. The native project is an asset in that case, not a liability.</p>
</li>
<li><p><strong>Despia</strong> if you need the native features that get an app past review and you do not want to own a native project to get them.</p>
</li>
</ul>
<h2>Common questions</h2>
<p><strong>Does the Base44 CLI build a mobile app?</strong> No. The CLI handles login, project creation, entity and function sync, deployment to Base44 hosting, and <code>eject</code>. There is no mobile build target in it.</p>
<p><strong>Does my Base44 backend need rebuilding for a native app?</strong> No. Entity schemas, records, and backend logic stay on Base44, and a native client talks to the same HTTPS API the web app does.</p>
<p><strong>Does Despia charge for each store update?</strong> No. Web changes ship over the air with no store review and no per-update fee. Native changes need a rebuild on any route, and on Despia it spends 5 credits from the 200 each license includes, which is 40 builds per app per OS. Beyond that, builds are $25 per pack of 10, flat rate with no per-minute metering.</p>
<p><strong>Is Capgo required with Capacitor?</strong> No, but something is. Capacitor has no update mechanism of its own, so without an OTA service every web change becomes a store release with a review wait.</p>
<p><strong>Can I leave Despia later?</strong> Yes. The Xcode project and Android Studio project export at any time, and the app is registered under your own Apple and Google accounts throughout.</p>
<h2>Before you budget, read these</h2>
<ul>
<li><p><a href="https://blog.despia.com/base44-in-app-purchases-without-webhooks-revenuecat">In-app purchases on Base44 without webhooks, using RevenueCat</a></p>
</li>
<li><p><a href="https://blog.despia.com/base44-app-store-approval-the-rejection-checklist">Base44 App Store approval: the rejection checklist</a></p>
</li>
<li><p><a href="https://blog.despia.com/base44-app-permissions-fix-the-5-1-1-rejection">Base44 app permissions: fix the 5.1.1 rejection</a></p>
</li>
</ul>
<h2>Ship one app, not two</h2>
<p>If you are going to keep building in Base44, keep building there. Despia ships that app to the App Store and Google Play, gives it native device access, and updates it over the air, from the one project you already have.</p>
<p><a href="https://setup.despia.com">Learn more about Despia in the docs</a> or <a href="https://despia.com">start building at despia.com</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Vibe Coding Mobile App Cost: 1, 5 and 10 Years]]></title><description><![CDATA[You have a terminal, a repo, and an agent that will happily run npx cap add ios and tell you it worked. That is the part that is genuinely cheap now. The part that has not gotten cheaper is the ten ye]]></description><link>https://blog.despia.com/vibe-coding-mobile-app-cost-1-5-and-10-years</link><guid isPermaLink="true">https://blog.despia.com/vibe-coding-mobile-app-cost-1-5-and-10-years</guid><category><![CDATA[capacitor]]></category><category><![CDATA[#ai-tools]]></category><category><![CDATA[apps]]></category><category><![CDATA[React Native]]></category><category><![CDATA[Flutter]]></category><dc:creator><![CDATA[Despia CEO]]></dc:creator><pubDate>Thu, 06 Aug 2026 07:07:49 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/67f0239ff4f5a0b7b4a852c4/e4f76b0f-000c-4a11-b89e-34626e140752.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>You have a terminal, a repo, and an agent that will happily run <code>npx cap add ios</code> and tell you it worked. That is the part that is genuinely cheap now. The part that has not gotten cheaper is the ten years afterwards, when someone has to keep a native project compiling against Apple's and Google's moving targets, and your agent's training data is a year behind the deadline that just broke your build.</p>
<p>Here is the full arithmetic, with every number sourced and every assumption stated.</p>
<h2>The scenario</h2>
<p>Fixing the scenario matters more than the pricing pages, because both routes get cheap or expensive depending on who you are.</p>
<ul>
<li><p>You built a web app with Cursor, Claude Code, or something similar. React, Vue, Svelte, whatever the agent reached for. It is built and hosted.</p>
</li>
<li><p>You ship it to both the App Store and Google Play.</p>
</li>
<li><p>It has 1,000 monthly active users.</p>
</li>
<li><p>You release web changes maybe six times a year.</p>
</li>
<li><p>You can read code and run commands, but you have never shipped a native app and you do not want the native project to become your job.</p>
</li>
</ul>
<p>That last line is where the money is. You are not the no-code builder who cannot open a terminal, and you are also not an iOS developer. The costs land differently for you than for either.</p>
<h2>Costs both routes pay</h2>
<p>These are identical either way, so they are not the argument. Listing them anyway, because a total that hides them is not a total.</p>
<table>
<thead>
<tr>
<th>Line item</th>
<th>Cost</th>
</tr>
</thead>
<tbody><tr>
<td>Apple Developer Program</td>
<td>$99 per year</td>
</tr>
<tr>
<td>Google Play Console</td>
<td>$25 one time</td>
</tr>
<tr>
<td>Web hosting</td>
<td>Whatever you already pay</td>
</tr>
</tbody></table>
<p>Both routes ship under your own developer accounts. Neither tool holds your listing.</p>
<h2>What Capacitor actually costs</h2>
<p>Capacitor itself is MIT licensed and free forever. That is real and worth saying plainly, and for someone with a working terminal it is a genuine option in a way it is not for a no-code builder. What is not free is everything Capacitor deliberately leaves to you.</p>
<p><strong>The thing your agent is good at is the cheap part.</strong> Generating <code>capacitor.config.ts</code>, pointing <code>webDir</code> at your build output, running <code>npx cap add ios android</code>. Twenty minutes, and the agent will do it well. That work was never the expensive part.</p>
<p><strong>The thing your agent is bad at is the recurring part.</strong> Native project maintenance is a moving target against SDK versions and store policies that changed after training. The failure mode is specific and it costs you real cycles: an agent will emit a plugin method that is real, documented, and idiomatic, just for a different package or a different major version than the one in your <code>package.json</code>. It compiles or it does not, you find out in a build, and every one of those round trips is a native rebuild rather than a hot reload. A web-only project absorbs that cheaply. A native project does not.</p>
<p><strong>Over-the-air updates.</strong> Capacitor has no update service. The standard answer is <a href="https://capgo.app/pricing/">Capgo</a>, and it is a good product. At 1,000 MAU you need the Solo plan, which is $12 a month billed annually at $146, and includes 2,000 MAU, 100 GiB of bandwidth, unlimited live updates, and one build hour a month. There is also a credits-only pay-as-you-go option at $0.003 per MAU plus $0.06 per GiB, which for a small app lands closer to $40 to $60 a year if you are willing to manage prepaid credits. Either way it is a subscription, and it is metered on how many people use your app.</p>
<p><strong>A build machine.</strong> Historically this meant buying a Mac. It no longer has to. Capgo Native Build runs macOS M4 machines and includes App Store publishing on every plan, so the Mac requirement is gone if you are already paying for Capgo. Solo includes one build hour a month, with overage at $0.08 per build minute.</p>
<p><strong>The annual compliance work.</strong> Capacitor generates <code>ios/</code> and <code>android/</code> folders that are yours to keep. That is the whole design. You own the Podfile, the Gradle config, the Info.plist, the Android manifest, the plugin versions, and the target SDK. Capgo's own documentation is explicit about the boundary: changes to <code>capacitor.config.ts</code>, native plugin configuration, native package installs or upgrades, and anything requiring <code>npx cap sync</code> all need a native app release, not a live update.</p>
<p>Two of those releases are mandatory every year whether your product changes or not:</p>
<ul>
<li><p>Since <a href="https://developer.apple.com/news/upcoming-requirements/">April 28, 2026</a>, every upload to App Store Connect must be built with Xcode 26 and the iOS 26 SDK. Apple resets this baseline annually.</p>
</li>
<li><p>From <a href="https://developer.android.com/google/play/requirements/target-sdk">August 31, 2026</a>, new Google Play submissions and updates must target Android 16 (API 36), and apps that fall below API 35 stop being discoverable to new users on newer devices. Google runs this on an annual clock too.</p>
</li>
</ul>
<p>If you do not do that work yourself, you hire it. Rates run roughly like this:</p>
<table>
<thead>
<tr>
<th>Who you hire</th>
<th>Typical range</th>
<th>What it covers</th>
</tr>
</thead>
<tbody><tr>
<td>Offshore freelancer</td>
<td>$150 to $350</td>
<td>Native folders generated, icons and splash screens, base store config, binary upload</td>
</tr>
<tr>
<td>Experienced Capacitor freelancer</td>
<td>$400 to $800</td>
<td>Native plugin setup, provisioning profiles, certificates, keystores, review guidance</td>
</tr>
<tr>
<td>Boutique agency or senior US/EU dev</td>
<td>$900 to $1,500+</td>
<td>CI/CD pipeline, OTA service wiring, dedicated support through rejections</td>
</tr>
</tbody></table>
<p>Mobile developers generally bill $30 to $100 an hour, and a first store deployment is 4 to 12 hours of work. That is the setup fee. After that you come back for two specific reasons, indefinitely.</p>
<p><strong>Native plugin changes.</strong> Adding Bluetooth, swapping push providers, wiring in a new SDK, or even changing the app icon all modify the native project and require a rebuilt binary. Expect $50 to $200 per update, or your own afternoon.</p>
<p><strong>Annual OS breakers.</strong> Every year Apple and Google raise the submission floor, and every year some plugin in your dependency tree stops compiling against the new SDK. Keeping a Capacitor app compiling and compliant runs $150 to $500 a year in general maintenance, before you have shipped a single new feature.</p>
<h2>What Despia actually costs</h2>
<p>$249 per app per OS, one time. One app on both stores is $498, paid once, for the life of the app.</p>
<p>Each license includes 200 cloud credits, and a build costs 5 credits, so that is 40 native builds per app per OS. A two-platform app starts with 80. At the release cadence in this scenario, two mandatory compliance rebuilds a year across both platforms plus the occasional icon or permission change, that is roughly a decade of releases before you buy a single build. If you do run out, builds come in packs of 10 for $25.</p>
<p>Some Despia users do burn through the whole allowance inside a year. That is almost always a workflow problem rather than a pricing one, because a build is for native changes and a web change that gets rebuilt instead of deployed over the air is a build spent for nothing. The table below still assumes a $25 pack every two years, which is deliberately pessimistic against an allowance that should cover the decade.</p>
<p>Builds are also flat rate. A build is a build: it is not metered by the minute, so a slow one costs the same as a fast one and there is no per-minute overage to model. Over-the-air updates are unlimited with no MAU metering. There is no plan, no renewal, and no line that scales with how many people open your app.</p>
<p>Web changes ship over the air the moment you deploy. Native changes still need a rebuild, exactly like Capacitor, because that is a store rule and not a tool limitation. The difference is what the two re-hire scenarios cost you here. A native plugin change is a toggle in the dashboard and one build, which spends 5 of the 200 credits your license came with, or one afternoon you get back. The annual OS breaker is not your line item at all, because there is no <code>ios/</code> folder in your repo to migrate to Xcode 26 and no <code>targetSdk</code> in a Gradle file you own.</p>
<p>There is also less surface for your agent to get wrong. Native capabilities are scheme calls in your web code, not a plugin graph with versions:</p>
<pre><code class="language-javascript">const isDespia = navigator.userAgent.toLowerCase().includes('despia')

if (isDespia) despia('successhaptic://')
</code></pre>
<p>No <code>npm install</code>, no <code>npx cap sync</code>, no native release to add a feature that was already compiled into the runtime.</p>
<h2>The ten-year totals</h2>
<p>Assumptions: one app on both stores, 1,000 MAU, six web releases a year, two mandatory native compliance rebuilds a year, Capgo Solo at the annual rate. Freelancer pricing taken at the middle of each range above: $600 for initial setup, $300 a year for OS compliance maintenance, and one native plugin change a year at $125. The Despia column assumes a $25 build pack every two years, which is conservative given two licenses already cover 80 builds.</p>
<table>
<thead>
<tr>
<th></th>
<th>Despia</th>
<th>Capacitor, you own the native project</th>
<th>Capacitor, you hire it out</th>
</tr>
</thead>
<tbody><tr>
<td>Year 1</td>
<td>$622</td>
<td>$270</td>
<td>$870</td>
</tr>
<tr>
<td>5 years</td>
<td>$1,068</td>
<td>$1,250</td>
<td>$3,550</td>
</tr>
<tr>
<td>10 years</td>
<td>$1,638</td>
<td>$2,475</td>
<td>$6,900</td>
</tr>
<tr>
<td>Recurring per year after setup</td>
<td>$99 store fee, plus about $13 a year in builds</td>
<td>$245</td>
<td>$670</td>
</tr>
<tr>
<td>Your hours, year one</td>
<td>Under 2</td>
<td>20 to 30</td>
<td>0</td>
</tr>
<tr>
<td>Your hours, per year after</td>
<td>Under 2</td>
<td>8 to 12</td>
<td>0</td>
</tr>
</tbody></table>
<p>The middle column is the one that applies to you, and it is the honest competition. $245 a year is cheap, and you are capable of running it. The question is not whether you can, it is whether 20 to 30 hours in year one and 8 to 12 hours every year after is where you want your attention. That is a real trade and nobody can price your hours but you.</p>
<p>The hired column exists because plenty of people start in the middle column and end up in the right-hand one the first time an SDK migration eats a weekend. At $6,900 over ten years, the free library becomes the most expensive option on the table.</p>
<p>Despia's cost is front-loaded and then nearly flat. After the license, the recurring lines are the $99 Apple fee that every route pays and about $13 a year in builds, and that build figure is a pessimistic placeholder rather than a bill you should expect.</p>
<h2>The line that actually diverges: growth</h2>
<p>The 1,000 MAU comparison is the friendly one for a metered service. Watch what happens when the app works.</p>
<table>
<thead>
<tr>
<th>Monthly active users</th>
<th>Capgo annual</th>
<th>Despia annual</th>
</tr>
</thead>
<tbody><tr>
<td>1,000</td>
<td>$146 (Solo)</td>
<td>$0</td>
</tr>
<tr>
<td>10,000</td>
<td>$396 (Maker)</td>
<td>$0</td>
</tr>
<tr>
<td>100,000</td>
<td>$998 (Team)</td>
<td>$0</td>
</tr>
<tr>
<td>1,000,000</td>
<td>$2,490+ (Enterprise)</td>
<td>$0</td>
</tr>
</tbody></table>
<p>Capgo counts MAU as distinct devices contacting Capgo in a rolling 30-day window, and the same device counts separately per native app ID. Despia does not meter update delivery at all. Over ten years, that is the difference between a cost that grows with your success and a cost that was fixed the day you bought it.</p>
<h2>Why Despia costs more on day one</h2>
<p>Two reasons, and neither is margin.</p>
<p>The first is that you are buying a runtime, not a library. Despia ships a compiled native layer with 50-plus device capabilities already in it, so the price includes the work that Capacitor pushes to plugin selection, native project maintenance, and your yearly SDK migration.</p>
<p>The second is that it is a license, not a subscription. The $249 is the whole software cost, decided once, at a price that does not move when your user count does. That is a worse deal for someone who ships an app, gets 40 users, and abandons it. It is a much better deal for anyone whose app survives.</p>
<p>The obvious objection stands: a Despia app runs your web code in WKWebView and the Android WebView, and that is not SwiftUI. True. The answer is the 50-plus native capabilities exposed through one JavaScript call, and the fact that the full Xcode and Android Studio projects export whenever you want them. If you outgrow the managed path, you take the project and go.</p>
<h2>When Capacitor is the right call</h2>
<p>Plainly, because a comparison that never concedes anything is an ad.</p>
<ul>
<li><p>You want to own the native project and you are willing to learn it. This is the most defensible reason and it is a good one.</p>
</li>
<li><p>You need custom native code or a specific plugin outside Despia's feature set. Capacitor's plugin ecosystem is large and open, and writing your own plugin is a supported path.</p>
</li>
<li><p>You are shipping several apps and want one pipeline you control end to end.</p>
</li>
<li><p>You want the entire stack auditable and self-hostable. Capacitor is MIT, and Capgo's plugin and backend are open source under MIT and MPL-2.0.</p>
</li>
</ul>
<h2>Common questions</h2>
<p><strong>Can my agent handle the annual SDK migration?</strong> Partly. It can bump versions and fix obvious compile errors. It struggles where the correct answer changed after its training cutoff, which is exactly what an annual store deadline is. Budget the hours rather than assuming they are free.</p>
<p><strong>I already have Cursor and a Mac. Should I just use Capacitor?</strong> Possibly, and the middle column is the honest number for you. Run it if you want to own the native project. Do not run it because the setup step looked cheap, since setup is not the recurring cost.</p>
<p><strong>Does Despia charge for each store update?</strong> No. Web changes ship over the air with no store review and no per-update fee. Native changes need a rebuild on any route, and on Despia it spends 5 credits from the 200 each license includes, which is 40 builds per app per OS. Beyond that, builds are $25 per pack of 10, flat rate with no per-minute metering.</p>
<p><strong>Is Capgo required with Capacitor?</strong> No, but something is. Capacitor has no update mechanism of its own, so without an OTA service every web change becomes a store release with a review wait.</p>
<p><strong>Can I leave Despia later?</strong> Yes. The Xcode project and Android Studio project export at any time, and the app is registered under your own Apple and Google accounts throughout.</p>
<h2>Before you budget, read these</h2>
<ul>
<li><p><a href="https://blog.despia.com/convert-your-web-app-to-ios-and-android-apps">Convert your web app to iOS and Android apps</a></p>
</li>
<li><p><a href="https://blog.despia.com/despia-mcp-server-stop-burning-credits-on-bad-code">The Despia MCP server: stop burning credits on bad code</a></p>
</li>
</ul>
<h2>Ship one app, not two</h2>
<p>If you are going to keep building in your web stack, keep building there. Despia ships that app to the App Store and Google Play, gives it native device access, and updates it over the air, from the one codebase you already have.</p>
<p><a href="https://setup.despia.com">Learn more about Despia in the docs</a> or <a href="https://despia.com">start building at despia.com</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Despia vs Capacitor: The Real Cost Over 10 Years]]></title><description><![CDATA[Capacitor is free and Despia costs $249 per app per OS. That comparison is true for about ten minutes, which is roughly how long it takes to discover that Capacitor is a library, not a delivery pipeli]]></description><link>https://blog.despia.com/despia-vs-capacitor-the-real-cost-over-10-years</link><guid isPermaLink="true">https://blog.despia.com/despia-vs-capacitor-the-real-cost-over-10-years</guid><category><![CDATA[capacitor]]></category><category><![CDATA[webview]]></category><category><![CDATA[PWA]]></category><dc:creator><![CDATA[Despia CEO]]></dc:creator><pubDate>Thu, 06 Aug 2026 06:26:56 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/67f0239ff4f5a0b7b4a852c4/056ea70a-f3c9-40ad-8317-622ed56c082b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Capacitor is free and Despia costs $249 per app per OS. That comparison is true for about ten minutes, which is roughly how long it takes to discover that Capacitor is a library, not a delivery pipeline. The question worth answering is what each route costs once you add the store accounts, the over-the-air service, the build machine, and the person who has to keep a native project alive for a decade.</p>
<p>Here is the full arithmetic, with every number sourced and every assumption stated.</p>
<h2>The scenario</h2>
<p>Fixing the scenario matters more than the pricing pages, because both tools get cheap or expensive depending on who you are.</p>
<ul>
<li><p>You already have a web app. React, Vue, Svelte, or something a no-code tool generated. It is built and hosted.</p>
</li>
<li><p>You ship it to both the App Store and Google Play.</p>
</li>
<li><p>It has 1,000 monthly active users.</p>
</li>
<li><p>You release web changes maybe six times a year.</p>
</li>
<li><p>You are not an iOS or Android developer, and you do not want to become one.</p>
</li>
</ul>
<p>That last line is the one that moves the total by thousands of dollars, and it is the line most cost comparisons leave out.</p>
<h2>Costs both routes pay</h2>
<p>These are identical either way, so they are not the argument. Listing them anyway, because a total that hides them is not a total.</p>
<table>
<thead>
<tr>
<th>Line item</th>
<th>Cost</th>
</tr>
</thead>
<tbody><tr>
<td>Apple Developer Program</td>
<td>$99 per year</td>
</tr>
<tr>
<td>Google Play Console</td>
<td>$25 one time</td>
</tr>
<tr>
<td>Web hosting</td>
<td>Whatever you already pay</td>
</tr>
</tbody></table>
<p>Both routes ship under your own developer accounts. Neither tool holds your listing.</p>
<h2>What Capacitor actually costs</h2>
<p>Capacitor itself is MIT licensed and free forever. That is real and worth saying plainly. What is not free is everything Capacitor deliberately leaves to you.</p>
<p><strong>Over-the-air updates.</strong> Capacitor has no update service. The standard answer is <a href="https://capgo.app/pricing/">Capgo</a>, and it is a good product. At 1,000 MAU you need the Solo plan, which is $12 a month billed annually at $146, and includes 2,000 MAU, 100 GiB of bandwidth, unlimited live updates, and one build hour a month. There is also a credits-only pay-as-you-go option at $0.003 per MAU plus $0.06 per GiB, which for a small app lands closer to $40 to $60 a year if you are willing to manage prepaid credits instead of a plan. Either way it is a subscription, and it is metered on how many people use your app.</p>
<p><strong>A build machine.</strong> Historically this meant buying a Mac. It no longer has to. Capgo Native Build runs macOS M4 machines and includes App Store publishing on every plan, so the Mac requirement is gone if you are already paying for Capgo. Solo includes one build hour a month, with overage at $0.08 per build minute. For a two-platform release cycle that is roughly two to four full builds a month, which is enough for most small apps.</p>
<p><strong>The person who maintains the native project.</strong> This is the line item that does not go away, and it is where the money is.</p>
<p>Capacitor generates <code>ios/</code> and <code>android/</code> folders that are yours to keep. That is the whole design. It means you own the Podfile, the Gradle config, the Info.plist, the Android manifest, the plugin versions, and the target SDK. Capgo's own documentation is explicit about the boundary: changes to <code>capacitor.config.ts</code>, native plugin configuration, native package installs or upgrades, and anything requiring <code>npx cap sync</code> all need a native app release, not a live update.</p>
<p>Two of those releases are mandatory every year whether your product changes or not:</p>
<ul>
<li><p>Since <a href="https://developer.apple.com/news/upcoming-requirements/">April 28, 2026</a>, every upload to App Store Connect must be built with Xcode 26 and the iOS 26 SDK. Apple resets this baseline annually.</p>
</li>
<li><p>From <a href="https://developer.android.com/google/play/requirements/target-sdk">August 31, 2026</a>, new Google Play submissions and updates must target Android 16 (API 36), and apps that fall below API 35 stop being discoverable to new users on newer devices. Google runs this on an annual clock too.</p>
</li>
</ul>
<p>If you cannot do that work, you hire it. Rates for Capacitor finalization and store submission run roughly like this:</p>
<table>
<thead>
<tr>
<th>Who you hire</th>
<th>Typical range</th>
<th>What it covers</th>
</tr>
</thead>
<tbody><tr>
<td>Offshore freelancer</td>
<td>$150 to $350</td>
<td>Native folders generated, icons and splash screens, base store config, binary upload</td>
</tr>
<tr>
<td>Experienced Capacitor freelancer</td>
<td>$400 to $800</td>
<td>Native plugin setup, provisioning profiles, certificates, keystores, review guidance</td>
</tr>
<tr>
<td>Boutique agency or senior US/EU dev</td>
<td>$900 to $1,500+</td>
<td>CI/CD pipeline, OTA service wiring, dedicated support through rejections</td>
</tr>
</tbody></table>
<p>Mobile developers generally bill $30 to $100 an hour, and a first store deployment is 4 to 12 hours of work. That is the setup fee. The part people do not budget for is that you come back to the same developer for two specific reasons, indefinitely.</p>
<p><strong>Native plugin changes.</strong> Adding Bluetooth, swapping push providers, wiring in a new SDK, or even changing the app icon all modify the native project and require a rebuilt binary. Expect $50 to $200 per update. This is not a bug in Capacitor's design, it is the design: the native project is yours, so changing it is your job.</p>
<p><strong>Annual OS breakers.</strong> Every year Apple and Google raise the submission floor and change privacy rules, and every year some plugin in your dependency tree stops compiling against the new SDK. Keeping a Capacitor app compiling and compliant runs $150 to $500 a year in general maintenance, and that is before you have shipped a single new feature.</p>
<p>Neither of those disappears in year three. They are the running cost of holding a native project you do not maintain yourself.</p>
<h2>What Despia actually costs</h2>
<p>$249 per app per OS, one time. One app on both stores is $498, paid once, for the life of the app.</p>
<p>Each license includes 200 cloud credits, and a build costs 5 credits, so that is 40 native builds per app per OS. A two-platform app starts with 80. At the release cadence in this scenario, two mandatory compliance rebuilds a year across both platforms plus the occasional icon or permission change, that is roughly a decade of releases before you buy a single build. If you do run out, builds come in packs of 10 for $25.</p>
<p>Some Despia users do burn through the whole allowance inside a year. That is almost always a workflow problem rather than a pricing one, because a build is for native changes and a web change that gets rebuilt instead of deployed over the air is a build spent for nothing. The table below still assumes a $25 pack every two years, which is deliberately pessimistic against an allowance that should cover the decade.</p>
<p>Builds are also flat rate. A build is a build: it is not metered by the minute, so a slow one costs the same as a fast one and there is no per-minute overage to model. Over-the-air updates are unlimited with no MAU metering. There is no plan, no renewal, and no line that scales with how many people open your app.</p>
<p>Web changes ship over the air the moment you deploy. Native changes still need a rebuild, exactly like Capacitor, because that is a store rule and not a tool limitation. The difference is what the two re-hire scenarios above cost you here.</p>
<p>A native plugin change, meaning a new capability, a different push provider, or a new icon, is a toggle in the dashboard and one build. That build spends 5 of the 200 credits your license came with, against $50 to $200 on the other route.</p>
<p>The annual OS breaker is not your line item at all. There is no <code>ios/</code> folder in your repo to migrate to Xcode 26 and no <code>targetSdk</code> in a Gradle file you own. Keeping the runtime compiling against the current Apple and Google SDKs is the thing you bought, and it is covered for the life of the license.</p>
<p>Adding a native capability does not touch a native project either:</p>
<pre><code class="language-javascript">const isDespia = navigator.userAgent.toLowerCase().includes('despia')

if (isDespia) despia('successhaptic://')
</code></pre>
<p>No <code>npm install</code>, no <code>npx cap sync</code>, no native release to add a feature that was already compiled into the runtime.</p>
<h2>The ten-year totals</h2>
<p>Assumptions: one app on both stores, 1,000 MAU, six web releases a year, two mandatory native compliance rebuilds a year, Capgo Solo at the annual rate. Freelancer pricing taken at the middle of each range above: $600 for initial setup, $300 a year for OS compliance maintenance, and one native plugin change a year at $125. The Despia column assumes a $25 build pack every two years, which is conservative given two licenses already cover 80 builds. That is $425 a year in developer time, which is a conservative read of a $150 to $500 maintenance band plus $50 to $200 per native change.</p>
<table>
<thead>
<tr>
<th></th>
<th>Despia</th>
<th>Capacitor, you own the native project</th>
<th>Capacitor, you hire it out</th>
</tr>
</thead>
<tbody><tr>
<td>Year 1</td>
<td>$622</td>
<td>$270</td>
<td>$870</td>
</tr>
<tr>
<td>5 years</td>
<td>$1,068</td>
<td>$1,250</td>
<td>$3,550</td>
</tr>
<tr>
<td>10 years</td>
<td>$1,638</td>
<td>$2,475</td>
<td>$6,900</td>
</tr>
<tr>
<td>Recurring per year after setup</td>
<td>$99 store fee, plus about $13 a year in builds</td>
<td>$245</td>
<td>$670</td>
</tr>
<tr>
<td>Your hours, year one</td>
<td>Under 2</td>
<td>20 to 30</td>
<td>0</td>
</tr>
<tr>
<td>Your hours, per year after</td>
<td>Under 2</td>
<td>8 to 12</td>
<td>0</td>
</tr>
</tbody></table>
<p>Three things fall out of that table.</p>
<p><strong>The Capacitor DIY column is genuinely cheap in cash and expensive in hours.</strong> If you already write code, own the native project, and do not mind a yearly Xcode migration, Capacitor at $245 a year is a defensible number. Price your own 8 to 12 hours a year at anything above $30 an hour and the cash advantage disappears, but that is your call to make, not a number anyone can make for you.</p>
<p><strong>The hired column is the one most no-code builders actually land in</strong>, because the whole reason they chose a web stack is that they do not write Swift. At $6,900 over ten years, the free library is the most expensive option on the table, and it stays expensive because the maintenance never ends.</p>
<p><strong>Despia's cost is front-loaded and then nearly flat.</strong> After the license, the recurring lines are the $99 Apple fee that every route pays and about $13 a year in builds, and that build figure is a pessimistic placeholder rather than a bill you should expect.</p>
<h2>The line that actually diverges: growth</h2>
<p>The 1,000 MAU comparison is the friendly one for a metered service. Watch what happens when the app works.</p>
<table>
<thead>
<tr>
<th>Monthly active users</th>
<th>Capgo annual</th>
<th>Despia annual</th>
</tr>
</thead>
<tbody><tr>
<td>1,000</td>
<td>$146 (Solo)</td>
<td>$0</td>
</tr>
<tr>
<td>10,000</td>
<td>$396 (Maker)</td>
<td>$0</td>
</tr>
<tr>
<td>100,000</td>
<td>$998 (Team)</td>
<td>$0</td>
</tr>
<tr>
<td>1,000,000</td>
<td>$2,490+ (Enterprise)</td>
<td>$0</td>
</tr>
</tbody></table>
<p>Capgo counts MAU as distinct devices contacting Capgo in a rolling 30-day window, and the same device counts separately per native app ID. Despia does not meter update delivery at all. Over ten years, that is the difference between a cost that grows with your success and a cost that was fixed the day you bought it.</p>
<h2>Why Despia costs more on day one</h2>
<p>Two reasons, and neither is margin.</p>
<p>The first is that you are buying a runtime, not a library. Despia ships a compiled native layer with 50-plus device capabilities already in it, so the price includes the work that Capacitor pushes to plugin selection, native project maintenance, and your yearly SDK migration. You are paying up front for the job you would otherwise pay a freelancer to do twice a year.</p>
<p>The second is that it is a license, not a subscription. The $249 is the whole software cost, decided once, at a price that does not move when your user count does. That is a worse deal for someone who ships an app, gets 40 users, and abandons it. It is a much better deal for anyone whose app survives.</p>
<p>The obvious objection stands: a Despia app runs your web code in WKWebView and the Android WebView, and that is not SwiftUI. True. The answer is the 50-plus native capabilities exposed through one JavaScript call, and the fact that the full Xcode and Android Studio projects export whenever you want them. The license is not a cage.</p>
<h2>When Capacitor is the right call</h2>
<p>Plainly, because a comparison that never concedes anything is an ad.</p>
<ul>
<li><p>You have an in-house mobile developer, or you are one. The native project is an asset in that case, not a liability, and the cash cost is low.</p>
</li>
<li><p>You need custom native code or a specific plugin that is not in Despia's feature set. Capacitor's plugin ecosystem is large and open, and writing your own plugin is a supported path.</p>
</li>
<li><p>You want the entire stack auditable and self-hostable. Capacitor is MIT, and Capgo's plugin and backend are open source under MIT and MPL-2.0, which matters in regulated environments.</p>
</li>
<li><p>You are already on Capacitor and shipping fine. Migration is a cost too, and "it works" is a real reason.</p>
</li>
</ul>
<h2>Common questions</h2>
<p><strong>Does Despia charge for each store update?</strong> No. Web changes ship over the air with no store review and no per-update fee. Native changes need a rebuild on any route, and on Despia it spends 5 credits from the 200 each license includes, which is 40 builds per app per OS. Beyond that, builds are $25 per pack of 10, flat rate with no per-minute metering. Annual OS compliance is covered by the license rather than billed as maintenance.</p>
<p><strong>Is Capgo required with Capacitor?</strong> No, but something is. Capacitor has no update mechanism of its own, so without an OTA service every web change becomes a store release with a review wait.</p>
<p><strong>Can I leave Despia later?</strong> Yes. The Xcode project and Android Studio project export at any time, and the app is registered under your own Apple and Google accounts throughout.</p>
<p><strong>Does the 1,000 MAU assumption favor Despia?</strong> It favors Capgo, actually. Solo is the cheapest tier and 1,000 users sits at half its cap. The Despia advantage widens at every tier above it.</p>
<p><strong>What if I already own a Mac and know Xcode?</strong> Then run the middle column. Capacitor at $245 a year is a fair price for a route where the maintenance is already inside your skill set.</p>
<h2>Ship one app, not two</h2>
<p>If you are going to keep building in your web stack, keep building there. Despia ships that app to the App Store and Google Play, gives it native device access, and updates it over the air, from the one codebase you already have.</p>
<p><a href="https://setup.despia.com">Learn more about Despia in the docs</a> or <a href="https://despia.com">start building at despia.com</a>.</p>
]]></content:encoded></item></channel></rss>