After running several wallpaper apps, the assumption that changed most for me was this: launch is only the starting line. The day the app appears on the store feels like an achievement, but it's the months that follow that quietly split the apps that grow from the ones that fade.
The build itself got far lighter once Rork arrived. Describe the screens and features in plain English, and you get working React Native code back. But the same ease means more lookalike apps ship every week. What sets an app apart isn't the initial polish — it's how you keep tending it, day after day, once it's live.
Assuming you've assembled a wallpaper app in Rork, these are notes on the three areas that actually mattered to me once I moved into operations: keeping content fresh, surviving the first week, and a revenue design that doesn't thin out. Whether you're building your first one or already running one that has plateaued, I hope these land at a level you can act on.
Freshness Carries DAU — Use Scheduled Publishing to Add a Little Every Day
For wallpaper retention, the most direct lever is the feeling that there's "a little new" every time someone opens the app. Dump 200 images in at once and people exhaust them in a few days, then stop coming back. Release those same 200 at five a day, and each visit offers a small discovery.
Scheduled publishing is what supports this in practice. Give each piece of content a publish timestamp and only serve the ones whose time has passed. Without touching app code at all, setting a future publish date is enough to create a "grows a little every day" experience.
-- Supabase: wallpapers table, with published_at driving scheduled delivery
create table wallpapers (
id uuid default gen_random_uuid() primary key,
title text not null,
category text not null check (
category in ('nature', 'city', 'abstract', 'minimal')
),
tags text[] default '{}',
-- split thumbnail and full image to keep bandwidth down
thumbnail_url text not null, -- low-res preview (~400×700px)
full_url text not null, -- high-res original (1080×1920px+)
is_premium boolean default false,
is_active boolean default true,
published_at timestamptz default now(), -- a future time makes it scheduled
download_count integer default 0,
created_at timestamptz default now()
);
-- RLS: only published (published_at <= now) and active rows are readable
alter table wallpapers enable row level security;
create policy "Public wallpapers are readable when published"
on wallpapers for select
using (is_active = true and published_at <= now());The key is to put the visibility filter in RLS (Row Level Security), not in app logic. Even if you forget where published_at <= now() on the query side, the policy won't return future rows, so an unpublished wallpaper can't slip into view by accident. I once relied on a client-side filter alone and had an unpublished batch flash through a stale cache for a moment — since then I always keep this second layer in place.
Surfacing new arrivals is also easier to operate when it's resolved on the data side.
// "New" tab: items published in the last 7 days, newest first
async function fetchNewArrivals(supabase: SupabaseClient) {
const sevenDaysAgo = new Date(
Date.now() - 7 * 24 * 60 * 60 * 1000
).toISOString();
const { data, error } = await supabase
.from("wallpapers")
.select("id, title, thumbnail_url, is_premium, published_at")
.gte("published_at", sevenDaysAgo)
.lte("published_at", new Date().toISOString()) // exclude future, just in case
.order("published_at", { ascending: false })
.limit(30);
if (error) throw error;
return data;
}The extra lte excluding future rows absorbs drift between server and client clocks. RLS makes it unnecessary in principle, but on a screen that orders strictly by date, being explicit avoids ordering surprises.
Operationally, all this takes is spreading the published_at values of a batch you made at the start of the month across future days, a few at a time. That sustains the experience of "an app updated daily" without posting by hand every day. After switching to scheduled publishing, the rhythm of return visits became visibly steadier for me.
Don't Lose Them in Week One — Onboarding and Reasons to Return
Even after a download, if the first few minutes say "this isn't for me," that person almost never comes back. First-week churn in wallpaper apps tends to come less from missing features and more from a first experience that's simply too flat.
What helped me was a light onboarding that asks for a category right after launch. Asking about taste just once lets you bias the initial home view toward that person. Keep the questions modest — confined to a single screen.
// Ask for one preferred category on first launch
function OnboardingCategory({ onDone }: { onDone: (c: string) => void }) {
const categories = [
{ key: "nature", label: "Nature" },
{ key: "minimal", label: "Minimal" },
{ key: "abstract", label: "Abstract" },
{ key: "city", label: "City" },
];
return (
<View style={styles.wrap}>
<Text style={styles.heading}>Pick a mood you like</Text>
<Text style={styles.sub}>You can change this later</Text>
<View style={styles.grid}>
{categories.map((c) => (
<Pressable
key={c.key}
style={styles.card}
onPress={() => onDone(c.key)}
>
<Text style={styles.cardLabel}>{c.label}</Text>
</Pressable>
))}
</View>
</View>
);
}The "You can change this later" line is there to lower the pressure of choosing. Make the first choice feel heavy and some people drop off right there. An easygoing tone tends to carry them forward.
As reasons to return, favorites and notifications work plainly. But notifications are easy to resent, so my rule is to limit the reason for sending to "a category you like has new arrivals." Send everyone the same promotional push and you'll swing them straight to notifications-off or uninstall.
// Notify only when a favorited category gets new arrivals
async function scheduleNewArrivalNotice(
favoriteCategory: string,
newCount: number
) {
if (newCount <= 0) return;
await Notifications.scheduleNotificationAsync({
content: {
title: "New wallpapers just landed",
body: `${newCount} new in "${categoryLabel(favoriteCategory)}"`,
data: { category: favoriteCategory },
},
// aim for a time people actually open, e.g. 8am next morning
trigger: nextMorningAt(8),
});
}Make sure a user who opens from the notification lands on "new arrivals in their favorite category," not the top of the app. If the copy and the landing don't match, even an opened notification disappoints, and the next one goes unopened.
When reading retention, day-based survival rates anchored on install day are the clearest. The figures below are roughly where I feel a wallpaper app is healthy.
| Metric | Struggling | Feels healthy |
|---|---|---|
| Day-1 retention (D1) | under 20% | 30% or more |
| Day-7 retention (D7) | under 5% | 12% or more |
| Avg sessions in week one | under 2 | 4 or more |
More than the absolute numbers, watching how these survival rates move before and after you add scheduled publishing and onboarding gives you a clearer sense of whether a change worked.