What I learned building and launching a tool to fix my own worst habit.
Photo by Brett Jordan on Unsplash
There’s a moment every app developer knows. The build settles. The last bug is dead. TestFlight is quiet. Your app actually works, and for about ten minutes you feel like you’ve finished something.
Then you remember the website.
I’ve shipped more than ten iOS apps over the years, and I hit this wall every single time. Not the fun wall — not “how do I architect this” or “why is this animation janky.” The other one. The one that shows up after the interesting work is over.
You need a landing page. You need a privacy policy, or Apple won’t let you through review. You need hosting, a domain, an SSL certificate. You need Open Graph tags so the link doesn’t look like garbage when someone shares it. You need app-ads.txt at your domain root or the ad networks won't verify your inventory.
And then there’s the part that always got me: you have to type your app’s name, subtitle, and description into a website builder — the exact same text you just typed into App Store Connect. Re-upload the same screenshots. Re-upload the same icon.
The same work. Twice. Every launch.
I did this ten times before I admitted it was a problem worth solving.
Here’s what finally broke me. I was staring at App Store Connect, copy-pasting my own app description into a website builder in another tab, and the thought arrived fully formed:
This data already exists. It’s public. It’s sitting at a URL.
Every published app has a store listing — name, icon, screenshots, description, ratings, reviews — sitting there, structured, public, one link away. And I was hand-copying it into a website like it was 2009.
That’s the whole idea behind what I built. You paste your App Store link. The website assembles itself.
Not “connect your account and grant these seventeen permissions.” Paste a link. That’s the entire onboarding.
I want to be honest about the timeline, because founder stories tend to compress the boring parts and inflate the heroic ones.
The core product — pull the listing, generate the page, generate the legal pages, host it, wire up a custom domain — came together faster than I expected. Next.js on Vercel, Turso for the database, Better Auth, Cloudflare for DNS. Nothing exotic. The app-import logic that I assumed would be the hard part turned out to be the predictable part.
The thing that nearly stopped me was payments.
I picked a Merchant of Record provider — the kind that handles global sales tax so a solo developer doesn’t have to become an expert in VAT rules across forty countries. I applied. I sent them my product details, my pricing, my demo video.
They rejected me.
Not with a reason. With a paragraph explaining that decisions rely on “multiple data points” and are “guided by regulations imposed on us by Stripe, PayPal and card companies.” Which is a polite way of saying: a risk model said no, and we’re not going to tell you which part.
I want to be fair here: I understand why they do this. When a company acts as Merchant of Record, they’re the ones legally on the hook. They’re not evaluating whether your product is good. They’re evaluating whether you’re a liability. And a brand-new domain, a brand-new social account, a founder with no history in their system — that’s a risk profile, not a person.
Understanding it didn’t make it less annoying. My entire launch was blocked by a form.
So I did the thing you’re supposed to do, which I’d been too lazy to do the first time.
I tore out the payment integration and rebuilt it so my application didn’t know or care which payment provider it was talking to.
Concretely: one interface my app depends on. Each provider is one file implementing that interface. Their differently-named webhooks get translated into my event types before anything else in the codebase sees them. My database is the source of truth for who’s subscribed — not a live API call to a vendor. Swapping providers means writing one new file and changing one environment variable.
This sounds like architecture-astronaut work. It isn’t. It took an afternoon, and it’s the single best decision I made on this project.
Here’s the punchline. While I was rebuilding, I appealed the rejection. They came back — not with an apology, but with more questions. What exactly do you sell? How is it licensed? Is it fully automated, or does it require manual fulfillment on your end?
That last question was the real one. They needed to know I was a self-serve SaaS, not a person hand-delivering custom work. I answered honestly. A few days later: approved.
So I ended up with two approved payment providers and a payment layer that treats both as interchangeable. I launched on the newer one because it fit my stack better and the integration was cleaner. The other one sits behind an environment variable as a warm standby.
If I hadn’t been rejected, I’d have shipped the naive version — hardcoded to one vendor, welded to their webhook shapes, and completely stuck the day something went wrong.
The rejection was the best thing that happened to the codebase.
Your setbacks are load-bearing. I don’t mean that in a motivational-poster way. I mean the rejection forced a specific architectural decision that made the product structurally better, and no amount of good advice would have made me do it voluntarily. I’d read “abstract your payment provider” a hundred times. I did it when it hurt not to.
Abstract the things you can’t control. Not everything — abstraction has a cost, and premature abstraction is its own disease. But payment providers, email providers, anything where a stranger’s risk model can turn your business off on a Tuesday? Put a layer between you and them. You’re not doing it for elegance. You’re doing it so a rejection email is an inconvenience instead of a rewrite.
Own your data. My subscription state lives in my database, updated by webhooks, not queried live from a vendor. This means feature-gating is a fast local read, and it means that if a provider disappears, I still know who paid me. Card details can’t be migrated between providers — that’s a hard, legal reality — but everything else can, if you kept it.
Ship the thing you needed ten times. The best product idea I’ve had in years wasn’t a market gap I spotted in a spreadsheet. It was a chore I’d done ten times and complained about ten times and never once thought to automate. Look at what you keep re-doing by hand. That’s the list.
It’s live. It’s called Storestand. You paste your App Store link and get a real website — landing page, privacy policy and terms, hosting, SSL, custom domain, SEO tags, Open Graph, sitemap, app-ads.txt and llms.txt at the root, your best reviews pulled in as social proof. Ten minutes instead of most of a week.
It’s free for one site, forever. I built it for one specific person: the developer who just finished their app and doesn’t want to spend the next five days building a website for it.
I know that person well. I’ve been him ten times.
If you’ve shipped an app and remember this exact wall — I’d genuinely like to hear how you handled it. And if you want to skip it next time, it’s at storestand.app.
The first no was the risk model looking at a new domain. The useful bit was the follow-up: automated vs you fulfill by hand. That's them asking if they can be the seller of a digital good, which is how they take VAT in 40 countries off you. I'd put that in the first application unprompted – self-serve, no manual delivery, subscription. Saves a week of sitting on a form. I'd still keep the second provider around. The next no might not come with extra questions.
The interesting opportunity isn't generating App Store landing pages—it's becoming the default launch infrastructure developers rely on every time they ship an app. I'd keep validating whether customers adopt Storestand because it saves time on one launch or because they never want to rebuild that workflow again.