Inspiration
In 1898 a young glassmaker named Pedro Ayala Guerrero arrived in Puebla, Mexico. He learned the trade under Pellandini and spent his life filling Mexican churches with light — stained glass, enameled and fired, the kind you stand under at seven in the morning while somebody two rows ahead prays a rosary out loud.
He was my great-grandfather.
Rezo started as a much smaller idea: the prayer apps my family actually uses are unusable. Banner ads during the rosary. Interstitials between mysteries. Reviews that say so, in Spanish, over and over. Hallow proved that Catholics will pay for a beautiful prayer app — but Hallow is English-first, and the devotions that matter most in a Mexican house (Guadalupe, San Judas Tadeo, the novena for the dead) are second-class citizens there.
So the product question was: what does a prayer app look like if it is designed for that house, in that language, with no ads at all?
The answer to the visual question came from the family. Rezo's identity is stained glass, because that is what the Ayalas made. Every screen is lit like a window; the signature color is a Marian blue, not a startup gradient. That is not art direction I chose off a moodboard — it is inheritance, and it is the reason this app does not look like anything else in the category.
The demo video opens with Pedro's craft and closes dedicated to my father, Gerardo Ayala Helú.
What it does
Rezo is a Catholic prayer app written entirely in Mexican Spanish. That is a product decision, not a limitation: it is the gap the English-first incumbents leave open.
You open it and it already knows what today asks for — no searching, no sign-up, not a single ad.
The rosary, as it is actually prayed. All four sets of mysteries, with today's already selected by day of the week. Bead by bead, advancing with a tap, resuming exactly where you left it. When you have five minutes instead of twenty, there is a single-decade mode. In Premium, the whole rosary is prayed aloud with a guided voice — two voices to choose from — with background audio and ±15s skip.
Novenas. Guadalupe, St. Jude, Sacred Heart, Divine Mercy, St. Joseph and the novena for the departed. Nine days each, with opening prayer, meditation and closing prayer, plus an optional daily reminder while the novena is running. Two are free forever; the other four are Premium.
The saint of the day. Every one of the 366 dates of the year is covered: who they were, why they are remembered, and their prayer — with a browsable calendar and an optional morning notification. This is the retention engine, and it is completely free.
"What do I pray when…?" Twelve moments of a life — fear, grief, gratitude, a hard decision — and what to pray in each, with the source of every text visible on screen.
The prayers everyone knows. Our Father, Hail Mary, Glory Be, Creed, Salve Regina, Angelus, Magnificat, the prayer to St. Michael, the Act of Contrition — with adjustable text size for reading at night or at arm's length.
A prayer intention, kept private. Before a rosary or a novena you can answer one optional question: who are you offering this for? It reappears on the closing screen. It never leaves the phone. And a quiet streak on the home screen for whoever wants it.
Your prayer is yours. No account, no email, no trackers, no analytics — and no backend at all. Every word of content ships inside the binary, so the app works with the phone in airplane mode.
How I built it
Native Swift 5.10 + SwiftUI, iOS 17 minimum, local-first with no server of any kind. The Xcode project is generated from project.yml with XcodeGen and never committed, so the source of truth is a text file.
Monetization is RevenueCat, end to end. purchases-ios via SPM, a single entitlement — premium — and RevenueCat Paywalls rendering a template authored in the dashboard, so the paywall could be redesigned and repriced without shipping a binary. That mattered more than expected: when a rejection landed on the paywall's legal links, the fix went live from the dashboard the same day, with no new build. The paywall never appears at first launch; it appears where the value is, when you open a Premium novena or tap "Escuchar en audio."
Today the offering is two products against that one entitlement: $0.99/month with a 7-day free trial, and $4.99 once, for life (a non-consumable tied to the same premium entitlement).
The content is the real critical path, and it is gated by a human. All devotional text lives in content/**/*.json — 97 files in the release corpus. Traditional prayers are transcribed from verified sources and cite that source in the file; they are never generated, paraphrased or "improved." AI drafts only the mystery meditations and the saints' biographies. Every file carries a reviewed flag that only a human may flip, and a mechanical gate (check-content.mjs --release) refuses to let an unreviewed file into a release build. With no OTA and no backend, that gate is the only safety net there is — a wrong word means a full App Store release to fix it.
The same discipline applied to the art: 133 pieces, signed off one at a time by a person before they could ship.
Accessibility was treated as a build gate, not a nice-to-have: check-contrast.mjs verifies every color pair against WCAG AA in both light and dark mode — 39 pairs green on the shipping build.
The whole project was built by one person orchestrating Claude Code subagents with hard rules written down in the repo: a scope guard bound to the PRD, an ADR log where any expensive-to-reverse decision has to be written before it is implemented, and a rule that no task closes without evidence — a build, a test, or a screenshot.
Challenges I ran into
Version 1.0 took five App Review cycles. Version 1.1 was approved on the first. The difference was not luck, and it is the most useful thing I have to report:
- 3.1.2 — the EULA link. Metadata, not code. Fixed and resubmitted same day.
- 2.1 Information Needed. No defect reported: Apple asked seven questions about a new app, including a recording on a physical device. Answering properly took two videos shot on an iPhone 17 Pro Max.
- 3.1.2(c) — the paywall's legal links pointed at
example.com. The first rejection with a genuine defect in it. Because the paywall is a RevenueCat template, the fix shipped from the dashboard the same day without a build. - Then the queue simply waited: for three days I believed I had a fresh rejection when in fact nothing had been resubmitted. App Store Connect does not change state on its own — somebody has to press the button.
For 1.1 I wrote reviewer notes that named the exact tap path to the paywall, attached a screenshot of the real paywall to the lifetime product, and explained in plain English what "de por vida" means. Approved in under a day.
A bug I only found because I was filming the demo. While recording, the status bar — the clock, the battery — was rendering in black ink on the app's dark screens: 1.75:1 contrast on the audio player against the 4.5:1 AA requires. The obvious hypothesis (a missing .preferredColorScheme(.dark)) was wrong: all five screens had it. preferredColorScheme is a preference, and the one declared highest in the tree wins — the appearance selector added weeks earlier had started declaring one at the root and silently overriding every descendant. It only reproduced if you had touched the selector, which is why no one had seen it. Fixed at the root; measured across 24 combinations; nothing regressed by a single pixel.
Pricing was the decision that cost the most to make. Adding a lifetime purchase contradicted my own PRD, which said no lifetime in v1 precisely because it kills the thing I was here to learn — watching a real subscription renew. It also caught a hidden requirement: the 7-day trial lived on the annual plan, and retiring the annual plan would have quietly deleted a Shipaton requirement. The trial had to be moved to the monthly plan in the same change. Written down as an ADR, with the irreversibility stated out loud: at $4.99 for life against $0.99/month, lifetime amortizes in about five months, so in practice it caps lifetime revenue per user — forever.
Accomplishments that I'm proud of
- Rezo is live on the App Store, in Mexico and the United States. 1.0 shipped 23 August, 20 days before my own deadline; 1.1 was approved on the first try on 31 August.
- A visual identity nobody else can copy, because it is a hundred and twenty-eight years old and it belongs to my family.
- Not one ad, not one tracker, not one analytics SDK, not one line of backend. The app cannot phone home because there is nowhere to phone.
- 97 content files and 133 pieces of art, every one signed off by a human before it could ship. In a category where a paraphrased prayer is a real failure, that gate is the product.
- 366 of 366 days of the calendar of saints covered, in Spanish, offline.
- WCAG AA verified mechanically on every color pair, in light and dark.
What I learned
- RevenueCat Paywalls earned their keep in the one situation that matters: a rejection on paywall content, fixed and live the same day, without a binary. If that copy had been compiled into the app it would have cost a full review cycle.
- A free app cannot give judges premium access with a promo code. App promo codes only grant the download — which is already free. What actually unlocks Premium is an offer code on the paid product, which is what
SHIPATON2026is. - A trial belongs to a plan, not to an app. Retire the plan and the trial goes with it.
- Reviewer notes are a feature. Five cycles versus one, same app.
- Store screenshots must be regenerable by a script. Mine were made by hand in August and when the visual identity changed nobody remembered the launch arguments or the type sizes. Now one command relaunches the app with debug flags, photographs real states, and reads the palette straight out of
Tokens.swift— there is not one hand-written hex in it. No pixel of UI is ever edited. - Write the expensive decisions down before making them. Every reversal in this project that hurt was one I had already argued against in my own document.
What's next for Rezo
- Verify the first real production purchase. Everything tested so far ran in sandbox. It is the one part of the flow that changes when the app goes live, and it is the next thing on the list.
- Android. A second target exists and shares the same content corpus; the clock there is Google's closed-testing requirement — 12 testers for 14 days.
- Clear the review queue. One piece of content (a guided nightly examen) is parked outside the bundle because a human has not signed it yet. It ships when it is signed, and not before.
Built With
- avfoundation
- claude-code
- ios
- json
- purchases-ios
- revenuecat
- revenuecat-paywalls
- storekit
- swift
- swiftui
- usernotifications
- xcode
- xcodegen
Log in or sign up for Devpost to join the conversation.