KOLTA is a shared board for families who live apart, with no chat, no comments, no read receipts, and no like counts. One person subscribes. Everyone they invite uses everything free, forever.

  • Cut a person out of a photo, stick the cutout on a shared board, and the home screen widget on every other member's phone changes within seconds.
  • There is nothing to reply to and nothing to keep up with. No feed, no streaks, no ads.
  • The subscription belongs to the space, not the seat. The person who set the board up pays, and the grandparents and children they invite are free permanently.
  • RevenueCat executes the purchase. The entitlement gate stays on device, Apple-signed and offline-safe.

Targeting: HAMM Award · RevenueCat Peace Prize · RevenueCat Design Award

Inspiration

My grandmother does not use messaging apps. When a message arrives she feels she has to answer it, and answering is work, so photos of her great-grandchildren sit unopened in an app she opens twice a month.

Every family app I looked at answers distance with more conversation: threads, read receipts, likes, streaks. Each one is a small obligation, and obligation is precisely what stops the person we most want to reach. Her problem was never that she lacked a way to reply. It was that everything asked her to.

So the brief was subtractive. Take out the reply, and find out what has to be true for the app to still work.

What survives is one board per space, decorated a little at a time by the people in it, living on the home screen as a widget so that the person who never opens apps still receives what her family made. The Japanese tagline is the whole product in five words: we don't talk, we decorate together.

What it does

You pick a photo and the app lifts the person out of it. You stick that cutout onto a shared board, next to handwriting, sticky notes and stickers. Within seconds, the widget on every other member's home screen changes.

That is the entire interaction. A space holds one active board, and when a board has held a season you start a new one while the old one is kept as a finished piece rather than scrolled past.

The receiving side is the part we care most about. Nobody has to open KOLTA to get what their family made, and nobody is ever told who looked. There is no chat, no comment field, no read receipt, no like count, no feed and no streak. What KOLTA does not have is the product.

How we built it

SwiftUI on iOS 18 with Swift 6 strict concurrency, generated from a single XcodeGen manifest. The backend is Firebase for auth, Firestore and push, with Cloud Functions in TypeScript and image assets in Cloudflare R2 over the S3-compatible API. Cutouts use Vision's foreground instance mask, drawing uses PencilKit, and the widget is WidgetKit with an AppIntent for choosing which space it shows.

The widget is the hard part, because it has to stay current for people who never launch the app. Three independent paths feed one App Group cache: the app composites a snapshot locally when you leave the board screen, a silent push triggers a background refresh when somebody else edits, and the widget's own timeline fetches a server-composited image rendered with sharp in Cloud Functions. If all three fail it shows the last good image, because a family board that goes blank is worse than one that is ten minutes stale.

RevenueCat. Purchases run through the RevenueCat SDK, using purchases-ios 5.85 in StoreKit 2 mode with purchasesAreCompletedBy: .revenueCat. The import RevenueCat appears in exactly one file: PurchaseManager.swift wraps the StoreKit 2 product and calls Purchases.shared.purchase(product:), so the transaction itself is executed by the SDK and lands in the dashboard, where both products and the plus entitlement are imported from App Store Connect.

What we deliberately did not move is the gate. Whether somebody is Plus is still decided on device by StoreKit 2's Transaction.currentEntitlements, which is signed by Apple and works with no network at all. Fifty-three call sites depend on that gate, and putting a network service in front of it would mean a family loses their invited members' seats during a flight or a backend incident. So the split is explicit: the SDK owns the transaction and the revenue picture, the local Apple-signed transaction owns the gate, and the two cannot disagree, because a RevenueCat purchase is a StoreKit purchase on the same Apple ID. If the SDK returns an error, the app re-reads the real entitlement before showing a failure, so a backend hiccup can never tell somebody their successful purchase failed.

Challenges we ran into

We removed the machinery that brings people back, and had to replace it. Streaks, likes and read receipts are not decoration. They are the engine that makes a social app worth opening on an ordinary Tuesday, and we had thrown all of them away on purpose. Then we found the hole they left. Every reason to open KOLTA was somebody else's action, symmetrically for everyone in the space, which is a deadlock: nothing changes until someone moves, and nobody moves until something changes. The only asset that grows without anyone lifting a finger, the family's own accumulated past, was not surfaced anywhere in the product.

The replacement had to work without smuggling the obligation back in. Each day the app picks one item from the space's own history through a deterministic function of the date and the space id, so every member sees the same one with no synchronisation code and no server involved, and it appears at all only once a space has enough history to draw from honestly. It shows up in the widget and in a single evening notification, and only on days when such an item genuinely exists. No streak, no quota, no "you haven't posted today". The board changes on its own, and nobody was asked to perform.

The board froze while you were decorating it. Intermittent hitches, about a second after you stopped moving something. The widget snapshot compositor was annotated @MainActor, and a SwiftUI Task {} started from a view inherits the main actor, so a 1024x1024 ImageRenderer pass plus a multi-megabyte PNG encode and a disk write were all running on the main thread on a 1.2 second debounce. We measured 87ms per encode on a Mac for a photographic image, and a phone is slower. The fix was to delete the debounce rather than tune it: mark the snapshot dirty, and render only when the board screen goes away, since the widget is not on screen while you are looking at the board anyway.

Two renderers had to agree. The widget image is composited on device for you and on the server for everyone else, and Japanese text was the fault line. The Linux container has no Hiragino Sans, fontconfig fell back to Noto Sans CJK, and with no language hint it chose Simplified Chinese glyph forms, so a sticky note could reach your mother in the wrong writing system. A font-family hint and an xml:lang="ja" attribute both failed against the real service. We stopped asking the server to draw text at all: the client rasterises a note to PNG the moment you place it, and the server only pastes. A Diagnostics screen renders both versions side by side, and we check it with characters whose Japanese and Chinese forms differ.

Accomplishments that we're proud of

Six public releases between 5 August and 1 September, with the last three approved on the first review cycle. Every release ships behind 217 unit tests and a 41-case UI suite.

The paywall survived an App Review rejection under Guideline 3.1.2(c) and came out more honest than it went in. Trial length and the exact amount charged afterwards are now pinned in a sticky footer, always visible, rather than sitting below a scroll.

Most of all, the free tier held. In four weeks of shipping under a deadline we never once solved a problem by taking something away from it. Everything needed to reach the moment this product exists for, lifting a person out of a photo and changing somebody else's home screen, is free and unlimited, including every item type and all three widget sizes.

What we learned

A bug that only affects people you have never met will not appear in your own testing. In 1.0.4 we removed a step from the onboarding enum and left its page implementation behind, so the app stopped asking for notification permission anywhere. Every device we owned had already granted it, so everything looked correct to us and was broken only for new installs. It shipped that way for two versions. Permission prompts, first-run paths and invitations all have this shape, and we now test them on a device that has never seen the app.

A gate is not a good thing to outsource. Adding RevenueCat was clearly right for the transaction and for the revenue picture, but the question of who decides whether this user is Plus deserved a different answer from who runs the purchase. Separating the two gave us a dashboard without giving up offline correctness.

Test store keys look exactly like production keys. RevenueCat issues a test_ key for its virtual store and an appl_ key for the App Store app, and the two sit next to each other in the same list. Configuration now rejects anything that is not appl_ and falls back to the direct StoreKit path, so a mis-pasted key can never ship as a store that cannot take money.

Sandbox purchases are invisible on the dashboard by design. Paid subscribers and total revenue count production only, so a perfectly correct integration looks dead for exactly as long as you are testing it. The real check is the customer detail view, and the real proof is a production purchase.

What's next for KOLTA - Decorate Together

The next release hardens the moment a second person joins, which is the one part of this product that cannot be tested alone.

Then instrumentation for the number we actually steer by: the weekly rate of spaces where at least two people joined and both installed the widget. Not conversion, and not installs. A board with one person on it is a photo album, and no amount of revenue would make that the thing we set out to build.

After that, a wider archive boundary if cancellation reasons point at it, and a way to begin a fresh space for a new year without losing the old one. Pricing is tested sequentially rather than in parallel, starting from week eight, and judged ninety days later on trial-to-paid conversion, refund rate and retention rather than on gross revenue. If we ever raise the price, the families who are already here keep theirs.

Built With

  • app-groups
  • app-intents
  • cloud-functions
  • cloudflare-r2
  • cryptokit
  • firebase
  • firebase-auth
  • firebase-cloud-messaging
  • firestore
  • ios
  • node.js
  • pencilkit
  • photokit
  • revenuecat
  • sharp
  • storekit
  • swift
  • swiftui
  • typescript
  • vision
  • widgetkit
  • xcodegen
Share this project:

Updates

Submission history