Shipping an iOS app means building some things on the web that have nothing to do with the app's code. It is easy to leave them until last, and then the submission form won't let you past an empty URL field. Here is what I put together.
Two URLs are required
A support URL and a privacy policy URL. Both have fields in the submission form and neither can be left blank.
The support URL is published as the place users go when something goes wrong, and it is linked from the App Store listing, so people actually arrive there. The privacy policy URL is published the same way.
Both can be ordinary web pages. No dedicated domain, no special format — one more page on a site you already have.
What goes on the support page
Not much, as it turns out. This is enough:
- Which app this is support for
- How to get in touch
- Roughly how long a reply takes (and if you can't reply quickly, say so)
What to avoid is putting a bare email address on it. This is a public page reachable from the App Store listing, which makes it a scraping target. A form is safer.
This site is statically exported and has no server, so the form is a Google Form that I link out to rather than embed.
export const SUPPORT_FORM_URL = 'https://docs.google.com/forms/d/e/.../viewform';
Both the contact page and the support page point at the same form, so the URL is a constant in one file. Updating one and forgetting the other is the obvious failure here.
An external form changes your privacy policy
This is the part that is easy to miss. Using a Google Form means the submitted content is stored on Google's servers. Your site may receive nothing at all, but from the user's side their information is going to a third party.
The policy has to say so: that form responses are transmitted to and stored by Google, and that this involves a transfer overseas. Swap the form for a different service later and the policy becomes false until you update it too.
Don't put the policy prose in a component
I started with the policy text written directly into JSX. Don't.
The same text ends up duplicated across two language versions, adding one clause means editing both components, and the heading-and-paragraph structure drifts apart between them over time.
I moved it into structured data and left the rendering to one shared component:
{ "privacy": { "title": "Privacy Policy", "sections": [ { "heading": "What is collected", "paragraphs": ["The app requires no account and ..."], "bullets": [] } ] } }
bullets is always present even when empty. Letting one language omit a key means branching in the renderer, which is more annoying than typing [].
As for the content: writing only what is actually true turned out to be the low-effort path. "We don't collect this" commits you to not collecting it. Adding a single analytics tool makes the sentence a lie, so the real question is whether you'll remember the policy exists when you add a feature six months from now.
The URL you register outlives your implementation
Avoid registering a URL that leaks your implementation, like /support.html. Rebuild the site and it starts returning 404.
App Store metadata doesn't update until your next submission, so fixing it there isn't quick. Absorbing it on the site is. On Cloudflare Pages that's one line in public/_redirects:
/support.html /support/ 301
The static-site side of this is a separate post.
Registering something extensionless like /support/ from the start makes the URL much easier to keep alive across rewrites. Assume any URL you hand to Apple will outlive the implementation behind it.