This article was originally published in Japanese on Qiita.

Introduction

Hi, I'm Kohei Matsunobe. I'm a second-year student in the online (correspondence) program at ZEN University, studying to become an AI engineer.

On my own, I build an iOS app called KamiMeg (カミメグ). It's a digital goshuin book: you take photos of the goshuin (the calligraphy stamps you receive at Japanese shrines), find shrines nearby, and keep a record of your visits. It's free, there's no account, and your records are stored on your device.

I released v1.0 in October 2026 and v1.1.0 in November 2026. The headline feature of v1.1.0 is a generative-AI feature where a "companion deity" introduces the shrine to you when you press a button.

This post is about what I thought through before shipping that feature. It's not a code walkthrough; it's about three decisions:

  • Where the API key should live (and why I went serverless)
  • How to keep the app's promise — "your records stay on your device" — even after adding AI
  • Why I ended up with Gemini, even though I first looked at Bedrock and the Claude API

KamiMeg before the AI feature

Originally, KamiMeg was an app that did almost everything on the device.

Role What I use
App Expo / React Native (TypeScript)
Storing records SQLite (on device)
Finding nearby shrines Google Places API
Ads Google AdMob

There was no backend server of my own and no account. "Your goshuin photos and visit records live only on your own device" was something I really cared about as the person building it.

When I submitted v1.0, App Store review needed some extra back-and-forth. I responded, it passed, and the app went live — but it taught me that even as an indie developer, once you ship to a store, you should build things you can explain. That became a starting assumption when I designed the AI feature.

What I wanted to build

When you're looking for shrines and logging visits, there are moments where you wonder, "What kind of place is this shrine?" So I thought it would be fun to show a short introduction for each shrine. Just showing a plain description felt a bit flat, though, so in the app it's the "companion deity" who introduces the shrine.

The introduction is never generated automatically. It's generated only when the user presses the button. I'll explain why in the privacy section.

In v1.1.0 I also made it so that when you search for a shrine by name, only that shrine is shown, and I unified the app's display name as "カミメグ".

Decision 1: Don't put the API key in the app (API Gateway → Lambda → Gemini)

The simplest way to use a generative-AI API from an app is to call it directly from the app. But that means shipping the API key inside the app. A key inside an app binary can be extracted, and since I manage the spending cap myself as an indie developer, I wanted to avoid that.

So I put a thin AWS serverless layer between the app and the AI.

flowchart LR A[iOS app<br/>KamiMeg] -->|Shrine name & address<br/>after consent only| B[Amazon API Gateway] B --> C[AWS Lambda] C --> D[Google Gemini API] D --> C C --> B B -->|Introduction text| A

The flow is simple: app → Amazon API Gateway → AWS Lambda → Google Gemini API. The Gemini API key lives only on the server side; the app never has it.

Why serverless:

  • I didn't want an always-on server. Introductions are generated only when the button is pressed, so it's enough for something to run only when a request comes in.
  • I can swap what's behind it without changing the app. The app only talks to API Gateway, so which AI the Lambda calls can change without an app update. This turned out to matter a lot when I switched to Gemini (more on that below).
  • I can enforce usage limits on the server. A daily usage limit can live on the server side (see below).

Decision 2: Don't break the "records stay on your device" promise

Adding an AI feature means sending something off the device. KamiMeg's selling point is "no account, records on device," so this is where I was most careful.

Here are the rules I settled on:

  • Only the shrine name and address are sent. That's the minimum needed to write an introduction.
  • They're sent only after the user consents. Without consent, the AI feature isn't used.
  • Goshuin photos, visit records, and location are not sent.
  • No account. I don't ask users to log in for the AI feature either.
  • There's a daily usage limit. It's tracked with a random per-device ID, which isn't information that identifies a person.
  • Generated introductions may be stored on the developer's server for up to 90 days. This is stated explicitly in the privacy policy (PRIVACY.md).

"Generate only when the button is pressed" is part of these rules too. I didn't want a behavior where just opening a screen silently sends data somewhere. Data that the user agreed to share is sent only when they press the button themselves. I think that's easier to explain, and it's more reassuring for the people using the app.

The usage limit also protects my costs. In indie development, if an API keeps getting called unexpectedly, that bill is on me. Being able to hold the limit on the server is another reason I'm glad the app doesn't call the API directly.

Decision 3: Why Gemini

To be honest, I didn't decide on Gemini from the start. At first I also considered Amazon Bedrock and the Claude API as candidates.

But once I actually moved forward, I found that usage limits and payment conditions didn't fit my situation as an indie developer, and I couldn't take it to production as-is. So I switched to the Gemini API, finished the feature, and shipped it to production.

This is where Decision 1 saved me. Because the app only knows about API Gateway, changing which AI gets called was a matter inside the Lambda. Leaving room to change "which AI to use" later is, I think, the reason the switch didn't stall the release.

The lesson I took from this: before comparing model quality, check early whether you can actually use the service in production from your own position. Limits and payment conditions are hard to see when you just call an API a few times to try it out.

Decide what you won't publish

When releasing the app and writing about it, I also decided what not to publish: the API endpoint URL, the app's key, the AWS account and region, and specific numbers for usage limits. None of them appear in this post.

Showing the flow "API Gateway → Lambda → Gemini" in a diagram is fine for explaining how it works. Concrete endpoints and limit values, on the other hand, could become clues for abuse. As an indie developer you sometimes feel like making everything open, but this is where I draw the line.

Caveats and what's next

  • AI-generated introductions aren't guaranteed to be accurate. As the builder, I see them as an "entry point," and I think it's best to check the details with the shrine's official information.
  • Because introductions are generated through the server, the feature assumes the user has consented and is online.
  • Going forward, I want to keep adding features without breaking the app's promises: no account, records stay on the device.

Wrap-up

  • I didn't call the generative-AI API directly from the app. A serverless setup — API Gateway → Lambda → Gemini — keeps the key and the usage limit on the server.
  • Only the shrine name and address are sent, and only after consent. Photos, records, and location are not sent, and there's no account.
  • I considered Bedrock and the Claude API, but switched to Gemini because of limits and payment conditions. Since the app doesn't know what's behind the API, the switch didn't stop the release.

When you put generative AI into an app, I found it pays off to decide "what you send and what you don't" and "whether you can swap it later" before picking a model. I hope this is useful for anyone else building apps on their own.

KamiMeg is available on the App Store (Japanese). If you like visiting shrines, give it a try:
https://apps.apple.com/jp/app/カミめぐ/id6821107881