How to Publish Your App Without the App Stores, Using Back4app

Your app can be on someone's home screen this afternoon, with an icon and no address bar, and nobody has to approve it. Three files do it — and one of them is the one everybody leaves out.

A timeline drawn to scale comparing two routes to the same phone home screen: the store route spends 14 days on a closed test with 12 testers and then up to seven days of review, reaching the phone on day 21 and only if it passes, while the web route reaches it on day 0 as soon as three files are served over HTTPS

You finished the app. Now you want it on a phone, and the two front doors both have a queue.

Google Play asks personal developer accounts for a closed test: at least 12 testers, opted in continuously for 14 days, before you may even apply for production access. That application review then “usually takes seven days or less”.

Apple asks a different question, and guideline 4.2 states it plainly: your app “should include features, content, and UI that elevate it beyond a repackaged website”.

There is a third door, and it has no queue at all. A web app can sit on the home screen with its own icon, open full screen with no address bar, and keep working with no connection. This guide builds one, then proves both halves of that claim against a real browser.

Watch it instead

The app stores are number 13 in our countdown of what actually goes wrong for people building with AI.

Video: We Analyzed 130,000 Vibe Coder Posts. These Are the Problems That Actually Hurt.
Watch it instead We Analyzed 130,000 Vibe Coder Posts. These Are the Problems That Actually Hurt.
From the Back4app channel. This guide is the long version of its first position; the other twelve are in the companion post. Open on YouTube →

What are the stores actually asking for?

Two different things, which is why the same app can stall at both for unrelated reasons.

Google’s is arithmetic. Developers with personal accounts created after 13 November 2023 must run a closed test with “a minimum of 12 testers who have been opted in continuously for at least 14 days”. Opting out resets the clock for that person, so the twelve have to stay. Organization accounts skip the test entirely.

Apple’s is editorial. Guideline 4.2.2 adds that apps “shouldn’t primarily be marketing materials, advertisements, web clippings, content aggregators, or a collection of links”. A site wrapped in a native shell is the exact case the sentence describes, so a reviewer reading it has already been told what to look for.

Before anyone can install it
12testers, for 14 days
→
7days of review, usually
$99a year, on the other store
Google's figures are from its own Play Console documentation. Apple charges annually for the developer programme whether or not an app is accepted.

The web route asks for none of that. It also asks for no money, no account and no appeal process, because there is nobody to appeal to.

What makes a web page installable?

A manifest file. That is genuinely the whole mechanism: a small JSON document that tells the browser what the app is called, which icon to use and that it should open in its own window.

{
  "name": "Shift Notes",
  "short_name": "Shift Notes",
  "start_url": "./index.html",
  "scope": "./",
  "display": "standalone",
  "background_color": "#F4F4EF",
  "theme_color": "#0A1220",
  "icons": [
    { "src": "icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" },
    { "src": "icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" },
    { "src": "icons/icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
  ]
}

Link it from the page and you are done with this part:

<link rel="manifest" href="manifest.webmanifest" />

display: standalone is the line that removes the address bar. Without it you get a bookmark; with it you get something a person will describe as an app, because the thing on screen has no browser in it.

What does the browser really require?

Less than the checklists say. We asked Chrome itself, one change at a time, using its own installability verdict rather than a blog post’s summary.

What we changedChrome’s verdict
icons at 192 and 512installable
only a 192 iconinstallable
only a 512 iconinstallable
no icons at allmanifest-missing-suitable-icon, no-acceptable-icon
display: browsermanifest-display-not-supported
no short_nameinstallable

So one suitable icon is enough, and the much-repeated “you need 192 and 512” is advice about how your icon looks on different screens, not a condition of installing.

One more thing worth knowing before you start fixing things. With three faults present at once — no short_name, no large icon and display: browser — Chrome named exactly one: manifest-display-not-supported. It reports the blocker it hit, not an audit. Fix it and the next one appears.

How we measured: each row is one edit to the manifest, then a fresh Chrome profile driven over the DevTools protocol, reading Page.getInstallabilityErrors on the loaded page. The script is in the repo.

How do you make it work with no connection?

A service worker, which is a script the browser keeps even when your page is closed. On install it copies the app’s shell into a cache; afterwards it answers requests from that cache first.

const VERSION = 'shift-notes-v1';
const SHELL = ['./', './index.html', './manifest.webmanifest', './icons/icon-192.png', './icons/icon-512.png'];

self.addEventListener('install', (e) => {
  e.waitUntil(caches.open(VERSION).then((c) => c.addAll(SHELL)).then(() => self.skipWaiting()));
});

self.addEventListener('fetch', (e) => {
  if (e.request.method !== 'GET') return;
  e.respondWith(
    caches.match(e.request, { ignoreSearch: true })
      .then((hit) => hit || fetch(e.request).catch(() => caches.match('./index.html')))
  );
});

Register it from the page, once it has loaded:

if ('serviceWorker' in navigator) {
  addEventListener('load', () => navigator.serviceWorker.register('sw.js'));
}

The catch on the last line matters more than it looks. When the network is gone and the request was a navigation, it hands back the cached page instead of letting the browser show its own error — which is exactly the difference the next section measures.

Does it actually survive losing the network?

Yes, and we checked it the hard way. Cutting the browser’s network is not enough on its own, because a server that is still running can still be answering from somewhere. So the test cuts the network and stops the server, then confirms from outside the browser that the origin is really dead before reloading.

✓ Chrome reports no installability errors    none
✓ a service worker is active                 state=activated
✓ the page is controlled by it               controller=true
✓ the shell is in the cache                  5 entries
✓ the origin is genuinely gone               ECONNREFUSED
✓ it still renders with the server stopped   h1="Shift Notes"
✓ it is still interactive offline            form and list present

first load            4430 bytes over the wire, 18 ms to load event
offline reload        0 bytes over the wire

Fourteen checks, all passing, stable across three consecutive runs. The number that carries the claim is the last one: 0 bytes over the wire on a full reload, while a plain fetch to the same origin from outside the browser comes back ECONNREFUSED.

How we measured: Node 22 drives a real Chrome over the DevTools protocol, with a fresh profile each run. Network.emulateNetworkConditions puts the page offline, the static server is closed rather than paused, and the reload is a normal one, not a cache-only load.

Installable and works-offline are two different properties, and only one of them is what a person means by “it’s an app”.

What happens if you skip the service worker?

Chrome installs the app anyway — and that is the trap. We deleted one line, the registration, and ran the same checks.

Chrome’s installability verdict did not change: no errors. It would still offer to install it, still give it an icon, still open it full screen with no address bar.

Then the offline reload landed on Chrome’s own error page. Our script reads the first heading on the page, and it came back h1="Press space to play" — the dinosaur game. The reload also pulled 184,663 bytes over a connection that was supposed to be dead, because that error page is itself a page.

The same app installed twice, side by side on a phone, with the network cut and the server stopped: with the service worker it shows Shift Notes and its three saved notes, and without it the screen is Chrome's offline error page showing the dinosaur and Press space to play

So the service worker is not the optional polish step. It is the file that makes the install honest.

What does this cost you on an iPhone?

Three concrete things, and you should know them before you promise anyone a smooth install.

There is no install prompt. Safari shows no banner and fires no event you can hook; the reader has to open the Share menu and choose Add to Home Screen. That means your app has to ask for it in its own interface, at a moment when the reader already wants it.

Push notifications work, but only afterwards. They arrived in iOS 16.4 and only for a web app already on the Home Screen — a site in a Safari tab cannot receive push, whatever permission it holds. Background sync is still not available, so anything that must happen while the app is closed needs to happen on your server instead.

None of that stops the app working. It changes what you can promise: an icon, a full-screen window and offline use are yours; silent background behaviour is not.

When should you use a store anyway?

When one of four things is true, and it is worth being honest about them before you commit.

You need the store’s payment system, because selling digital goods inside an iOS app is not something you can route around. You need deep hardware access — Bluetooth peripherals, background location, health data. You are counting on store search to find you users. Or your buyer’s procurement process simply requires a listing.

If none of those is true, the store is mostly a gate you are asking to be let through. And if one of them becomes true later, the backend you build now does not change — the same accounts and the same API serve a native client whenever you write one.

Where does the backend come in?

The moment two people need the same data. The app in the repo keeps notes in localStorage, which is honest for a demo and wrong for a product: it is per-device, per-browser, invisible to you, and gone when someone clears their cache.

That is the same wall the store route hits, just without the review queue in front of it. Accounts, a database, file storage and an API are what turn the thing on the home screen into something two phones can share.

A managed backend covers that without changing the three files you just wrote: sign-up and login, a database with per-record permissions, and REST and GraphQL endpoints your page can call with fetch.

Adding sign-up and login is the next step, and it is the same work whether this ends up in a store or never does.

How do you put it online?

Serve the folder over HTTPS. That is the only hard requirement, and it is not negotiable: a service worker will not register over plain HTTP anywhere except localhost, so without a certificate the offline half simply does not exist.

Any static host with HTTPS will do, and a container will too — hosting a static site in a container measures that route end to end, including the file that serves it.

Then check the thing you shipped rather than the thing you wrote. Install it from the browser, turn the network off, and reload. If you see your app, you are done. If you see a dinosaur, your service worker is not registered.

What should you do first?

Open your app’s folder and add the manifest. It is the smallest change with the largest visible effect — one file and one <link>, and the browser starts offering to install something that used to be a bookmark.

Then add the service worker, because the install without it is a promise you have not kept. Then run the checks against a real browser instead of a checklist, since the checklist was wrong about icons and silent about the thing that actually breaks.

The stores will still be there if you need them. The difference is that this route puts your app on a home screen today, and nobody has to say yes.

Tagspwaweb-appapp-storeservice-workerofflinedeployment

Frequently asked questions

Do I need an app store to publish an app?

No. A web app can be installed to a phone's home screen, open full screen with its own icon and no address bar, and work with no connection. Nobody reviews it and nobody can remove it. What you give up is store discovery, the stores' payment systems, and a handful of native capabilities — most visibly background sync on iOS.

Why does Google Play need 12 testers?

Google requires personal developer accounts created after 13 November 2023 to run a closed test with at least 12 testers opted in continuously for at least 14 days before they can apply for production access. The application review then usually takes seven days or less. Organization accounts are not subject to the test.

Why does Apple reject apps that are just a website?

App Review guideline 4.2 says an app should include features, content and UI that elevate it beyond a repackaged website, and 4.2.2 says apps shouldn't primarily be web clippings or a collection of links. Wrapping a site in a shell and submitting it is the case the guideline names, so it is the case reviewers look for.

Is an installable web app the same as one that works offline?

No, and this is the trap. We removed the service worker registration from an app and Chrome still reported no installability errors — it would install it. The offline reload then landed on Chrome's own error page, which our script caught as a heading reading Press space to play. Installability is about the manifest; working offline is about the service worker.

Can an installed web app send push notifications on an iPhone?

Yes, since iOS 16.4, but only once the site has been added to the Home Screen — a site open in a Safari tab cannot receive push. There is no install prompt on iOS either: the reader has to use Share, then Add to Home Screen, so the app has to ask for that in its own interface.

Where does the backend fit in a web app with no store?

Exactly where it would in a native one. The three files here keep notes in localStorage, which is per-device and per-browser, so nothing is shared and nothing survives a cleared cache. Accounts, a database and an API replace that without changing the three files, and the same backend serves a native app later if you ever do go to a store.

Further reading

Reviewed & verified by Back4app Engineering, Editorial review at Back4app

Tested Oct 27, 2026 — Browser: Chrome 155.0.8059.85 on macOS, arm64 · App: three static files, no build step · Harness: Node 22 over the DevTools protocol · Backend: Back4app, free plan.

Changelog

  • Oct 28, 2026 — The companion repository is not published yet, so the link to it and the button were removed; they come back when it is.
  • Oct 28, 2026 — The companion repository is published, so the button and the inline link to it are back.

More from Back4app Engineering

Containers · tutorial

How to Host a Static Website on Back4app Containers With Docker and nginx [Full Repo]

Backend · tutorial

How to Add Authentication to a Web or Flutter App Without Writing an Auth Server [Full Repo]

Backend · report

Why Vibe-Coded Apps Break: 13 Problems From 130,000 Posts, Ranked, With the Fix for Each