Building SpellTales: What I Thought Would Happen, What Actually Happened

Get SpellTales here: https://spelltales.co.uk/

Introduction

The SpellTales app started with a very simple idea. After spending most evenings battling with my 9-year-old over her lack of reading and struggles with spelling, I asked her, “What would you actually read?” The reply: “stories about kittens, YouTube and poo”. Ah. Wonderful. So I went to ChatGPT and asked it to write an age-appropriate story featuring kittens, YouTube and poo. Well, readers, she loved it. Read the whole thing laughing and asked for another story. I asked it to create another one, but to add in 10 words that she’d been given as her “weekly spelling words”. She loved the second story too and never noticed the slightly odd phrasing of shoe-horning in “multiracial”, “glacial” and “potential”. At this point, SpellTales was born, and this 2-part blog will take you through how it went from a frustrated parent’s kitchen table idea into an iOS App that’s available to download in 172 countries.

In this post I am not going to give you a neat, cleaned up version of the build. Instead, I want to walk through what really happened. The parts that worked, the parts that absolutely did not, and the decisions I would make differently next time.

About Me

Let’s be clear about who I am – About Me. If you’ve read anything on my blog before, you’ll know that I’m a database consultant and not a developer! I have never coded my own website nor built an app. I work with AWS and GCP technologies daily, but I have only a layman’s understanding of the services required to build a secure and scalable app. I’ve never really worked with AWS Lambda, never touched Amazon API Gateway or Amazon Cognito, and I’ve certainly never used React. At the beginning, this was going to be a small personal project, and it was only through talking to other parents about it that I realised this could solve problems for many other families too. GenAI and Kiro have made this project feasible for me. I think that having the basic background in AWS and knowing how things connect together, and generally having an understanding of technology and how coding works was a requirement, but the takeaway I hope you have at the end of my blog is: “ANYONE CAN DO THIS“. GenAI allows ideas to flourish and creativity to run unbounded.

Starting with iOS and React Native

Right from the beginning I chose to build for iOS as the primary with web and Android planned to come later. I chose this because I am an Apple user so I can easily create iOS Sims and push my builds to my phone for testing. I used React Native with Expo and my thinking was simple. If I built the app well in React Native, I could always extend to Android and the web later. One codebase, several platforms if the app proved useful.

On paper this sounded completely reasonable. In practice, the differences between platforms are not just about screen size. They affect how sessions are handled, how secure storage works, how navigation behaves and how network calls are made.

Why I parked Android and web for now

As the app grew, it became clear that supporting Android and web in a secure and consistent way would be a significant amount of extra work.

  • Session handling: iOS, Android and web all handle identity and tokens differently. On iOS I could rely on secure storage and a certain lifecycle. On the web I would need to cope with page reloads, multiple tabs and a different threat model.
  • Rendering differences: Some screens that looked fine on iOS did not translate cleanly to web. Layout expectations are different and some of the patterns I used simply did not make sense in a browser.
  • CORS and networking: The backend lives on AWS using Amazon API Gateway, AWS Lambda, Amazon Cognito and Amazon S3. Talking to that stack from a mobile app is relatively straightforward. Talking to it from a browser involves CORS rules, preflight requests and a lot of subtle failure cases.
  • Security: Once I started mapping out how data would move between three different worlds, it became clear that doing this properly would require a much more formal identity and security design than I had capacity for in a side project.

At that point I made a deliberate choice. SpellTales would be iOS only for now. If the app proves successful and useful, I can come back and design Android and web as proper first class citizens rather than bolting them on as afterthoughts.

Skipping the specification and paying for it later

My next mistake was trying to skip a detailed specification. I told myself I would build a small pilot, see it working, and then extend it naturally from there. It felt like the quickest way to get something in my hand. I started using Amazon Q (now renamed Kiro CLI). This went wrong really fast. Kiro CLI doesn’t handle session context, so each logout meant I had to explain where we’d got up to last time, and it doesn’t create tasks and subtasks. Each command is a standalone operation without consideration for what else might be going on. For example, at one point, I wanted to persist user data in Amazon DynamoDB instead of S3. Kiro CLI happily made the changes, but not only missed multiple references to S3 in the code, it also arbitrarily deleted my S3 bucket before I’d migrated the data over. Luckily, I was still very early in so there was nothing critical lost, but in a more formal or production environment, the lack of governance and controls on Kiro CLI would be devastating.

Kiro CLI also meant I had no real structure to the app. Bits were just being bolted on when needed. This lack of structure meant that every new feature I added forced me to revisit decisions I had made earlier. The app did not grow in a straight line. It consistently broke in catastrophic ways, and more worryingly, added data handling bugs. I would add a feature, realise it fought against the way I had structured the rest of the app, remove it, and then add a slightly different version. The codebase slowly became harder to modify, understand, and most importantly, secure.

The styling refactor that should have been unnecessary

One concrete example of this was styling. Early on I relied on Kiro CLI to generate React Native components and it placed styles inline on each element. That looked something like this:

 <View style={{ padding: 10, backgroundColor: "#fff" }}>

For a quick prototype this felt fine. The app looked roughly how I wanted and I could see progress. The problem appeared when I decided the overall look and feel needed to change.

Because all the styles were inline, there was no shared theme, no reusable style set and no concept of a visual language for the app. If I wanted to change the default padding or a core colour, I had to hunt for every place it appeared. It was fragile and repetitive.

What I actually needed was something much closer to this:

import { globalStyles } from "../styles/global";

<View style={globalStyles.card}>

I tried to get Kiro CLI to perform this refactor for me. No matter how I prompted it, the tool could not reliably move the styles into a shared file without rewriting large parts of the components. The result was broken layouts and more confusion.

In the end I spent more than a full day manually creating a global style file, moving inline definitions into it and replacing references throughout the project. It was not interesting work and it existed only because I had taken a shortcut earlier.

The lesson for me was straightforward. If you do not set a sensible structure at the start, you will almost always pay for it later when you want to change or extend the app.

Letting Kiro IDE do the job it is good at

At this point I stopped trying to use Kiro CLI mainly as a code generator and instead used it for what it is genuinely good at. I gave it a careful description of what SpellTales should do and asked it to help shape the architecture.

From that narrative description, Kiro CLI helped me identify the main components.

ComponentResponsibility
React Native with ExpoMobile user interface and reading experience
AWS LambdaStory generation logic and inserting spelling words into the narrative
API GatewayProviding a controlled and secure way for the app to call backend functions
CognitoAuthentication for parents and child profiles
DynamoDBStoring user profiles and generated stories so they can be retrieved later

Once this structure was clear, each feature had a natural home. Story text belonged in DynamoDB, authentication lived in Cognito, and the app itself simply orchestrated calls between those building blocks. The code became easier to reason about and far easier to test.

Switching to Kiro IDE

Up to this point, I had been using Kiro in a fairly limited way. My original work had been done with Kiro’s command line developer mode, which I treated like a smarter version of autocomplete. It could generate pieces of code when prompted, but I was still working without a proper structure. I was getting output, not direction.

Eventually, I reached the limits of that approach. I needed something that could help shape the project rather than just extend whatever state it happened to be in. That is when I moved into the Kiro IDE properly.

The transition was not automatic. I did not throw the old code away. Instead, I took the project I already had and used it as the foundation for something better.

Using existing code as input, not an anchor

The first thing I did inside the Kiro IDE was create:

  • Tasks for specific pieces of functionality
  • Steering documents to explain what those tasks should achieve
  • Requirements that captured what “done” looked like

This was the first time the app had a shared language for intent. Until then, my codebase reflected a series of decisions rather than a plan. Kiro gave me a structured place to articulate how things should work instead of how they happened to work.

Iterative refinement instead of rewriting everything

Once the core documents were in place, I could take each part of the app and ask Kiro to review it against those requirements. The IDE identified:

  • logical inconsistencies
  • duplicated code paths
  • missing validation
  • UI flows that did not match the stated goal
  • assumptions I had carried through without noticing

This was a much more productive use of Kiro. Instead of generating entire screens from scratch, I used it to examine what was already there and refine it. I stopped fighting the tool, and the tool stopped fighting me.

Fixing issues one by one in the iOS simulator

The most satisfying part of this workflow was seeing changes land in real time. I would:

  1. update a requirement
  2. ask Kiro to adjust the code
  3. run the updated build in the iOS simulator
  4. observe the behaviour directly
  5. repeat as needed

This loop created momentum. I was no longer guessing whether a fix aligned with intent. Each cycle brought the app closer to what I actually wanted, not just what I had typed weeks earlier.

The real benefit: understanding the codebase

By switching to the Kiro IDE, the code stopped feeling like something that grew organically and started behaving like something designed. I learned more about the project in that period than in the entire early build phase. Issues that once felt like symptoms turned into understandable causes.

SpellTales became easier to work on not because the code was new, but because the thinking was finally consistent.

Expo, Xcode and slightly different worlds

During development I relied heavily on Expo. It is an excellent way to work on a mobile app. You run a single command, scan a QR code and see your changes almost immediately on a real device. For day to day UI work that speed is invaluable.

Later, when it was time to produce an actual iOS build, I opened the project in Xcode. This was the first time I saw how different the world looks once your code is bundled into a production app.

Some modules behaved slightly differently when bundled. The timing of initialisation changed in places. Network calls that felt instant in Expo now had visible delays. None of these issues were dramatic, but they were important for user experience. When I fixed the issues in XCode, Expo then failed instead due to module incompatibilities. It meant I lost my instant testing sim and instead had to build and push to App Store Connect and TestFlight now for all proper testing. This slowed me down a lot.

The key point here is that Expo, Xcode and eventually TestFlight behave in subtly different ways. If you only ever test inside Expo, you can easily miss problems that only appear once the app is packaged and running in the environments Apple will actually use to review it.

Subscriptions and monetisation

Once the core of SpellTales was working, I started looking at subscriptions. In my head this was a small piece of work. Users would pay a small amount each month, and in return they would unlock more frequent story generation and additional features.

It became clear very quickly that subscriptions are not a small piece of work. They involve product registration, receipt validation, entitlement decisions, support for cancellation and renewal and different expectations across platforms. I reached the point where building this manually felt both time consuming and easy to get wrong.

This is where I brought in RevenueCat. It provided a way to manage subscriptions, map App Store products to logical entitlements and handle the underlying complexity for me. I added it later in the project and I am glad I did. If I were starting again I would make that decision much earlier.

Where I finished and what I took from it

By the time I was ready to think about App Store submission, I had an app that could:

  • Authenticate parents and allow them to set up child profiles
  • Accept weekly spelling words and story preferences
  • Generate stories using a Lambda backend that calls Bedrock Nova and store them in DynamoDB
  • Present stories in a reading view that was pleasant to use
  • Use subscriptions to gate access to premium behaviour

SpellTales worked, but the route to that point was more complicated than it needed to be. If I summarise the main lessons I took from Part 1 of this journey, they would be these.

  • Start with one platform and do it properly rather than assuming that three platforms will magically fall out of React Native.
  • Spend a little more time on structure and specification before rushing into the first working prototype.
  • Keep styling centralised and consistent from the beginning. Future you will be grateful.
  • Treat tools like Kiro as partners in design and architecture rather than a way to skip thinking.
  • Remember that Expo is a development environment and that behaviour in Xcode and TestFlight may not be identical.

In the next post I will walk through what happened when I took this app that worked well enough on my devices and tried to put it in front of Apple for review.

Loading

Related Post