Back to projects

Google Play release · Next hypothesis under review

Umaso

A content-first v1 that delivers team-managed Korean recipes to people cooking in Japan.

The original concept connected Korean recipes with locally available ingredients and substitutes in Japan, but features grew before the target problem was clear. Instead of delaying indefinitely, we reduced v1 to team-managed recipe content and released it on Google Play. We now operate that version while redefining the next user and value hypothesis.

Period
2026.02 - Present
Ownership
Flutter features · Go APIs · admin web · operations
Team
Started with 2 designers, 4 Flutter, and 4 server roles
Repository
Private repository

Connecting recipe discovery with ingredient shopping

The project began from a team hypothesis: people in Japan could find Korean recipes, but still had to research local ingredients and substitutes separately. During development, features grew before the target user and first behavior were clear.

We froze new scope, reduced v1 to team-managed recipe content, and released it on Google Play. We now operate that version while redefining which value—including the original ingredient hypothesis—to validate next.

From use to operation

The path connecting a person viewing a recipe with the team operating it

  1. 01

    Recipe app

    Browse and search in Flutter

  2. 02

    Application server

    Go APIs manage auth and rules

  3. 03

    Recipe data

    Translations and ingredients in PostgreSQL

  4. 04

    Admin tools

    Next.js admin and R2 images

From stopping feature expansion to shipping a content-first v1

Ingredient substitutes, shopping, community, and recommendation ideas grew in parallel until the team could no longer name one user behavior the product should change first.

In a side project, contributor availability changes; waiting for a complete redesign would delay release and feedback. We also had no usage data that justified treating the original hypothesis as correct.

We froze new scope, reduced v1 to team-managed recipe content, and released it on Google Play. We did not present the original ingredient-and-substitute concept as validated; it returned to the hypothesis backlog.

  • Closed the content path from admin entry to Flutter display across Go APIs.
  • Moved app and server into a monorepo so feature changes were easier to trace.
  • Defined a feature-owner approach for one behavior at a time in the next phase.

We released the content-first v1 on Google Play and left the team with an admin workflow for recipes, ingredients, and translations. Usage and retention metrics are not yet available.

I learned that release is not the end of validation; it is the first checkpoint that makes the next measurement possible.

Moving translation from the read path into content operations

People needed Japanese recipe content, but generating a translation on every read would make latency, cost, and quality variation part of every user's request.

For Phase 1, the team could register and review content through the admin web. Translation cost and review policy for user-generated recipes or comments were not decided yet.

I chose to create a translation draft when the team entered Korean content in the admin web, then save it after review. The app falls back to another language only when no saved translation exists.

  • Connected source entry and translation-draft review in one admin content workflow.
  • Tracked translation state with is_translated so operators could find missing content.
  • Made the app API use saved translations with locale fallback.

The team can operate recipes, ingredients, and translations together in the admin web, while the app reads saved content instead of generating a translation on every request.

I learned to treat translation as a content lifecycle—who reviews it, when it becomes publishable, and how missing states are shown—not only as a display feature.

Roulette flow in the current public release

A single ingredient and a mixed sauce are not the same substitute

Replacing ingredient A with ingredient B is different from replacing a sauce with a measured combination of several ingredients.

A simple one-to-one relation could not represent a substitute made from several ingredients or another sauce. A flexible polymorphic relation, however, could not get every referential guarantee from database foreign keys alone.

I kept ingredient substitution as a one-to-one relation and modeled sauce substitution as a list of components with quantities. Using component_type and component_id meant validating the reference through queries and application rules instead of relying only on foreign keys.

  • Separated ingredient substitution and sauce substitution into different query shapes.
  • Joined ingredient or source conditionally by component_type and used COALESCE for the resolved name.
  • Wrapped source creation, component rows, translations, and audit logging in one transaction.

The API and admin workflow can represent both a single-ingredient replacement and a sauce composed of multiple components.

I learned that a flexible data model trades simpler representation for application-level responsibility over integrity that the database cannot enforce by itself.

Current release flow for finding recipes by ingredient

Keeping complex recipe retrieval visible in SQL

A recipe detail response had to combine ingredients, translations, substitutes, and sauces into one screen-shaped result.

The query needed locale fallback, and the columns and joins differed by use case. A table-centered abstraction alone would make the actual read intent harder to review.

I chose pgx and sqlc instead of an ORM: each use case keeps its SQL explicit, while generated types preserve type safety in application code. The point was not that an ORM is always worse, but that this service benefited from reviewing query shape directly.

  • Kept recipe detail, translation, and substitute reads as use-case-specific SQL.
  • Used sqlc-generated code to keep query result types explicit.
  • Made locale fallback and conditional relation reads visible in SQL.

The Go API implements reads for recipes, ingredients, translations, and substitutes with query-level visibility into their relationships. I did not measure a speed comparison with an ORM or quantify N+1 improvements.

I learned that a technology choice should explain where data shape is reviewed and which invariants are protected, not only name the libraries used.

Fridge inventory in the current public release

Was the repeated logout really a login-function bug?

Users were repeatedly logged out of the Flutter app, so the authentication path had to be traced beyond the visible login screen.

User and admin authentication used different secrets and lifetimes, but similar settings sat next to each other at the dependency-injection boundary. Reading the function in isolation did not reveal the cause.

I traced which user refresh secret and lifetime were injected by the DI container instead of stopping at the login function, then reconnected user and admin paths to their own settings.

  • Traced the secret and lifetime injected into the user refresh-token middleware.
  • Replaced the admin JWT settings wired into the user path with user refresh settings.
  • Reviewed auth logic, configuration, expiry, and failure UI as one flow.

I separated the user refresh settings from the admin settings and fixed the DI wiring that caused the repeated logout. There is no public recurrence rate or session-retention metric.

I learned that authentication testing must cover not only function logic but also the assembly boundary where secrets and lifetimes reach middleware.

We released a content-first v1 on Google Play. In addition to Flutter features, I built Go APIs and the team-operated admin workflow for recipes, ingredients, and translations.

I now clarify who has the problem, who owns each piece of data, and which screens and APIs must be complete before release. For example, adding a recipe field means checking the Flutter display, Go API, database migration, and admin input together.

Public usage or retention metrics are not available. The Google Play release proves delivery, not validation of the original ingredient-and-substitute hypothesis.

  • Measure recipe discovery and return behavior in the released v1
  • Choose one target user and one primary behavior for the next phase
  • Use evidence to decide whether the substitute model belongs in the next phase