88 Analytics
Back

Case study

Dwellago

Buyer/seller real-estate matching app. Flutter client, Go backend. We took over a broken platform and have been shipping it for over a year.

US stakeholder platform · Ongoing · Phase 4 in progress (AWS + store redeploy)

What we took over

Dwellago is a buyer/seller real-estate matching product for iOS and Android. End users run surveys and search preferences, search and change location, browse listings, chat, pay through Stripe, and see match scores against properties. The mobile client is Flutter/Dart. The backend is Go with Gin, Ent, and MySQL. REST/OpenAPI sits between them, with session cookie auth, check-in, and logout.

We inherited the platform when it was already in production trouble. Builds were fragile. Ownership of the codebase was unclear. Product progress had stalled. Later the store listings were pulled because the published apps were still old pre-migration builds. The job was not a greenfield rewrite. It was take the existing system, make it runnable, migrate it, ship features against the APIs the app already called, and get distribution back.

Phase 1: revival

Before migration talk, the codebase had to run. We took ownership of the inherited Flutter and Go trees, stabilized critical paths, cleaned the worst failure modes, and restored enough release confidence that the stakeholder team could trust the repo again. White-screen crashes, broken builds, and paths nobody wanted to touch got diagnosed first. The point of this phase was simple: you cannot migrate what you cannot build and run.

Phase 2: Flutter and backend modernization

With a living tree, we ran the Flutter migration. That meant FVM for toolchain pinning, AGP and Gradle updates, Android namespace and build.gradle changes, dependency upgrades, and compilation recovery after those upgrades broke the build. We renamed the package to app.dwellago_inc.com and set up a new upload keystore for signed release. Env-based API base URLs (EnvConfig / --dart-define) keep prod and local pointed at the right host.

On the backend we cleaned schema and API surfaces enough that later preference, survey, and matching work had a stable place to land. Datasources, repositories, usecases, and UI stayed in a clean-ish layering on the client. OpenAPI-generated clients (packages/openapi, webserv) were used where they stayed current. Where OpenAPI lagged, we added manual REST for conversations, photo order, device register, and block user.

Build troubleshooting showed up constantly: disk space, Gradle heap and R8 OOM, symbol-strip warnings. We also owned signed release APK and AAB generation for Play, with versionCode/versionName and release notes for the Internal Testing path.

Flutter product work (ongoing across phases)

Most continuous commits have been hands-on Flutter feature work across buyer and seller modes. That includes onboarding, homes feed, preferences, listings, chat, payments, auth/session, and settings. State is BLoC/Cubit across those surfaces. Navigation and routing cover buyer and seller tab flows. Loading, empty, and error states were fixed where the app used to fail open or white-screen.

  • -Property Search Preferences UI for buyer and seller: fetch, dirty state, save, category switching, floating save actions and save dialogs
  • -Dynamic question rendering: Yes/No, chips, dropdowns, numeric ranges
  • -Seller onboarding and property location capture with validation
  • -Buyer location search and change-location flows, including location hints and set-buyer-location payload wiring
  • -Smart search feature work and match score / match summary display
  • -Chat messaging UI with conversation polling
  • -Block / Unblock in chat (overflow menu, composer disabled when blocked), backed by manual API calls
  • -Seller photo gallery: drag-to-reorder, persist order API, cover photo and multi-photo upload
  • -Buyer favorites / wishlist related work
  • -Account deletion, auth/session cookie handling, check-in/logout, profile mode switching
  • -Stripe integration fixes and live config work
  • -Phone number input migration to flutter_libphonenumber
  • -UI modernization pass: typography, colors, dropdowns, app bars, reusable widgets (preferences, tags, photo boxes, message bubbles, buttons)
  • -Production/debug logging cleanup plus targeted feature logs when diagnosing live issues
  • -Android permissions and manifests (notifications among them)
  • -FCM scaffolding: firebase_messaging plus device register/unregister endpoints (Firebase project config still required for full prod push)

Phase 3: matching and lead generation

Matching sits on a dynamic system of questions, answers, and tags. Buyers and sellers answer preference surveys. Those answers feed a scoring path so properties can be matched interactively instead of dumping a static list. Match score and match summary UI surface that result in the app.

Location feed behavior needed backend and frontend diagnosis together. Local vs National scope is not the same as raw geo distance. City intent and distance-based matching diverge, and wrong scope makes the homes feed look broken even when the API returns 200. We wrote backend handoff briefs for location city Local behavior, photo order, push registration, and block user so contracts stayed explicit across the stack.

Survey answer write path (buyer and seller)

Buyers save answers with PUT /api/buyer-survey-level/{surveyId}/{level}/answer/{questionId}. Sellers use the listing-survey equivalent. Level is 1 in the flows we care about. Yes/No questions are SelectOption. optionGroupId 1 means 1 = Yes and 2 = No. The question payload often comes back with options[] empty. That is normal for this API. The Flutter app already sends questionType, importance, selectedValues, and answerId. We did not invent a new endpoint or new fields.

What was broken in practice: vital toggles returned 400 with "question {id} has no select options configured" for ids like 5060, 5041, 5064, 5057 on survey 1000, even when the body had selectedValues:[1], importance:1.0, and an existing answerId. Answer 23 / question 5044 / survey 1001 was stored wrong in MySQL: a SelectOption row saved as numericValues:[5] instead of selectedValues. Separately, the app sometimes logged HTTP 200 on a bulk save, then bedrooms or sqft snapped back to the minimum after reload. Success in the client log was not the same as a persisted answer.

We cut logging back to failures only: one line tagged SURVEY_ANSWER_WRITE_FAILED with surveyId, level, questionId, answerId when present, profile id, status, short reason, and a truncated body (args + importance, about 500 chars). No dumps of successful 200s. That made the 400s visible immediately.

The fix stayed on the existing answer-write function. If options[] is empty but optionGroupId is set, resolve options from that group (group 1 = Yes/No). If the client sends numericValues on a SelectOption, reject with 400 and do not store. selectedValues that are not in the option group also 400. If the answer already exists and only importance is changing, do not fail just because options[] is empty. importance >= 1.0 is a vital and must be stored and returned on GET, not dropped. A valid Yes/No write has to persist and round-trip on GET. A bad write has to 400, not 500, and must not leave a corrupt row. Seller listing survey PUT uses the same write path, so we verified both.

Phase 4 (current): AWS, ECS, and store redeploy

Current work is operating the complex backend on AWS, including ECS and related service topology, plus getting the apps back into Play Store and App Store distribution. Old pre-migration builds were pulled. That means signed post-migration APK/AAB, Play Console Internal Testing uploads, versionCode/versionName, release notes, and App Store redeploy of the current Flutter tree.

Android release ownership includes upload keystore management, package identity under app.dwellago_inc.com, and the build troubleshooting that shows up when Gradle or R8 falls over. iOS build issues have been fixed historically as part of keeping the cross-platform client shippable. Push registration is scaffolded on the client and API side; full production Firebase project wiring is still part of the remaining ops work.

How we work on this codebase

We reproduce first. Observable failure, logs, environment, then map the unfamiliar path. Proposed patches are not accepted until the write path is traced by hand. Smallest change that fixes the reproduction. Then adjacent paths and a diff skim for unrelated edits. On Dwellago that pattern showed up clearly on survey save: trust the reload, not the 200 in the Flutter log.

Collaboration is backend handoff briefs and shared diagnosis, not slide decks. Location city Local, photo order, push, and block user each got written contracts when the OpenAPI surface was incomplete or wrong.

What shipped

  • -Inherited and stabilized a stalled Flutter + Go production codebase
  • -Flutter migration: FVM, AGP/Gradle, deps, package rename, upload keystore, signed APK/AAB
  • -Buyer and seller preferences with dynamic questions, dirty state, and save that round-trips
  • -Chat polling, block/unblock, photo drag-reorder + persist order, Stripe, auth/session
  • -Location search/change flows and Local vs National feed diagnosis with backend
  • -Matching over dynamic questions, answers, and tags with match score UI
  • -Survey answer write fixed: option group resolve, reject bad SelectOption shapes, vital importance kept
  • -Failure-only SURVEY_ANSWER_WRITE_FAILED logging; no fake 200 success noise
  • -Play Internal Testing upload path and store redeploy after pulled pre-migration builds
  • -AWS / ECS backend operations in progress for the live stack
  • -FCM device register/unregister scaffolding on client and API
  • -Handoff briefs for location, photo order, push, and block user

Stack

FlutterDartBLoC / CubitFVMGoGinEntMySQLOpenAPIRESTStripefirebase_messagingFCMAWSECSGradle / AGPPlay ConsoleApp StoreEnvConfig / dart-define

Also shipped

Legal Archive OCR

Legal firm in India · Completed

Custom OCR pipeline for a decade of paper legal records. Variable scan quality, mixed formats. We extracted text, made the archive searchable, and cut the time staff spent digging for files. Python, document processing, search.

Got a stalled app or messy backend?

Email what you inherited. We will tell you what to stabilize first.

Get in Touch