Problem
Most internet access in Cuba happens through Etecsa’s Nauta service: you authenticate against a captive portal to come online, and you manage your plan, balance, and bonus hours from a separate user portal. For years that meant two desktop browser flows that didn’t share state. Subscribers needed a single mobile surface where they could log in to the captive portal, check what they had left, and manage their account — without juggling browsers, copy-pasting credentials, or interpreting raw HTML responses.
In parallel, the operational platforms behind these surfaces — serving roughly 7 million subscribers — were being rebuilt off a legacy Javax stack onto Spring Boot. The app needed to land on top of a backend that was actively moving under it.
The mobile surface
I built the Nauta mobile app end-to-end in Flutter, as the sole mobile developer. Four primary flows:
- App picker — single entry that splits into the two distinct portals (user account vs. captive-portal navigation) so subscribers see a clear mental model before they pick.
- Captive portal login — the everyday flow: open the app, authenticate against the Nauta Wi-Fi, get online.
- User portal login — credentialed access to the personal account, including the captcha challenge the server-side portal still requires.
- Account dashboard — plan type, status, sale date, expiry, bonus hours, currency, and balance — pulled from the user portal and rendered as structured data instead of the raw HTML the legacy portal returned.
Decisions worth calling out:
- One app, two portals. The captive portal and the user portal are different systems with different auth lifetimes and different error semantics. I kept them visually unified but kept their state machines separate so a session in one couldn’t accidentally bleed into the other.
- Honest about the captcha. The user portal requires a server-issued image captcha. I didn’t try to bypass it — the app refreshes the challenge on demand and treats the captcha as a first-class part of the form, not a workaround.
- Structured data over raw portal HTML. The user portal returns its account view as a page, not as a clean API. The app normalizes it into a typed model so the dashboard reads as a product, not a screen scrape.
The backend it talks to
The Etecsa team — myself included — migrated the legacy Javax modules behind these portals to Spring Boot, module-by-module, behind a shared API contract. The non-obvious decision: legacy Javax and new Spring Boot services ran side-by-side in the same operational network, routed by feature flag rather than by endpoint, so a single team could be moved over, validated, and only then expanded.
There was no migration weekend. There was a sequence of safe Tuesdays.
Outcome
The mobile app gave subscribers a single place for everything they used to do across two browser flows — the everyday tasks (log in, check balance, see bonus hours) became one-screen, one-tap operations. Behind it, the modules already migrated to Spring Boot showed a 3× throughput improvement against the legacy baseline. Operational staff noticed response times getting faster without anyone telling them why. Zero outages during the migration window for the modules we shipped.
The pattern of “no big migration weekend” is now the team’s default for any modernization work — and the app continues to evolve against whichever backend the flag routes it to.


