Vibe Coding & Mobile
From Lovable App to Mobile PWA: Push Notifications with Supabase
You have built something brilliant with Lovable. Now your users want it on their phone, with push notifications. Here is exactly how to turn your Lovable web app into a fully installable PWA with real-time alerts, using Supabase as your backend.


Curated by Matt Perry
CTO
The Gap Between "It Works" and "It's on My Phone"
Lovable is brilliant at what it does. You describe what you want, and it builds you a working web app. React frontend, clean UI, functional logic. For a lot of use cases, that is enough.
But then someone asks: "Can I get this on my phone?"
They do not mean opening a browser and typing in a URL. They mean tapping an icon on their home screen, getting push notifications when something changes, and using it offline when they lose signal. They mean it should feel like a proper app.
The good news: you do not need to rebuild anything in Swift or Kotlin. You do not need to submit to app stores. Progressive Web Apps give you all of this, and Lovable apps are already 90% of the way there.
What Is a PWA, and Why Should You Care?
A Progressive Web App is a web application that uses modern browser APIs to deliver an app-like experience. When done right, your users get:
- Home screen installation with your app icon, no app store needed
- Push notifications that work even when the browser is closed
- Offline support through service worker caching
- Full-screen mode without browser chrome
- Automatic updates without users needing to do anything
For vibe-coded apps, this is transformative. You get mobile distribution without the overhead of native development, and your users get an experience that feels native.
Step 1: Add a Web App Manifest
The manifest tells browsers that your site can be installed as an app. Create a manifest.json in your public folder:
{
"name": "Your App Name",
"short_name": "AppName",
"description": "What your app does",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#383E48",
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}Link it in your HTML head:
<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#383E48" />The display: standalone setting is what makes your app launch without browser UI. Your users will see a full-screen app with your branding, not a web page.
Step 2: Register a Service Worker
Service workers are the engine behind PWA features. They sit between your app and the network, intercepting requests and managing caches. Create a service-worker.js in your public folder:
const CACHE_NAME = 'app-cache-v1';
const STATIC_ASSETS = [
'/',
'/index.html',
'/manifest.json'
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => cache.addAll(STATIC_ASSETS))
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((response) => response || fetch(event.request))
);
});Register it in your app's entry point:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js')
.then((reg) => console.log('SW registered:', reg.scope))
.catch((err) => console.error('SW registration failed:', err));
}This gives you basic offline support. Static assets load from cache, so your app works even without a connection.
Step 3: Set Up Supabase for Push Notifications
This is where it gets interesting. Supabase gives you a Postgres database, authentication, edge functions, and real-time subscriptions. Everything you need for push notifications without managing your own backend infrastructure.
Create a Supabase Project
Head to supabase.com and create a new project. You will need:
- Your project URL (e.g.
https://xxxxx.supabase.co) - Your anon key (safe for client-side use)
- Your service role key (server-side only, never expose this)
Set Up the Subscriptions Table
Create a table to store push notification subscriptions:
CREATE TABLE push_subscriptions (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
subscription JSONB NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
ALTER TABLE push_subscriptions ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users manage own subscriptions"
ON push_subscriptions FOR ALL
USING (auth.uid() = user_id);Row Level Security ensures users can only access their own subscription data. This is critical for production apps.
Generate VAPID Keys
Web push notifications use VAPID (Voluntary Application Server Identification) keys for authentication. Generate them once:
npx web-push generate-vapid-keysStore the public key in your frontend config and the private key as a Supabase secret:
supabase secrets set VAPID_PRIVATE_KEY="your-private-key"Step 4: Request Permission and Subscribe
In your React app, create a hook that handles the notification permission flow:
import { useEffect, useState } from 'react';
import { supabase } from './supabaseClient';
const VAPID_PUBLIC_KEY = 'your-public-key-here';
export function usePushNotifications() {
const [isSubscribed, setIsSubscribed] = useState(false);
async function subscribe() {
const permission = await Notification.requestPermission();
if (permission !== 'granted') return;
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: VAPID_PUBLIC_KEY
});
const { data: { user } } = await supabase.auth.getUser();
await supabase.from('push_subscriptions').upsert({
user_id: user.id,
subscription: subscription.toJSON()
});
setIsSubscribed(true);
}
return { isSubscribed, subscribe };
}Call subscribe() when the user taps an "Enable Notifications" button. Never request permission on page load. Users dismiss unexpected permission prompts and it becomes much harder to ask again.
Step 5: Send Notifications with Supabase Edge Functions
Create a Supabase Edge Function that sends push notifications. Run supabase functions new send-push and add:
import { serve } from 'https://deno.land/std/http/server.ts';
import webPush from 'npm:web-push';
import { createClient } from 'https://esm.sh/@supabase/supabase-js';
const vapidPublicKey = Deno.env.get('VAPID_PUBLIC_KEY');
const vapidPrivateKey = Deno.env.get('VAPID_PRIVATE_KEY');
webPush.setVapidDetails(
'mailto:hello@yourdomain.com',
vapidPublicKey,
vapidPrivateKey
);
serve(async (req) => {
const { userId, title, body } = await req.json();
const supabase = createClient(
Deno.env.get('SUPABASE_URL'),
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')
);
const { data: subs } = await supabase
.from('push_subscriptions')
.select('subscription')
.eq('user_id', userId);
const results = await Promise.allSettled(
subs.map((sub) =>
webPush.sendNotification(
sub.subscription,
JSON.stringify({ title, body })
)
)
);
return new Response(JSON.stringify({ sent: results.length }));
});Handle Incoming Notifications in the Service Worker
Add a push event listener to your service worker:
self.addEventListener('push', (event) => {
const data = event.data.json();
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: '/icons/icon-192.png',
badge: '/icons/badge-72.png'
})
);
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
event.waitUntil(
clients.openWindow('/')
);
});Step 6: Trigger Notifications from Database Changes
The real power comes from combining Supabase real-time with push notifications. Set up a database trigger that fires your edge function when data changes:
CREATE OR REPLACE FUNCTION notify_on_new_order()
RETURNS TRIGGER AS $$
BEGIN
PERFORM net.http_post(
url := 'https://xxxxx.supabase.co/functions/v1/send-push',
headers := jsonb_build_object(
'Authorization', 'Bearer ' || current_setting('app.settings.service_role_key')
),
body := jsonb_build_object(
'userId', NEW.user_id,
'title', 'New Order',
'body', 'You have a new order to review'
)
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER order_notification_trigger
AFTER INSERT ON orders
FOR EACH ROW
EXECUTE FUNCTION notify_on_new_order();Now when a new row hits your orders table, your user gets a push notification on their phone. No polling, no WebSocket connections to maintain, no third-party notification service.
Testing Your PWA
Before you share it with users, verify everything works:
- Lighthouse audit: Run a Lighthouse PWA audit in Chrome DevTools. Aim for a perfect PWA score
- Installation: Open your app in Chrome on Android or Safari on iOS. You should see an "Add to Home Screen" prompt
- Offline mode: Turn on aeroplane mode and reload. Your cached pages should still work
- Notifications: Send a test push from your Supabase dashboard. It should arrive even with the browser closed
A note on iOS: Safari added PWA push notification support in iOS 16.4, but the user must first add the app to their home screen. It will not work from the browser alone.
Common Pitfalls
HTTPS is mandatory. Service workers only run on secure origins. Lovable apps deployed to their default domain already have this. If you use a custom domain, make sure your SSL certificate is valid.
Do not cache everything. Be selective about what goes in your service worker cache. API responses with user data should not be cached, or you risk showing stale or another user's data.
Handle subscription expiry. Push subscriptions can expire or be revoked. Your edge function should handle failed sends gracefully and clean up dead subscriptions.
Test on real devices. The Chrome DevTools mobile simulator does not support push notifications or service worker installation prompts. Test on actual phones.
What You End Up With
Following this guide, your Lovable app transforms from a web page into something that genuinely feels like a mobile app:
- Users tap an icon on their home screen to open it
- It loads instantly from cache, even on slow connections
- They get real-time push notifications driven by database events
- No app store submission, no review process, no 30% commission
- Updates deploy instantly because it is still a web app
The entire stack, Lovable for the frontend, Supabase for the backend, and standard Web APIs for the PWA features, costs nothing until you have real users. And when you do, it scales with Supabase's infrastructure rather than your own servers.
Need Help Making It Production Ready?
Turning a vibe-coded app into something you can rely on takes more than just adding a manifest file. You need proper error handling, security hardening, monitoring, and a deployment pipeline that does not involve copying and pasting.
That is exactly what our Vibe Coding Production Support service covers. We take your Lovable, Bolt, or Cursor app and make it production grade, so you can focus on what your app does rather than worrying about whether it will stay up.

Curated by Matt Perry
CTO
The Gap Between "It Works" and "It's on My Phone"
Lovable is brilliant at what it does. You describe what you want, and it builds you a working web app. React frontend, clean UI, functional logic. For a lot of use cases, that is enough.
But then someone asks: "Can I get this on my phone?"
They do not mean opening a browser and typing in a URL. They mean tapping an icon on their home screen, getting push notifications when something changes, and using it offline when they lose signal. They mean it should feel like a proper app.
The good news: you do not need to rebuild anything in Swift or Kotlin. You do not need to submit to app stores. Progressive Web Apps give you all of this, and Lovable apps are already 90% of the way there.
What Is a PWA, and Why Should You Care?
A Progressive Web App is a web application that uses modern browser APIs to deliver an app-like experience. When done right, your users get:
- Home screen installation with your app icon, no app store needed
- Push notifications that work even when the browser is closed
- Offline support through service worker caching
- Full-screen mode without browser chrome
- Automatic updates without users needing to do anything
For vibe-coded apps, this is transformative. You get mobile distribution without the overhead of native development, and your users get an experience that feels native.
Step 1: Add a Web App Manifest
The manifest tells browsers that your site can be installed as an app. Create a manifest.json in your public folder:
{
"name": "Your App Name",
"short_name": "AppName",
"description": "What your app does",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#383E48",
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}Link it in your HTML head:
<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#383E48" />The display: standalone setting is what makes your app launch without browser UI. Your users will see a full-screen app with your branding, not a web page.
Step 2: Register a Service Worker
Service workers are the engine behind PWA features. They sit between your app and the network, intercepting requests and managing caches. Create a service-worker.js in your public folder:
const CACHE_NAME = 'app-cache-v1';
const STATIC_ASSETS = [
'/',
'/index.html',
'/manifest.json'
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => cache.addAll(STATIC_ASSETS))
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((response) => response || fetch(event.request))
);
});Register it in your app's entry point:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js')
.then((reg) => console.log('SW registered:', reg.scope))
.catch((err) => console.error('SW registration failed:', err));
}This gives you basic offline support. Static assets load from cache, so your app works even without a connection.
Step 3: Set Up Supabase for Push Notifications
This is where it gets interesting. Supabase gives you a Postgres database, authentication, edge functions, and real-time subscriptions. Everything you need for push notifications without managing your own backend infrastructure.
Create a Supabase Project
Head to supabase.com and create a new project. You will need:
- Your project URL (e.g.
https://xxxxx.supabase.co) - Your anon key (safe for client-side use)
- Your service role key (server-side only, never expose this)
Set Up the Subscriptions Table
Create a table to store push notification subscriptions:
CREATE TABLE push_subscriptions (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
subscription JSONB NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
ALTER TABLE push_subscriptions ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users manage own subscriptions"
ON push_subscriptions FOR ALL
USING (auth.uid() = user_id);Row Level Security ensures users can only access their own subscription data. This is critical for production apps.
Generate VAPID Keys
Web push notifications use VAPID (Voluntary Application Server Identification) keys for authentication. Generate them once:
npx web-push generate-vapid-keysStore the public key in your frontend config and the private key as a Supabase secret:
supabase secrets set VAPID_PRIVATE_KEY="your-private-key"Step 4: Request Permission and Subscribe
In your React app, create a hook that handles the notification permission flow:
import { useEffect, useState } from 'react';
import { supabase } from './supabaseClient';
const VAPID_PUBLIC_KEY = 'your-public-key-here';
export function usePushNotifications() {
const [isSubscribed, setIsSubscribed] = useState(false);
async function subscribe() {
const permission = await Notification.requestPermission();
if (permission !== 'granted') return;
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: VAPID_PUBLIC_KEY
});
const { data: { user } } = await supabase.auth.getUser();
await supabase.from('push_subscriptions').upsert({
user_id: user.id,
subscription: subscription.toJSON()
});
setIsSubscribed(true);
}
return { isSubscribed, subscribe };
}Call subscribe() when the user taps an "Enable Notifications" button. Never request permission on page load. Users dismiss unexpected permission prompts and it becomes much harder to ask again.
Step 5: Send Notifications with Supabase Edge Functions
Create a Supabase Edge Function that sends push notifications. Run supabase functions new send-push and add:
import { serve } from 'https://deno.land/std/http/server.ts';
import webPush from 'npm:web-push';
import { createClient } from 'https://esm.sh/@supabase/supabase-js';
const vapidPublicKey = Deno.env.get('VAPID_PUBLIC_KEY');
const vapidPrivateKey = Deno.env.get('VAPID_PRIVATE_KEY');
webPush.setVapidDetails(
'mailto:hello@yourdomain.com',
vapidPublicKey,
vapidPrivateKey
);
serve(async (req) => {
const { userId, title, body } = await req.json();
const supabase = createClient(
Deno.env.get('SUPABASE_URL'),
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')
);
const { data: subs } = await supabase
.from('push_subscriptions')
.select('subscription')
.eq('user_id', userId);
const results = await Promise.allSettled(
subs.map((sub) =>
webPush.sendNotification(
sub.subscription,
JSON.stringify({ title, body })
)
)
);
return new Response(JSON.stringify({ sent: results.length }));
});Handle Incoming Notifications in the Service Worker
Add a push event listener to your service worker:
self.addEventListener('push', (event) => {
const data = event.data.json();
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: '/icons/icon-192.png',
badge: '/icons/badge-72.png'
})
);
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
event.waitUntil(
clients.openWindow('/')
);
});Step 6: Trigger Notifications from Database Changes
The real power comes from combining Supabase real-time with push notifications. Set up a database trigger that fires your edge function when data changes:
CREATE OR REPLACE FUNCTION notify_on_new_order()
RETURNS TRIGGER AS $$
BEGIN
PERFORM net.http_post(
url := 'https://xxxxx.supabase.co/functions/v1/send-push',
headers := jsonb_build_object(
'Authorization', 'Bearer ' || current_setting('app.settings.service_role_key')
),
body := jsonb_build_object(
'userId', NEW.user_id,
'title', 'New Order',
'body', 'You have a new order to review'
)
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER order_notification_trigger
AFTER INSERT ON orders
FOR EACH ROW
EXECUTE FUNCTION notify_on_new_order();Now when a new row hits your orders table, your user gets a push notification on their phone. No polling, no WebSocket connections to maintain, no third-party notification service.
Testing Your PWA
Before you share it with users, verify everything works:
- Lighthouse audit: Run a Lighthouse PWA audit in Chrome DevTools. Aim for a perfect PWA score
- Installation: Open your app in Chrome on Android or Safari on iOS. You should see an "Add to Home Screen" prompt
- Offline mode: Turn on aeroplane mode and reload. Your cached pages should still work
- Notifications: Send a test push from your Supabase dashboard. It should arrive even with the browser closed
A note on iOS: Safari added PWA push notification support in iOS 16.4, but the user must first add the app to their home screen. It will not work from the browser alone.
Common Pitfalls
HTTPS is mandatory. Service workers only run on secure origins. Lovable apps deployed to their default domain already have this. If you use a custom domain, make sure your SSL certificate is valid.
Do not cache everything. Be selective about what goes in your service worker cache. API responses with user data should not be cached, or you risk showing stale or another user's data.
Handle subscription expiry. Push subscriptions can expire or be revoked. Your edge function should handle failed sends gracefully and clean up dead subscriptions.
Test on real devices. The Chrome DevTools mobile simulator does not support push notifications or service worker installation prompts. Test on actual phones.
What You End Up With
Following this guide, your Lovable app transforms from a web page into something that genuinely feels like a mobile app:
- Users tap an icon on their home screen to open it
- It loads instantly from cache, even on slow connections
- They get real-time push notifications driven by database events
- No app store submission, no review process, no 30% commission
- Updates deploy instantly because it is still a web app
The entire stack, Lovable for the frontend, Supabase for the backend, and standard Web APIs for the PWA features, costs nothing until you have real users. And when you do, it scales with Supabase's infrastructure rather than your own servers.
Need Help Making It Production Ready?
Turning a vibe-coded app into something you can rely on takes more than just adding a manifest file. You need proper error handling, security hardening, monitoring, and a deployment pipeline that does not involve copying and pasting.
That is exactly what our Vibe Coding Production Support service covers. We take your Lovable, Bolt, or Cursor app and make it production grade, so you can focus on what your app does rather than worrying about whether it will stay up.
Subscribe to the AI Growth Newsletter
Get weekly AI insights, tools, and success stories — straight to your inbox.
Here's what you'll get when you subscribe::

- AI for SMBs – adopt AI without big budgets or complex setup
- Future Trends – what's coming next and how to stay ahead
- How to Automate Your Processes – save time with workflows that run 24/7
- Customer Service AI – chatbots and agents that delight customers
- Voice AI Solutions – smarter calls and seamless accessibility
- AI News – how to stay ahead of the ever changing AI world
- Local Success Stories – how AI has changed business in the UK.
No spam. Just practical AI tips for growing your business.