9.7 KiB
Part 1 – Turn your current app into a PWA
You already have:
index.htmlstyle.cssscript.js
We’ll add:
manifest.webmanifestsw.js- A couple of small changes to
index.html
I’ll assume your app is served from the site root (e.g. https://mydomain.com/) and that your icon is a PNG. You can keep everything inside web/ in your repo; when you deploy, make sure these files end up in the same directory as index.html.
1. Create icons from your existing icon file
Chrome expects at least:
- 192×192 PNG
- 512×512 PNG
If your icon file is large enough (e.g. 1024×1024), generate:
icons/icon-192.pngicons/icon-512.png
(Use something like GIMP, Figma, or an online “PWA icon generator”.)
Folder structure (in web/):
web/
index.html
style.css
script.js
sw.js
manifest.webmanifest
icons/
icon-192.png
icon-512.png
2. Add manifest.webmanifest
Create web/manifest.webmanifest:
{
"name": "Life Manager",
"short_name": "LifeMgr",
"description": "Offline-first life organizer with calendar, tags, and backups.",
"start_url": "/",
"scope": "/",
"display": "standalone",
"background_color": "#0f1014",
"theme_color": "#0f1014",
"orientation": "portrait",
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
]
}
start_urlandscopeassume the app is at the root. If you serve from/life-manager/, change both to"/life-manager/".
3. Wire manifest + theme color into index.html
Modify your <head> in web/index.html to include the manifest and theme color.
Right now you have:
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>life-manager</title>
<link rel="stylesheet" href="style.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
</head>
Change to:
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>life-manager</title>
<!-- PWA: theme color (matches manifest) -->
<meta name="theme-color" content="#0f1014">
<!-- PWA: manifest -->
<link rel="manifest" href="manifest.webmanifest">
<link rel="stylesheet" href="style.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
</head>
4. Register a service worker from index.html
Just before </body> you already have:
<script src="script.js"></script>
</body>
</html>
Change to:
<script src="script.js"></script>
<!-- PWA: Service worker registration -->
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.catch(err => {
console.error('SW registration failed', err);
});
});
}
</script>
</body>
</html>
If you host the app under a path (e.g.
/life-manager/), change the registration path to'/life-manager/sw.js'.
5. Add sw.js (service worker)
Create web/sw.js:
// sw.js - simple cache-first service worker
const CACHE_NAME = 'lifemgr-cache-v1';
// List everything needed for offline
const ASSETS = [
'/',
'/index.html',
'/style.css',
'/script.js',
'/manifest.webmanifest',
'/icons/icon-192.png',
'/icons/icon-512.png'
];
// Install: cache core assets
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(ASSETS);
})
);
self.skipWaiting();
});
// Activate: cleanup old caches if you bump the version
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(
keys.map((key) => {
if (key !== CACHE_NAME) {
return caches.delete(key);
}
})
)
)
);
self.clients.claim();
});
// Fetch: cache-first strategy
self.addEventListener('fetch', (event) => {
const request = event.request;
// Only handle GET
if (request.method !== 'GET') return;
event.respondWith(
caches.match(request).then((cached) => {
// Serve from cache if available, else network
return (
cached ||
fetch(request).catch(() => {
// Optional: custom offline response for HTML requests
if (request.headers.get('accept')?.includes('text/html')) {
return caches.match('/index.html');
}
})
);
})
);
});
Again, if hosted under a sub-path, prepend
/life-managerinASSETS.
Now when you deploy over HTTPS, Chrome will see:
- a valid manifest,
- a service worker controlling the scope,
- and will treat it as a PWA with install capability.
6. Deploy and sanity-check
-
Deploy
web/to somewhere HTTPS (Netlify, Vercel, etc.). -
Open it in Chrome (Android or desktop).
-
In DevTools → Application → Manifest/Service Workers:
- Confirm manifest is found,
- service worker is active,
- and pages are controlled.
-
On Android Chrome, you should see “Install app” / “Add to Home screen” in the menu.
Once this works, you’re ready for the Trusted Web Activity step.
Part 2 – Wrap your PWA in a Trusted Web Activity (TWA) with Bubblewrap
Now you have a real PWA live at (for example):
https://your-domain.com/
We’ll use Bubblewrap, which takes your hosted manifest.webmanifest and generates an Android project that starts your PWA as a TWA.
1. Prereqs checklist
You need:
-
Node.js (v14+)
-
Java JDK
-
Android SDK
-
Your PWA served via HTTPS with:
- manifest,
- service worker,
- and working offline (which we just did).
And of course, a Google Play developer account for publishing later.
2. Install Bubblewrap CLI
npm install -g @bubblewrap/cli
bubblewrap --version
(Just to confirm it’s installed.)
3. Initialize a Bubblewrap project from your manifest
Pick a new folder for the Android project:
mkdir life-manager-twa
cd life-manager-twa
Run the init command, pointing to your hosted manifest:
bubblewrap init --manifest https://your-domain.com/manifest.webmanifest
Bubblewrap will:
-
Download the web manifest.
-
Ask you about:
- Package ID (e.g.
com.yourdomain.lifemanager) - App name (can reuse
"Life Manager") - Colors, icons, orientation (it can reuse values from the manifest)
- Signing key (create a new keystore or use an existing one)
- Package ID (e.g.
It will generate:
- An Android project (Gradle, Java/Kotlin code that opens your URL as a TWA).
- A
twa-manifest.jsonwith config about your app. - An
assetlinks.jsonfile template for your website.
4. Build the APK/App Bundle
Inside that new project folder:
bubblewrap build
This compiles the Android project and outputs a release build (APK/App Bundle).
You can also install directly on a connected device:
bubblewrap install
At this stage, when you open the app on the device:
- It will usually open as a Custom Tab (with a little browser UI) until we set up the trust link (Digital Asset Links).
5. Set up Digital Asset Links (assetlinks.json)
To get true Trusted Web Activity (full screen, no browser bar), Android needs to verify that:
- The app signing key
- And your website’s domain
belong to the same owner, via Digital Asset Links.
Bubblewrap will give you an assetlinks.json snippet. If you need a template, it looks like this:
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.yourdomain.lifemanager",
"sha256_cert_fingerprints": [
"AA:BB:CC:DD:EE:FF:..."
]
}
}
]
- Replace
com.yourdomain.lifemanagerwith your actual package ID. - Replace the fingerprint with the SHA-256 of the keystore you actually sign the Play build with (Bubblewrap/Android Studio docs show how to get this).
Host this file at:
https://your-domain.com/.well-known/assetlinks.json
Once this is accessible and valid:
- Opening your app on device will show your PWA full-screen as a Trusted Web Activity, not a normal Chrome tab.
6. Open in Android Studio (optional but recommended)
You can open the generated Android project in Android Studio for:
- Changing app name, icon, version code, etc.
- Building signed App Bundles for the Play Store.
- Running on emulators.
Bubblewrap’s build and install commands already use Gradle under the hood, so this is optional, but very handy for Play Store upload.
7. Upload to Google Play
Once:
- PWA runs well over HTTPS.
- Asset Links are configured and verified.
- The app works as full-screen TWA on a device.
Then:
- In the Google Play Console, create a new app.
- Upload the signed App Bundle/APK from your Bubblewrap/Android Studio build.
- Fill in listing details (screenshots, description, etc.).
- Roll out a test track or production release.