Inspiration

Every AI photo app gives you the same thing: a fixed menu of filters that somebody at the company chose. You get twenty looks, everyone gets the same twenty, and when you find one you love you cannot pass it on — only the picture.

That felt backwards. The interesting part of a generated image is not the image, it is the style behind it, and styles are written by people. So Sayro makes the style the unit: you write one, publish it, and anyone can apply it to their own photo. What they cannot do is read it. The prompt stays with whoever wrote it.

What it does

Sayro turns a photo of you into a styled portrait.

Pick a style, add your photo, and you get back a portrait that still looks like you — in a style someone invented and shared. Or write your own description and make something nobody has made before.

The part that makes it different: every creation in Sayro comes from a style, and a style is something a person makes. Write the idea, publish it, and anyone can use it on their own photo. Your name stays on it. The styles people use most rise in Explore.

Using someone's style never reveals their prompt. That is not a UI decision — no consumer-facing model in the app carries a prompt field at all, and the vault the prompts live in is deny-all to clients. Generation reads it server-side and returns only the image.

  • Turn your photo into a styled portrait
  • Write your own style and publish it for others to use
  • Browse Explore to see what people are making and which styles are catching on
  • Follow creators, like and save work
  • Keep creations private, or share only what you want seen
  • Set your profile photo from one of your own creations

Generating costs coins. Every new account is granted 150 coins — five generations — so the whole app can be used before spending anything. Coin packs are one-time purchases: no subscription, no auto-renewal, and coins never expire.

How we built it

One codebase, two platforms. Kotlin Multiplatform with Compose Multiplatform UI shared across Android and iOS — no per-platform screens. Kotlin 2.4, AGP 9, multi-module with Gradle convention plugins in build-logic/, MVVM over Clean Architecture, Koin for DI, type-safe Compose Navigation.

Firebase through the native SDKs, not a wrapper. The Android SDK in androidMain, the iOS SDK bound through Kotlin 2.4's Swift Package Manager cinterop in iosMain, joined with expect/actual behind domain interfaces. Auth (Google and phone), Firestore, Storage, Cloud Functions, App Check and Crashlytics. No Firebase type escapes the data module.

Money is a ledger, never a counter. Purchases run on RevenueCat through purchases-kmp, declared once in commonMain. Buying a pack goes: Play Billing → RevenueCat receipt validation → a non_renewing_purchase webhook → a Cloud Function that writes an idempotent row into coinLedger keyed by the store transaction id → a wallet listener that pushes the new balance to the client. Signup grant, generation spend, refund-on-failure and purchase are all ledger entries. The balance is a projection of that ledger, and coins are bucketed by origin — granted, purchased, earned — because earned coins must never become withdrawable money.

The backend does the work that cannot be trusted to a client. Cloud Functions in TypeScript: generation orchestration, server-side moderation of every image and every public text field before publication, trending-score recomputation, a sweeper that fails stuck generations and refunds their coins, and a reconciler for purchases that completed while the app was being killed.

Design is a constraint, not a theme. The interface is monochrome on purpose — black and white chrome so the only colour on any screen comes from the pictures people make.

Challenges we ran into

Making prompts genuinely unreadable, not merely hidden. "The prompt never reaches a client" sounds like a rule about an API response. In practice it reshaped the data model: consumer-facing models cannot carry the field at all, the vault collection is deny-all, and every feature that touches a style had to be designed around not having it. It is the single constraint that influenced the architecture most.

Paying and receiving nothing is the only unrecoverable bug. Everything else can be fixed in a patch. So the ledger is idempotent per transaction id, a failed generation refunds itself, and a scheduled reconciler catches purchases that completed while the process was dying. None of it is visible in the UI, and all of it exists because the alternative is taking someone's money and giving them nothing.

Firebase on iOS through Swift-PM cinterop is new ground. Choosing the native SDK over a community wrapper meant breakages with no search results — most recently a toolchain issue where the linker shim assumed one compiler driver while a new Xcode used another. The upside is that both platforms talk to the real SDK, with no lowest-common-denominator layer in between.

App Check fails one service at a time. Enabling enforcement surfaced first as a sign-in failure, then as a Firestore permission error, then as a storage upload 401 — three unrelated-looking bugs, one cause, each reaching the user as the same generic message. The lesson was to read the nested error rather than the SDK's top-level one: the storage exception says "user is not authenticated" while the real cause, several frames down, is an invalid App Check token.

Store propagation is a phase of shipping. In-app products created correctly in Play Console returned nothing to the device for hours. With every id, price and region right, that is indistinguishable from a bug unless you already know to wait.

Accomplishments that we're proud of

Shipped to Google Play as a real, paid-capable product rather than a demo: store listing, content rating, data safety, a live privacy policy and terms, an account-deletion flow that actually deletes, and a money path verified end to end — catalog, purchase, credit, repeat purchase, ledger row — before a single real user saw it.

One codebase for two platforms with no per-platform UI, and a Firebase integration that goes through the native SDKs on both sides.

And the thing that started it: a creator economy where the creator's work is genuinely protected. You can use anyone's style. You can never read it.

What we learned

The last ten percent is the store, not the code. The build was finished days before the app was shippable. What remained was content ratings, data-safety answers that must agree with a live privacy policy, an account-deletion URL, service-account credentials scoped correctly enough to validate receipts — and a purchase flow that cannot be tested at all until the app is installed from the store rather than from the IDE.

We also learned to distrust the device when something is empty. Twice the answer was in a console, not in the app: an empty catalog and a silent purchase failure both looked like client bugs and both were configuration.

What's next for Sayro

iOS. The app is Kotlin Multiplatform and the iOS target builds today; it has not been submitted to the App Store yet.

Then creator payouts — style authors earning when their styles get reused, which is the economy the whole style-reuse model was built for. After that, opening beyond India, which means expanding the phone-auth SMS region policy, and turning App Check enforcement back on once production traffic shows clean attestation.

Built With

  • android
  • clean-architecture
  • cloud-firestore
  • cloud-functions
  • compose-multiplatform
  • crashlytics
  • firebase
  • firebase-app-check
  • firebase-auth
  • firebase-storage
  • generative-ai
  • google-play-billing
  • gradle
  • in-app-purchases
  • jetpack-compose
  • koin
  • kotlin
  • kotlin-multiplatform
  • material-design
  • mvvm
  • openai
  • revenuecat
  • swift-package-manager
  • typescript
Share this project:

Updates

posted an update —

Sayro is now on the web: sayro.in

Since submitting the Android app, we've shipped the full Sayro app in the browser. It's not just a landing page. You can sign in, browse Home and Explore, make a creation, publish a style, share, like, follow, and check your coins.

How it's built

  • Compose Multiplatform for Web on Kotlin/Wasm, written in Kotlin like the mobile app.
  • The data layer, models and coin rules are reused from the KMP app. The UI is new and made for the web, not a phone screen in a frame.
  • Firebase, with App Check, on the same backend as the app. One account and the same coins everywhere.
  • Plain HTML snapshots for fast first paint and link previews.

Where Sayro is today

  • Android: live on Google Play
  • Web: live at sayro.in since 10 October 2026
  • iPhone: in App Store review, coming soon

Every image, your style.

Log in or sign up for Devpost to join the conversation.

Submission history