Launching a small app does not mean skipping production: the Ultimate Porra checklist
Ultimate Porra started as a bounded app: a World Cup prediction pool, a small group of users, clear rules and a short operating window.
That kind of product can be misleading. Because it is not a large SaaS, it is tempting to treat it as if it did not need production discipline. But as soon as there is login, email, predictions, official results and a public domain, it is no longer "just a small website". It is a production surface.
The question that shaped the launch was:
ImportantWhat must be true before inviting real users, even if the app is small?
The answer was not a stack. It was a short list of invariants — properties that had to hold regardless of which services sat behind them. The useful part of this write-up is the invariants, not the topology.
Separation of responsibilities
The app can be small; the surface is not. Even a small product benefits from keeping responsibilities apart: a public entry, a server-side layer that holds anything sensitive, the product's own data and rules, identity, and outbound email. The specific technology matters less than the boundaries between those jobs — each one can change without dragging the others with it.
First invariant: the browser does not touch secrets
The browser should only call same-origin product routes. Anything sensitive — administrative credentials, session secrets, provider keys — stays server-side, behind a thin layer the browser talks to but never sees into.
That layer is not decorative. It is the boundary that keeps privileged operations out of browser JavaScript: the client asks, the server-side layer decides and holds the credentials. A leaked browser bundle should never be enough to perform an administrative action.
Second invariant: the technical domain is not the product
Static hosting providers usually generate a technical domain like https://<project>.pages.dev. It is useful to validate a deployment before the custom domain is active. But once the custom domain exists, the technical domain should not remain a second public entrypoint — it duplicates the origin, weakens the canonical domain, and complicates cookies, CORS, analytics and email links.
The post-launch policy is to redirect the technical domain to the canonical one (<project>.pages.dev/* -> 301 -> custom domain) and to keep preview deployments behind access control when they are used for QA.
NoteA technical domain is useful for diagnosis. It should not become a parallel production URL.
Third invariant: a perimeter you can bypass is optional
Putting an edge in front of an API does not protect anything on its own if a request can still reach the origin some other way. The invariant is blunt:
WarningIf a request can avoid the perimeter and still reach the origin, the perimeter is optional.
So the work does not end at "put the edge in front." It ends at making the origin reject anything that did not arrive through the expected path. How you achieve that depends on what the runtime supports; the property to guarantee is the same regardless of mechanism.
Match protection to the route's risk
Not every route carries the same risk, and the controls should say so. Public, low-risk reads can stay light. Authenticated actions need identity and per-user limits. The destructive and operational class — anything that changes official state — gets the strongest checks and is closed to the public by default. The practical sentence: the edge reduces surface; the backend decides permissions.
Email is part of launch too
The app is not ready if a user receives an email with the wrong brand. The same identity service could create users for more than one product, so an action email had to carry product context end to end — the right tenant, the right sending domain, the right branding, the right final destination. This is not cosmetic; it is continuity of trust.
Initial data: without seed there is no product
For a World Cup pool, the initial content is not optional — the competition, edition, teams and the full match calendar have to exist in production before anyone can play. The checklist was concrete: run the import, verify every match is present, confirm dates and stages, then smoke-test creating a game, making a prediction, updating a result and reading the leaderboard. If the seed fails, the app can be deployed and still not be launchable.
Operations during the event
A World Cup is not a continuous software flow; it is a calendar of external events. Results can be entered manually or ingested from a feed. For a small beta, choosing manual is not a technical failure — it is a scope decision. The point is not to automate everything; it is to know which manual operation exists, how long it takes and who owns it.
The rule I keep
A small app does not need a large organization. It does need production invariants. Mine for this launch were:
one canonical public domain
no browser secrets
no bypassable origin
correct email branding
verified initial data
written event operationsThat does not make the app heavy. It makes it launchable.
Technical sources
- Cloudflare Pages explains how to redirect
*.pages.devto a custom domain using Bulk Redirects: Redirecting *.pages.dev to a Custom Domain. - Cloudflare documents how to effectively disable access to the
*.pages.devsubdomain by combining Access for previews and Bulk Redirects for production: Custom domains. - Cloudflare Pages can protect preview deployments with Access; by default those previews are public: Preview deployments.
This article is part of my architecture and operations notes. If you are interested in this work, follow the blog or contact me.