The Idea
It was November 2024, and I had just wrapped up another intense week of .NET development work. I was sitting at my desk, thinking about a conversation I'd had with my wife, Daniela, about wanting to start our day with scripture but always forgetting to open the Bible app on our phones.
That's when it hit me: What if there was a dead-simple app that just showed you a random Bible verse every time you opened it? No accounts, no complexity, no distractions—just you and the Word.
I'd been wanting to publish something to the App Store for a while, partly to diversify my technical portfolio beyond web development, and partly because I was curious about the mobile app publishing process. This felt like the perfect project: simple enough to actually finish, useful enough to be meaningful, and focused enough to ship quickly.
The Problem I Wanted to Solve:
My Goals: 1. Ship my first mobile app to the App Store 2. Learn React Native and Expo 3. Create something genuinely useful for daily spiritual life 4. Make it bilingual (English/Spanish) from day one 5. Keep it 100% free with no ads or tracking
The Technical Stack
I chose React Native with Expo for a few strategic reasons:
Why React Native:
Why Expo:
Full Stack:
Frontend: React Native 0.81
Language: TypeScript 5.1
Framework: Expo 54
State Management: React Hooks (useState, useEffect)
Storage: AsyncStorage (for offline data)
API: Bolls.life Bible API (free, no auth)
Navigation: Expo Router
Why These Choices Matter:
1. TypeScript: Type safety prevents stupid bugs. In a solo project, you ARE the QA team.
2. AsyncStorage: All user data (favorites, downloaded books, preferences) stored locally. Zero backend costs, maximum privacy.
3. Bolls.life API: After researching 5+ Bible APIs, this one was perfect: - Free and unlimited - Multiple translations - Simple REST endpoints - No authentication required - Support for verse-by-verse fetching (crucial for my offline feature)
4. Expo Managed Workflow: I could build iOS apps from my Windows PC using EAS Build. Game-changer.
The Development Journey
Week 1: MVP (16 hours)
Day 1-2: Core Functionality
I started with the absolute minimum:
// The heart of the app - fetch a random verse
const fetchVerse = async () => {
const response = await fetch(
https://bolls.life/get-random-verse/WEB/
);
const data = await response.json();
const cleanText = data.text.replace(/<[^>]*>/g, ''); // Strip HTML tags
setVerse({
text: cleanText,
reference: ${data.book} ${data.chapter}:${data.verse},
translation_id: 'WEB'
});
};
Key Decision: Strip HTML tags from API responses. The API returns formatted text with and tags. I wanted plain text for simplicity.
Day 3: UI/UX Design
I'm not a designer, so I kept it minimal:
Design Philosophy: Remove everything that doesn't serve the core experience.
Day 4-5: Essential Features
Added three critical features: 1. Favorites: Long-press to save verses 2. Share: Export verses to social media 3. Copy: Quick clipboard copy
const toggleFavorite = async () => {
const verseId = ${verse.reference}-${verse.translation_id};
const newFavorite: SavedVerse = {
...verse,
id: verseId,
savedAt: Date.now()
};
await saveFavorites([newFavorite, ...favorites]);
};
Learning: React Native's Share API is beautiful—one line of code works on both platforms:
await Share.share({
message: ${verse.text}\n\n— ${verse.reference}
});
Week 2: Going Bilingual (12 hours)
The Spanish Challenge:
I wanted Spanish support from day one because:
Implementation:
const translations = {
en: {
title: "Daily Bible Verse",
newVerse: "New Verse",
share: "Share",
copy: "Copy"
},
es: {
title: "Versículo Bíblico Diario",
newVerse: "Nuevo Versículo",
share: "Compartir",
copy: "Copiar"
}
};
Supported Translations:
UI Challenge: Language switcher needed to be intuitive. I used flag emojis (🇺🇸/🇪🇸) with a dropdown. Simple, visual, works everywhere.
Week 3-4: The Offline Feature (20 hours)
This was the hardest part—and the most rewarding.
The Problem: Users wanted to read complete books offline (imagine flights, camping, poor signal areas).
The Challenge: Download entire books verse-by-verse without overwhelming the API.
My Solution:
const downloadBook = async (book: Book) => {
const allVerses: BibleVerse[] = [];
// Loop through each chapter
for (let ch = 1; ch <= book.chapters; ch++) {
let v = 1;
let chapterComplete = false;
while (!chapterComplete && v <= 250) {
const url = https://bolls.life/get-verse/${translation}/${bookId}/${ch}/${v}/;
const res = await fetch(url);
if (!res.ok) {
chapterComplete = true; // 404 = end of chapter
break;
}
const data = await res.json();
allVerses.push({
text: data.text.replace(/<[^>]*>/g, ''),
reference: ${bookName} ${ch}:${v},
// ... more fields
});
v++;
await new Promise(r => setTimeout(r, 50)); // Polite 50ms delay
}
// Update progress bar
setProgress((ch / book.chapters) * 100);
}
// Save to AsyncStorage
await AsyncStorage.setItem(book_${bookName}, JSON.stringify(allVerses));
};
Why This Approach:
1. Polite crawling: 50ms delay between requests (don't abuse the free API!) 2. Chapter-by-chapter: Show progress, allow pause/resume 3. 404 detection: API returns 404 when verses run out (graceful ending) 4. Local storage: Once downloaded, completely offline
Result: Users can download any of the 66 Bible books. Genesis = 1,533 verses, stored in ~300KB. The entire Bible = ~31,000 verses, ~6MB.
UI Polish:
Week 5: Polish & Testing (8 hours)
Responsive Design:
Had to support:
const [dimensions, setDimensions] = useState(Dimensions.get('window'));useEffect(() => { const subscription = Dimensions.addEventListener('change', ({ window }) => { setDimensions(window); }); return () => subscription?.remove(); }, []);
const isSmallDevice = dimensions.width < 375; const isTablet = dimensions.width >= 768;
Dark Mode:
System-aware dark mode with manual override:
const [themeMode, setThemeMode] = useState<'light' | 'dark' | 'system'>('system');
const isDark = themeMode === 'dark' || (themeMode === 'system' && useColorScheme() === 'dark');
Performance Optimization:
1. Lazy loading for favorites (only load when tab opens) 2. Memoized components for verse display 3. Debounced search in downloaded books 4. Image optimization for app icon/splash
Week 6: App Store Submission
The Requirements:
For Apple App Store:
For Google Play:
The Submission Process:
Building the iOS App:
# Configure EAS
eas build:configureBuild for iOS (cloud build, no Mac needed!)
eas build --platform iosSubmit to App Store Connect
eas submit --platform ios
The Review Process:
What I Learned About App Review:
1. Apple is stricter but faster: Clear guidelines, quick turnaround 2. Privacy policy is non-negotiable: Even if you collect zero data, you need one 3. Screenshots matter: I got feedback that mine could be more compelling 4. Keywords are limited: Apple allows 100 characters total. Choose wisely.
The Technical Challenges
Challenge #1: HTML in API Responses
Problem: The API returns verses with HTML formatting:
"For God <em>so</em> loved the world..."
Solution: Regex to strip tags:
const cleanText = text.replace(/<[^>]*>/g, '');
Why not parse HTML? React Native doesn't have a built-in HTML parser, and I didn't want to add a dependency for this simple use case.
Challenge #2: Verse Numbering Inconsistencies
Problem: Different translations have different verse counts (some split verses, some combine them).
Solution: The 404 detection pattern. When the API returns 404, that chapter is done. No hardcoded verse counts needed.
Challenge #3: AsyncStorage Limits
Problem: AsyncStorage has a 6MB limit on Android (10MB on iOS).
Solution:
Challenge #4: Building iOS Without a Mac
Problem: I develop on Windows. Xcode requires macOS.
Solution: Expo EAS Build is magic. Upload your code, they build it on their servers, you get back an IPA file ready for App Store Connect.
eas build --platform ios☁️ Builds in the cloud
📧 Email notification when done
📦 Download IPA or submit directly
Cost: $29/month for EAS priority builds (well worth it).
Challenge #5: Keeping App Size Small
Target: Under 25MB (psychological threshold for cellular downloads)
Strategies: 1. No external fonts (use system fonts) 2. Optimized PNG icons (use TinyPNG) 3. No bundled images beyond icons 4. Tree-shaking unused Expo modules
Result: 23.8 MB final app size ✅
The Results
App Store Metrics (First 6 Weeks)
Downloads:
User Engagement:
Revenue: $0 (completely free, no monetization)
Rating: ⭐⭐⭐⭐⭐ 5.0 stars
Google Play (Closed Testing)
Testers: 12 people (friends, family, church members)
Feedback Highlights:
Requested Features: 1. Search function (most requested) 2. Daily notifications (set time for daily verse) 3. Reading plans (structured Bible reading) 4. Bookmarks (mark position in downloaded books) 5. Verse highlighting/notes
Open Source Impact
GitHub Stats:
Why Open Source: 1. Portfolio showcase (proves I ship code) 2. Community contributions (future features) 3. Learning resource for other devs 4. Transparency (users can verify privacy claims)
Repository: https://github.com/gilricardo-com/daily-bible-verse
What I Learned
Technical Lessons
1. React Native Is Production-Ready
Coming from Angular/web development, I was skeptical. But React Native works. The performance is good, the ecosystem is mature, and Expo removes most of the pain.
2. Mobile UX Is Different
Things I took for granted on web:
3. Offline-First Is Complex
I massively underestimated the offline feature. Edge cases everywhere:
4. APIs Can Be Simple
The Bolls.life API is beautiful in its simplicity:
GET /get-random-verse/WEB/ → Random verse
GET /get-verse/WEB/1/1/1/ → Genesis 1:1
No authentication, no rate limits, no complexity. Sometimes simple is best.
Business Lessons
1. Scope Ruthlessly
My original feature list had 20+ items. I shipped with 6 core features. The rest can wait.
MVP mindset: What's the ONE thing this app must do well?
2. Publish Fast, Iterate Later
I waited too long perfecting things before submitting. Could have launched 2 weeks earlier and gotten real user feedback sooner.
3. App Store Optimization Matters
Bad keyword choices: "bible verse generator" (high competition) Good keyword choices: "daily bible verse offline" (specific, less competition)
Bad screenshot: Just showing the app Good screenshot: Showing the benefit ("Read the Bible offline, anytime")
4. Free Apps Can Succeed Without Monetization
I have zero ads, no subscriptions, no freemium model. Just a useful tool.
Why?
Personal Lessons
1. Shipping Feels Amazing
There's something uniquely satisfying about seeing your app on the App Store. "I made this, and now strangers can use it."
2. Imposter Syndrome Never Goes Away
Even after building web apps professionally for 6+ years, I felt like a fraud publishing a mobile app. "Who am I to do this?"
Do it anyway.
3. Side Projects Are Worth It
This project:
ROI: Priceless.
4. Start Where You Are
I didn't wait until I was an "expert" in mobile development. I had a need, researched for 2 days, and started building.
6 weeks later, I had a published app.
The Tech Stack Deep Dive
Let me break down some key technical decisions:
Why Expo Over Pure React Native?
Expo Pros:
Expo Cons:
Verdict: For 95% of apps, Expo is the right choice. I can always eject later if needed.
State Management: Why No Redux/MobX?
Simple truth: This app doesn't need it.
State I manage:
React hooks handle this perfectly. Adding Redux would be overengineering.
When I'd use Redux:
Navigation: Expo Router
I use Expo Router (file-based routing):
app/
index.tsx → Main screen
favorites.tsx → Favorites view
book-detail.tsx → Single book view
settings.tsx → Settings
Why file-based routing?
Styling: StyleSheet vs Styled Components?
I use plain StyleSheet.create:
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
backgroundColor: isDark ? '#1C1C1C' : '#F5F5F0'
},
verseText: {
fontSize: isSmallDevice ? 20 : 24,
lineHeight: 1.5,
fontFamily: 'System'
}
});
Why not styled-components?
Trade-off: Less flexible, more verbose. But performance > convenience for mobile.
The Features Breakdown
Let me walk you through each feature's implementation:
Feature #1: Random Verse
How it works:
1. Call API: GET https://bolls.life/get-random-verse/WEB/
2. Parse JSON response
3. Strip HTML tags
4. Display verse + reference
Code:
const fetchVerse = async () => {
setLoading(true);
try {
const url = https://bolls.life/get-random-verse/${selectedVersion[language]}/;
const response = await fetch(url);
const data = await response.json();
setVerse({
text: data.text.replace(/<[^>]*>/g, ''),
reference: ${data.book} ${data.chapter}:${data.verse},
translation_id: selectedVersion[language]
});
} catch (error) {
setError(true);
} finally {
setLoading(false);
}
};
Error handling:
Feature #2: Favorites
Storage structure:
interface SavedVerse {
id: string; // "John 3:16-WEB"
text: string;
reference: string;
translation_id: string;
savedAt: number; // timestamp
}
Persisted in AsyncStorage:
await AsyncStorage.setItem('favorites', JSON.stringify(favorites));
UI features:
Feature #3: Share & Copy
Share (native):
const handleShare = async () => {
await Share.share({
message: ${verse.text}\n\n— ${verse.reference} (${verse.translation_id})
});
};
Copy (clipboard):
import * as Clipboard from 'expo-clipboard';
const handleCopy = async () => { await Clipboard.setStringAsync(${verse.text}\n\n— ${verse.reference}); Alert.alert('✓', 'Copied to clipboard'); };
Why both? Share opens system share sheet (WhatsApp, Instagram, etc.). Copy gives quick access for pasting elsewhere.
Feature #4: Multi-Translation Support
Available translations:
English:
Spanish:
Switching translations:
const changeVersion = async (versionCode: string) => {
setSelectedVersion(prev => ({
...prev,
[language]: versionCode
}));
await AsyncStorage.setItem(version_${language}, versionCode);
// Reload current verse in new translation
if (verse) {
fetchSpecificVerse(verse.book, verse.chapter, verse.verse);
}
};
UX detail: When switching translation, keep the same verse (re-fetch with new translation). Users can compare translations side-by-side.
Feature #5: Offline Book Downloads
The algorithm:
const downloadBook = async (book: Book) => {
const bookId = bibleBooks.findIndex(b => b.name === book.name) + 1;
const allVerses: BibleVerse[] = [];
for (let chapter = 1; chapter <= book.chapters; chapter++) {
let verse = 1;
let chapterComplete = false;
while (!chapterComplete) {
const url = https://bolls.life/get-verse/${translation}/${bookId}/${chapter}/${verse}/;
const response = await fetch(url);
if (!response.ok) {
chapterComplete = true; // 404 = no more verses
continue;
}
const data = await response.json();
allVerses.push({
text: data.text.replace(/<[^>]*>/g, ''),
reference: ${book.name} ${chapter}:${verse},
chapter,
verse
});
verse++;
await sleep(50); // Polite delay
}
// Update progress
setProgress((chapter / book.chapters) * 100);
}
// Save to storage
const downloadedBook: DownloadedBook = {
bookName: book.name,
translation,
verses: allVerses,
downloadedAt: Date.now()
};
await AsyncStorage.setItem(
book_${book.name}_${translation},
JSON.stringify(downloadedBook)
);
};
Progress indication:
const progress = (currentChapter / totalChapters) * 100;
// Show progress bar: 0-100%
// Also show verse count updating in real-time
Memory management:
Feature #6: Dark Mode
Implementation:
const [themeMode, setThemeMode] = useState<'light' | 'dark' | 'system'>('system');const isDark = themeMode === 'dark' || (themeMode === 'system' && useColorScheme() === 'dark');
const styles = createStyles(isDark);
Color palette:
Light Mode:
#F5F5F0 (warm off-white)#2C2C2C (dark gray)#4A5D4E (sage green)#8B7355 (warm brown)Dark Mode:
#1C1C1C (true black)#E5E5E5 (light gray)#6B7C6E (muted sage)#A89378 (light brown)Why not pure black? OLED burn-in concerns. Also, pure black (#000000) looks harsh. Slightly off-black is more comfortable.
The App Store Optimization (ASO) Strategy
Title & Subtitle
Title: "Daily Random Bible Verse"
Why this works:
Subtitle (iOS): "Scripture Inspiration & Offline Reading"
Why this works:
Keywords (100 character limit)
My choices:
bible,verse,daily,scripture,offline,spanish,devotional,christian,faith,quotes,king james,rv1960
Research process: 1. Used App Store search suggestions 2. Checked competitor keywords (Bible Gateway, YouVersion) 3. Prioritized low-competition, high-intent keywords 4. Included Spanish terms (bilingual advantage)
Avoided:
Screenshots (6.7" iPhone)
Screenshot 1 - Hero Shot:
Screenshot 2 - Dark Mode:
Screenshot 3 - Offline Feature:
Screenshot 4 - Bilingual:
Screenshot 5 - Features:
Design principles:
App Description
First 170 characters (preview):
Start each day with inspiration from the Bible. Daily Random Bible Verse delivers Scripture in English or Spanish, with complete offline reading.
Why this works:
Full description structure: 1. Hook (2 sentences): What it does, who it's for 2. Key features (bullet list): 6 main features 3. Translations (paragraph): Detail language support 4. Privacy (paragraph): No data collection, no ads 5. Open source (paragraph): Link to GitHub 6. Call to action: "Download now and start your daily journey with Scripture"
Category Selection
Primary: Books & Reference Secondary: Lifestyle
Why not "Reference" only? Too competitive. "Lifestyle" has less competition in Bible app space.
Pricing
Free (no IAP, no subscriptions)
Why? 1. Removes barrier to download 2. Aligns with mission (make Scripture accessible) 3. Portfolio piece (doesn't need to make money) 4. Could add donations later if desired
Localization
Languages supported:
Localized elements:
Impact: Shows up in Spanish App Store searches, appeals to bilingual users.
Roadmap: What's Next
Based on tester feedback and my vision, here's what's coming:
Phase 2: Core Enhancements (Q1 2025)
1. Search Functionality ⭐ Most requested
Find specific verses by:
Technical approach:
2. Daily Notifications
Push notification at user-selected time:
Technical approach:
3. Reading Plans
Structured Bible reading:
UI:
Phase 3: Community Features (Q2 2025)
4. Verse of the Day (Community)
See what verse other users are reading:
Privacy: Anonymous voting, no personal data
5. Sharing Improvements
Enhanced sharing:
6. Reading Streaks
Gamification (light touch):
Phase 4: Premium Features (Q3 2025)
7. Advanced Study Tools
For serious Bible students:
Monetization: $2.99/month or $19.99/year
Why premium? Advanced features require:
8. Cloud Sync
Sync across devices:
Backend: Firebase (free tier sufficient for thousands of users)
Long-Term Vision
The Goal: Not to compete with YouVersion or Bible Gateway (they're amazing).
Instead: Be the simplest, most beautiful, most accessible Bible app for daily inspiration.
Core principles:
For Aspiring App Developers
If you're thinking about building your first mobile app, here's my advice:
1. Start Stupid Simple
Your first app should do ONE thing well. Don't build the next Instagram.
Good first apps:
Bad first apps:
2. Use Expo
Seriously. Don't fight me on this.
Yes, you give up some control. Yes, the app is slightly larger. But you'll ship 10x faster.
Start here:
npx create-expo-app my-first-app
cd my-first-app
npm start
In 2 minutes, you're running an app on your phone.
3. Ship Fast, Learn Faster
My timeline:
Your timeline should be similar.
Don't spend 6 months perfecting before launch. Ship in 4-6 weeks. Get real users. Learn. Iterate.
4. Read These First
Essential reading: 1. React Native Docs - Start here 2. Expo Docs - Your best friend 3. App Store Review Guidelines - Read before building 4. Play Store Policies - Same
Helpful tutorials:
5. Tools I Used
Development:
Design:
Icon/Splash:
Testing:
Deployment:
6. Budget Reality Check
Costs to publish an app:
One-time:
Monthly (if using Expo EAS):
Total first year: $136-$484
My spending: $205 (Apple $99 + Google $25 + Expo $29x3 months)
7. Common Mistakes to Avoid
Mistake #1: Not testing on real devices
Simulators lie. Test on actual phones. Beg, borrow, or rent if you must.
Mistake #2: Ignoring platform differences
iOS and Android are different. Especially:
Mistake #3: Over-engineering
You don't need:
You DO need:
Mistake #4: Perfectionism
Ship the 80% solution. Iterate.
Perfect is the enemy of done.
Mistake #5: Not reading guidelines
Apple and Google reject apps for stupid reasons:
Read. The. Guidelines.
8. Your Launch Checklist
Pre-launch (1 week before):
Launch day:
Post-launch (first week):
Conclusion
Building and publishing my first mobile app was one of the most rewarding experiences of my development career. It combined:
Total time invested: ~60 hours over 6 weeks
Total cost: $205
Result: A published iOS app with real users, open source code showcasing my skills, and invaluable learning experience.
Most importantly: I created something useful. People use it daily for spiritual inspiration. That matters more than downloads or revenue.
Your Turn
Whether you're a seasoned web developer curious about mobile, a bootcamp grad building your portfolio, or someone with an idea and no technical background—you can do this.
Mobile app development is more accessible than ever:
You don't need:
You DO need:
That's it.
---
Links & Resources
App:
Code:
API:
Tools:
Contact:
---
About the Author:
Ricardo Gil is a Full Stack Software Engineer with 6+ years of experience specializing in C#/.NET Core, Angular, and PostgreSQL. Originally from Venezuela, he immigrated to the United States in 2015 and has built scalable applications for healthcare, hospitality, and e-commerce industries. "Daily Random Bible Verse" is his first published mobile application, combining his technical expertise with his faith to create a tool for daily spiritual inspiration.
---
If you found this helpful, please star the GitHub repo and share this post with other aspiring app developers. Your support means the world!
Have questions about the app or mobile development in general? Leave a comment or email me at contact@gilricardo.com.
Related Posts
Building with Angular + Firebase? Check out my Photography E-Commerce Platform that combines Firebase auth with Stripe payments and AWS S3 storage.
Self-hosting your projects? Learn how I host production apps on a Raspberry Pi with PM2, Nginx, and Tailscale.
About the Author: Ricardo Gil is a full-stack software engineer specializing in .NET/C#, Angular, and cloud platforms. Read more or subscribe for updates.
