Building a Modern KJV Bible App with Next.js and Supabase
A technical deep dive into building a full-featured Bible reading app with search, favorites, and topic organization using modern web technologies.
Meta Description
Learn how we built a modern KJV Bible App using Next.js 15, Supabase, and React. Explore the architecture, database design, search implementation, and key features that make scripture accessible and interactive.
Introduction
Reading and studying the Bible should be accessible, fast, and intuitive. That's why we built a modern King James Version (KJV) Bible App that combines the power of Next.js 15 with Supabase's real-time database capabilities.
In this technical breakdown, we'll walk through the architecture, key features, and design decisions behind building a full-featured Bible reading application that handles over 31,000 verses with search, favorites, topics, and contextual reading.
Whether you're a developer looking to build a similar app or simply curious about how modern web technologies can enhance scripture study, this guide will give you an inside look at what it takes to build a production-ready Bible application.
The Tech Stack
We chose a modern, scalable stack that prioritizes performance and developer experience:
Core Framework
- Next.js 15.1.5 (App Router) - Server-side rendering and optimal performance
- React 18.3.1 - Component-based UI architecture
- TypeScript 5.6.3 - Type safety and better developer experience
Styling & UI
- Tailwind CSS 4.1.3 - Utility-first styling
- Radix UI - 20+ accessible, unstyled UI primitives
- Lucide React - Beautiful, consistent icon library
- class-variance-authority - Type-safe component variants
Database & Backend
- Supabase - PostgreSQL database with real-time capabilities
- @supabase/supabase-js 2.75.0 - JavaScript client library
Additional Tools
- react-hook-form - Efficient form management
- sonner - Toast notifications
- next-themes - Dark/light mode support
Database Architecture
Two-Table Design
The app uses a simple but effective two-table structure in Supabase:
1. bible_books Table
Stores metadata about each book of the Bible:
{
book_id: number, // Unique identifier
book_name: string, // E.g., "Genesis", "Matthew"
testament: string, // "Old Testament" or "New Testament"
seq_number: number // Sequential ordering (1-66)
}
This table maintains the canonical order of biblical books and enables efficient navigation.
2. bible_books_and_verses Table
Stores the actual scripture content:
{
book_name: string, // E.g., "Genesis"
book_chapter: number, // Chapter number
verse_number: number, // Verse number
verse_text: string // The actual verse content
}
With over 31,000 verses, this table is queried efficiently using indexed columns for book name and chapter number.
Supabase Configuration
The database client is configured in src/lib/supabase.ts:
import { createClient } from "@supabase/supabase-js";
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
export const supabase = createClient(supabaseUrl, supabaseAnonKey);
Key Features & Implementation
1. Bible Search
Search Method: PostgreSQL ILIKE Pattern Matching
The search uses Supabase's .ilike() method for case-insensitive pattern matching:
const searchVerses = async (query: string) => {
const { data, error } = await supabase
.from("bible_books_and_verses")
.select("book_name, book_chapter, verse_number, verse_text")
.ilike("verse_text", `%${query}%`) // Wildcard pattern matching
.limit(100)
.order("book_name")
.order("book_chapter")
.order("verse_number");
// Transform and return results...
};
Search Characteristics:
- Case-insensitive matching with
%${query}%pattern - Limited to 100 results for performance
- Results ordered by book → chapter → verse
- Returns matching verses with full context
Note: This implementation uses simple pattern matching rather than PostgreSQL full-text search. For future improvements, implementing ts_vector and ts_query would provide more advanced search capabilities.
2. Chapter Reading
Users can browse the Bible by book and chapter with optimized queries:
const fetchChapter = async (book: string, chapter: number) => {
const { data, error } = await supabase
.from("bible_books_and_verses")
.select("book_name, book_chapter, verse_number, verse_text")
.eq("book_name", book) // Filter by book
.eq("book_chapter", chapter) // Filter by chapter
.order("verse_number"); // Sequential ordering
return transformedVerses;
};
Features:
- Fast chapter loading with indexed queries
- Sequential verse ordering
- Loading skeletons for better UX
- Click any verse to expand to full chapter context
3. Contextual Reading
One unique feature is the ability to click any verse to see it in full chapter context:
const handleFetchChapter = async (book: string, chapter: number) => {
const { data, error } = await supabase
.from("bible_books_and_verses")
.select("book_name, book_chapter, verse_number, verse_text")
.eq("book_name", book)
.eq("book_chapter", chapter)
.order("verse_number");
return mappedVerses;
};
This allows users to understand verses in their full biblical context without losing their place.
4. Favorites System
The favorites feature uses client-side state management with React:
// State management
const [favorites, setFavorites] = useState<Set<string>>(new Set());
const [favoriteVerses, setFavoriteVerses] = useState<Verse[]>([]);
// Toggle favorite with unique key
const handleToggleFavorite = (verse: Verse) => {
const key = `${verse.book}_${verse.chapter}_${verse.verse}`;
const newFavorites = new Set(favorites);
if (newFavorites.has(key)) {
newFavorites.delete(key); // Remove from favorites
} else {
newFavorites.add(key); // Add to favorites
}
setFavorites(newFavorites);
};
Key Format: Verses are identified using the pattern "Genesis_1_1" for Genesis 1:1.
Current Limitation: Favorites are stored in React state only and are lost on page refresh. Future enhancement would add localStorage or Supabase persistence.
5. Topics Organization
Users can create custom topics to organize verses by theme:
interface Topic {
id: string; // Timestamp-based ID
title: string;
description: string;
verses: Verse[];
createdAt: Date;
}
const handleCreateTopic = (
topicData: Omit<Topic, "id" | "createdAt">,
) => {
const newTopic: Topic = {
...topicData,
id: Date.now().toString(),
createdAt: new Date(),
};
setTopics([...topics, newTopic]);
};
Features:
- Create custom topics with title and description
- Add verses from existing favorites
- Search and add new verses directly to topics
- Remove verses or delete entire topics
- Share topics with copy/share actions
Current Limitation: Like favorites, topics are session-only and not persisted to the database.
Component Architecture
The app follows a clean, modular component structure:
Main Components
page.tsx (Root Component)
- Central state management for favorites, topics, and navigation
- Orchestrates data flow between all components
- Handles tab routing and user interactions
BibleHeader.tsx (Navigation)
- Tab navigation (Read, Books, Favorites, Topics)
- Search bar with form submission
- Book and chapter selection dropdowns
- Loads Bible books from Supabase on mount
VerseDisplay.tsx (Read Tab)
- Display verses with loading skeletons
- Expandable verse context (click to see full chapter)
- Favorite/copy/share actions per verse
- Highlighted selected verse in context view
BooksExplorer.tsx (Books Tab)
- Three-level navigation: Testament → Chapters → Verses
- Grid layout for chapter selection
- Hardcoded Bible structure (66 books + 18 Apocrypha)
- Full chapter display with verse actions
Favorites.tsx (Favorites Tab)
- Display all favorited verses
- Remove favorites
- Copy/share individual verses
- Empty state prompts
Topics.tsx (Topics Tab)
- Create and manage custom topics
- Add verses from favorites or search
- Inline search for new verses
- Expandable editing interface
- Topic-level and verse-level actions
Custom Hooks
useBibleApi.ts
export function useBibleApi() {
return {
verses: Verse[],
loading: boolean,
error: string | null,
fetchChapter: (book, chapter) => Promise<void>,
searchVerses: (query) => Promise<void>
};
}
Centralizes all database queries and loading states.
Performance Considerations
Current Optimizations
✅ Loading States
- Skeleton loaders prevent layout shift
- Loading indicators for async operations
✅ Efficient State Management
Set<string>for O(1) favorites lookup- Key-based verse identification
✅ Query Limits
- Search limited to 100 results
- Prevents excessive data transfer
✅ Conditional Rendering
- Only active tab component renders
- Reduces unnecessary re-renders
Opportunities for Improvement
❌ No Caching Strategy
- Every navigation refetches from Supabase
- Could implement React Query or SWR for caching
- No localStorage for frequently accessed chapters
❌ Missing Memoization
- Could add
useMemofor expensive computations useCallbackfor event handlers passed to childrenReact.memofor pure components
❌ No Search Debouncing
- Search fires on form submit only
- Could add debounced live search
❌ No Pagination/Virtualization
- All 100 search results render at once
- Could use virtual scrolling for better performance
❌ No Offline Support
- No service worker or PWA capabilities
- Could cache chapters for offline reading
Lessons Learned & Best Practices
What Worked Well
- Simple Database Schema - Two tables are sufficient for a Bible app
- Component Separation - Clear responsibilities make maintenance easy
- TypeScript - Caught many bugs before runtime
- Radix UI - Accessible components out of the box
- Tailwind CSS - Rapid UI development
What We'd Change
- Add Data Persistence - localStorage or Supabase for favorites/topics
- Implement Full-Text Search - PostgreSQL
ts_vectorfor better search - Add Caching - React Query for smarter data management
- Memoize Components - Reduce unnecessary re-renders
- Move Chapter Counts to DB - Remove hardcoded Bible structure
Future Enhancements
Planned Features
- User Authentication - Save favorites and topics to user accounts
- Notes & Highlights - Allow users to annotate verses
- Reading Plans - Guided scripture reading schedules
- Cross-References - Link related verses
- Multiple Translations - Add ESV, NIV, NKJV support
- Audio Bible - Listen to scripture
- Offline Mode - PWA with service worker
- Social Sharing - Share verses with custom images
Technical Improvements
- Implement PostgreSQL full-text search with ranking
- Add React Query for intelligent caching
- Use virtual scrolling for long lists
- Optimize bundle size with code splitting
- Add comprehensive testing (Jest, React Testing Library)
- Implement analytics to understand usage patterns
Conclusion
Building a Bible app with modern web technologies demonstrates how accessible and powerful scripture study can be when combined with thoughtful architecture and user experience design.
Our KJV Bible App handles over 31,000 verses with fast search, intuitive navigation, and organizational features like favorites and topics—all powered by Next.js 15 and Supabase.
While there's always room for improvement (persistence, caching, advanced search), the current implementation provides a solid foundation for a production-ready Bible reading experience.
Try the app: https://kjv-bible-app.vercel.app/
View the source: Available upon request for educational purposes.
About the Author
William Simmons is a full-stack developer specializing in Next.js, React, and modern web applications. He builds solutions that combine technical excellence with user-centered design.
Connect: Contact Premier Dev Solutions
Have questions about building a similar app or need help with your Next.js project? Get in touch to discuss how we can help.