# Payload CMS Implementation Plan for Biblical Guide ## Executive Summary This document outlines a comprehensive plan to migrate Biblical Guide from its current Prisma/PostgreSQL backend to Payload CMS, leveraging Payload's built-in authentication system and payment handling capabilities. ## Table of Contents 1. [Overview & Benefits](#overview--benefits) 2. [Migration Strategy](#migration-strategy) 3. [Phase 1: Setup & Configuration](#phase-1-setup--configuration) 4. [Phase 2: Data Models Migration](#phase-2-data-models-migration) 5. [Phase 3: Authentication System](#phase-3-authentication-system) 6. [Phase 4: Payment System Integration](#phase-4-payment-system-integration) 7. [Phase 5: API Migration](#phase-5-api-migration) 8. [Phase 6: Admin Panel Transition](#phase-6-admin-panel-transition) 9. [Phase 7: Testing & Deployment](#phase-7-testing--deployment) 10. [Risk Analysis & Mitigation](#risk-analysis--mitigation) 11. [Timeline & Resources](#timeline--resources) ## Overview & Benefits ### Why Payload CMS? Payload CMS offers significant advantages for Biblical Guide: 1. **Next.js Native Integration**: Direct installation in existing `/app` folder 2. **Built-in Authentication**: Robust auth system with JWT, sessions, and role-based access 3. **Admin Panel**: Production-ready admin interface with no additional development 4. **TypeScript First**: Automatic type generation for all data models 5. **Flexible Database**: Supports PostgreSQL (current database) 6. **Internationalization**: Built-in i18n support for multi-language content 7. **Media Management**: Advanced file upload and image optimization 8. **No Vendor Lock-in**: Open-source and self-hosted ### Key Benefits for Biblical Guide - **Reduced Development Time**: 60-70% reduction in custom admin panel development - **Enhanced Security**: Enterprise-grade authentication out of the box - **Better Content Management**: Rich text editor and block-based layouts - **Simplified API Development**: Auto-generated REST and GraphQL APIs - **Improved Developer Experience**: TypeScript types and React components ## Migration Strategy ### Approach: Parallel Development with Phased Cutover We'll implement Payload CMS alongside the existing system, allowing for: - Zero downtime migration - Gradual feature migration - Rollback capability - A/B testing opportunities ### Migration Principles 1. **Data Integrity First**: No data loss during migration 2. **Feature Parity**: Maintain all existing functionality 3. **Progressive Enhancement**: Add new capabilities where beneficial 4. **User Transparency**: Seamless experience for end users ## Phase 1: Setup & Configuration ### 1.1 Initial Setup (Week 1) ```bash # Install Payload CMS in the existing Next.js project npx create-payload-app@latest --use-npm ``` ### 1.2 Configuration Structure ```typescript // payload.config.ts import { buildConfig } from 'payload/config'; import { postgresAdapter } from '@payloadcms/db-postgres'; import { lexicalEditor } from '@payloadcms/richtext-lexical'; import { stripePlugin } from '@payloadcms/plugin-stripe'; export default buildConfig({ serverURL: process.env.PAYLOAD_PUBLIC_SERVER_URL, admin: { user: 'users', bundler: '@payloadcms/bundler-webpack', meta: { titleSuffix: '- Biblical Guide Admin', favicon: '/favicon.ico', ogImage: '/og-image.png', }, }, editor: lexicalEditor(), db: postgresAdapter({ pool: { connectionString: process.env.DATABASE_URL, }, }), collections: [ // Collections will be defined in Phase 2 ], globals: [ // Global configs (settings, navigation, etc.) ], typescript: { outputFile: 'types/payload-types.ts', }, graphQL: { schemaOutputFile: 'generated-schema.graphql', }, plugins: [ stripePlugin({ stripeSecretKey: process.env.STRIPE_SECRET_KEY, webhookEndpointSecret: process.env.STRIPE_WEBHOOK_SECRET, }), ], localization: { locales: ['en', 'ro', 'es', 'it'], defaultLocale: 'en', fallback: true, }, }); ``` ### 1.3 Environment Variables Update ```env # Payload CMS Configuration PAYLOAD_SECRET=your-payload-secret-key PAYLOAD_PUBLIC_SERVER_URL=https://biblical-guide.com PAYLOAD_PUBLIC_FRONTEND_URL=https://biblical-guide.com # Database (existing) DATABASE_URL=postgresql://... # Stripe (existing) STRIPE_SECRET_KEY=... STRIPE_WEBHOOK_SECRET=... ``` ## Phase 2: Data Models Migration ### 2.1 Collection Definitions Transform existing Prisma models to Payload collections: #### Users Collection ```typescript // collections/Users.ts import { CollectionConfig } from 'payload/types'; export const Users: CollectionConfig = { slug: 'users', auth: { tokenExpiration: 604800, // 7 days (matching current) cookies: { secure: true, sameSite: 'strict', }, forgotPassword: { generateEmailHTML: ({ token, user }) => { // Custom email template }, }, }, admin: { useAsTitle: 'email', defaultColumns: ['email', 'name', 'role', 'createdAt'], }, fields: [ { name: 'name', type: 'text', required: true, }, { name: 'email', type: 'email', required: true, unique: true, }, { name: 'role', type: 'select', options: [ { label: 'User', value: 'USER' }, { label: 'Admin', value: 'ADMIN' }, { label: 'Super Admin', value: 'SUPER_ADMIN' }, ], defaultValue: 'USER', required: true, }, { name: 'favoriteVersion', type: 'select', options: [ { label: 'Cornilescu', value: 'VDC' }, { label: 'NASB', value: 'NASB' }, { label: 'RVR', value: 'RVR' }, { label: 'NR', value: 'NR' }, ], defaultValue: 'VDC', }, { name: 'subscription', type: 'relationship', relationTo: 'subscriptions', hasMany: false, }, { name: 'stripeCustomerId', type: 'text', unique: true, admin: { position: 'sidebar', readOnly: true, }, }, { name: 'profileSettings', type: 'group', fields: [ { name: 'fontSize', type: 'number', defaultValue: 16 }, { name: 'theme', type: 'select', options: ['light', 'dark'] }, { name: 'showVerseNumbers', type: 'checkbox', defaultValue: true }, { name: 'enableNotifications', type: 'checkbox', defaultValue: true }, ], }, ], hooks: { beforeChange: [ async ({ data, operation }) => { if (operation === 'create' && data.password) { // Password will be hashed automatically by Payload } return data; }, ], afterChange: [ async ({ doc, operation }) => { if (operation === 'create') { // Create default user settings // Track user creation analytics } return doc; }, ], }, }; ``` #### Subscriptions Collection ```typescript // collections/Subscriptions.ts export const Subscriptions: CollectionConfig = { slug: 'subscriptions', admin: { useAsTitle: 'planName', defaultColumns: ['user', 'planName', 'status', 'currentPeriodEnd'], }, fields: [ { name: 'user', type: 'relationship', relationTo: 'users', required: true, unique: true, }, { name: 'stripeSubscriptionId', type: 'text', unique: true, required: true, }, { name: 'planName', type: 'select', options: [ { label: 'Free', value: 'free' }, { label: 'Premium', value: 'premium' }, ], required: true, }, { name: 'status', type: 'select', options: [ { label: 'Active', value: 'active' }, { label: 'Cancelled', value: 'cancelled' }, { label: 'Past Due', value: 'past_due' }, { label: 'Trialing', value: 'trialing' }, ], required: true, }, { name: 'currentPeriodEnd', type: 'date', required: true, }, { name: 'conversationCount', type: 'number', defaultValue: 0, admin: { description: 'Monthly conversation count for free tier', }, }, ], hooks: { beforeChange: [ async ({ data, operation }) => { // Reset conversation count on new billing period if (operation === 'update' && data.currentPeriodEnd) { const now = new Date(); const periodEnd = new Date(data.currentPeriodEnd); if (periodEnd > now && data.planName === 'free') { data.conversationCount = 0; } } return data; }, ], }, }; ``` ### 2.2 Bible Data Collections ```typescript // collections/BibleBooks.ts export const BibleBooks: CollectionConfig = { slug: 'bible-books', admin: { useAsTitle: 'name', group: 'Bible Content', }, fields: [ { name: 'bookId', type: 'number', required: true, unique: true }, { name: 'name', type: 'text', required: true, localized: true }, { name: 'abbreviation', type: 'text', required: true }, { name: 'testament', type: 'select', options: ['OT', 'NT'], required: true }, { name: 'chapterCount', type: 'number', required: true }, { name: 'order', type: 'number', required: true }, ], }; // collections/BibleVerses.ts export const BibleVerses: CollectionConfig = { slug: 'bible-verses', admin: { useAsTitle: 'reference', group: 'Bible Content', pagination: { defaultLimit: 50, }, }, fields: [ { name: 'book', type: 'relationship', relationTo: 'bible-books', required: true, }, { name: 'chapter', type: 'number', required: true }, { name: 'verse', type: 'number', required: true }, { name: 'text', type: 'textarea', required: true, localized: true }, { name: 'version', type: 'text', required: true }, { name: 'embedding', type: 'json', admin: { hidden: true, // Hide from UI but available in API }, }, { name: 'reference', type: 'text', admin: { readOnly: true, }, hooks: { beforeChange: [ ({ data, siblingData }) => { // Auto-generate reference return `${siblingData.book} ${data.chapter}:${data.verse}`; }, ], }, }, ], indexes: [ { fields: { book: 1, chapter: 1, verse: 1, version: 1 }, unique: true, }, ], }; ``` ## Phase 3: Authentication System ### 3.1 Migration Strategy 1. **Parallel Authentication**: Support both JWT (existing) and Payload auth during transition 2. **User Migration**: Batch migrate existing users with password reset option 3. **Session Management**: Implement Payload's cookie-based sessions 4. **Role Mapping**: Map existing roles to Payload's access control ### 3.2 Authentication Hooks ```typescript // auth/hooks.ts export const authHooks = { // Custom login hook to maintain compatibility afterLogin: async ({ user, req }) => { // Log user activity await logUserActivity({ userId: user.id, action: 'login', ip: req.ip, }); // Sync with existing analytics await updateAnalytics({ event: 'user_login', userId: user.id, }); return user; }, // Validate subscription status afterRead: async ({ doc, req }) => { if (doc.subscription) { const subscription = await validateSubscription(doc.subscription); doc.subscriptionActive = subscription.status === 'active'; } return doc; }, }; ``` ### 3.3 Access Control Implementation ```typescript // access/userAccess.ts export const userAccess = { read: ({ req: { user } }) => { if (user) { return { or: [ { id: { equals: user.id } }, // Users can read their own data { 'role': { equals: 'ADMIN' } }, // Admins can read all ], }; } return false; }, update: ({ req: { user } }) => { if (user) { return { or: [ { id: { equals: user.id } }, // Users can update their own data { 'role': { equals: 'ADMIN' } }, ], }; } return false; }, delete: ({ req: { user } }) => { // Only admins can delete users return user?.role === 'ADMIN'; }, }; ``` ## Phase 4: Payment System Integration ### 4.1 Stripe Plugin Configuration ```typescript // plugins/stripe.config.ts import { stripePlugin } from '@payloadcms/plugin-stripe'; export const stripeConfig = stripePlugin({ stripeSecretKey: process.env.STRIPE_SECRET_KEY, webhookEndpointSecret: process.env.STRIPE_WEBHOOK_SECRET, webhooks: { 'checkout.session.completed': async ({ event, stripe, payload }) => { const session = event.data.object; // Create or update subscription await payload.create({ collection: 'subscriptions', data: { user: session.metadata.userId, stripeSubscriptionId: session.subscription, planName: session.metadata.planName, status: 'active', currentPeriodEnd: new Date(session.expires_at * 1000), }, }); }, 'customer.subscription.updated': async ({ event, stripe, payload }) => { const subscription = event.data.object; await payload.update({ collection: 'subscriptions', where: { stripeSubscriptionId: { equals: subscription.id }, }, data: { status: subscription.status, currentPeriodEnd: new Date(subscription.current_period_end * 1000), }, }); }, }, sync: [ { collection: 'products', stripeResourceType: 'products', stripeResourceTypeSingular: 'product', fields: [ { field: 'name', property: 'name' }, { field: 'description', property: 'description' }, { field: 'active', property: 'active' }, ], }, { collection: 'prices', stripeResourceType: 'prices', stripeResourceTypeSingular: 'price', fields: [ { field: 'product', property: 'product' }, { field: 'unitAmount', property: 'unit_amount' }, { field: 'currency', property: 'currency' }, { field: 'recurring', property: 'recurring' }, ], }, ], }); ``` ### 4.2 Payment Collections ```typescript // collections/Products.ts export const Products: CollectionConfig = { slug: 'products', admin: { useAsTitle: 'name', group: 'E-commerce', }, fields: [ { name: 'name', type: 'text', required: true }, { name: 'description', type: 'textarea' }, { name: 'stripeProductId', type: 'text', unique: true }, { name: 'active', type: 'checkbox', defaultValue: true }, { name: 'features', type: 'array', fields: [ { name: 'feature', type: 'text' }, ], }, ], }; // collections/Donations.ts export const Donations: CollectionConfig = { slug: 'donations', admin: { useAsTitle: 'donorName', group: 'E-commerce', }, fields: [ { name: 'donorName', type: 'text', required: true }, { name: 'donorEmail', type: 'email', required: true }, { name: 'amount', type: 'number', required: true }, { name: 'currency', type: 'text', defaultValue: 'USD' }, { name: 'stripePaymentIntentId', type: 'text', unique: true }, { name: 'status', type: 'select', options: ['pending', 'completed', 'failed'] }, { name: 'message', type: 'textarea' }, { name: 'anonymous', type: 'checkbox', defaultValue: false }, ], }; ``` ## Phase 5: API Migration ### 5.1 API Route Mapping | Current Route | Payload Equivalent | Migration Notes | |--------------|-------------------|-----------------| | `/api/auth/login` | `/api/users/login` | Built-in with Payload | | `/api/auth/register` | `/api/users` (POST) | Built-in with Payload | | `/api/auth/me` | `/api/users/me` | Built-in with Payload | | `/api/bible/verses` | `/api/bible-verses` | REST API auto-generated | | `/api/bookmarks` | `/api/bookmarks` | Custom collection | | `/api/prayers` | `/api/prayers` | Custom collection | | `/api/subscriptions` | `/api/subscriptions` | Stripe plugin handles | ### 5.2 Custom Endpoints ```typescript // endpoints/customEndpoints.ts export const customEndpoints = [ { path: '/api/bible/search', method: 'post', handler: async (req, res) => { const { query, version, locale } = req.body; // Implement vector search using Payload's database adapter const results = await searchBibleVerses({ query, version, locale, limit: 20, }); return res.json({ results }); }, }, { path: '/api/daily-verse', method: 'get', handler: async (req, res) => { const verse = await getDailyVerse(req.query.locale); return res.json(verse); }, }, ]; ``` ### 5.3 GraphQL Schema ```graphql # Auto-generated GraphQL schema additions type Query { searchBibleVerses(query: String!, version: String, locale: String): [BibleVerse] dailyVerse(locale: String): BibleVerse userStats(userId: ID!): UserStatistics } type Mutation { createBookmark(verseId: ID!, note: String): Bookmark generatePrayer(category: String!, locale: String!): Prayer updateReadingProgress(planId: ID!, day: Int!): ReadingProgress } type Subscription { verseOfTheDay: BibleVerse prayerRequests: [Prayer] } ``` ## Phase 6: Admin Panel Transition ### 6.1 Admin UI Customization ```typescript // admin/components/Dashboard.tsx import React from 'react'; import { AdminViewComponent } from 'payload/config'; export const Dashboard: AdminViewComponent = () => { return (