Includes all Phase 1 features: - Search-first navigation with auto-complete - Responsive reading interface (desktop/tablet/mobile) - 4 customization presets + full fine-tuning controls - Layered details panel with notes, bookmarks, highlights - Smart offline caching with IndexedDB and auto-sync - Full accessibility (WCAG 2.1 AA) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1093 lines
28 KiB
Markdown
1093 lines
28 KiB
Markdown
# 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 (
|
|
<div>
|
|
<h1>Biblical Guide Admin Dashboard</h1>
|
|
<div className="dashboard-grid">
|
|
<StatsCard title="Total Users" metric={userCount} />
|
|
<StatsCard title="Active Subscriptions" metric={subscriptionCount} />
|
|
<StatsCard title="Daily Active Users" metric={dauCount} />
|
|
<StatsCard title="Revenue This Month" metric={monthlyRevenue} />
|
|
</div>
|
|
<RecentActivity />
|
|
<SystemHealth />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// admin/views.ts
|
|
export const adminViews = [
|
|
{
|
|
path: '/admin/dashboard',
|
|
Component: Dashboard,
|
|
exact: true,
|
|
},
|
|
{
|
|
path: '/admin/analytics',
|
|
Component: AnalyticsView,
|
|
},
|
|
{
|
|
path: '/admin/content-moderation',
|
|
Component: ContentModerationView,
|
|
},
|
|
];
|
|
```
|
|
|
|
### 6.2 Admin Permissions
|
|
|
|
```typescript
|
|
// admin/access.ts
|
|
export const adminAccess = {
|
|
admin: ({ req: { user } }) => {
|
|
return user && ['ADMIN', 'SUPER_ADMIN'].includes(user.role);
|
|
},
|
|
|
|
// Granular permissions
|
|
collections: {
|
|
users: {
|
|
read: ({ req: { user } }) => user?.role === 'ADMIN',
|
|
update: ({ req: { user } }) => user?.role === 'SUPER_ADMIN',
|
|
delete: ({ req: { user } }) => user?.role === 'SUPER_ADMIN',
|
|
},
|
|
subscriptions: {
|
|
read: ({ req: { user } }) => user?.role === 'ADMIN',
|
|
update: ({ req: { user } }) => user?.role === 'ADMIN',
|
|
delete: false, // Never delete subscription records
|
|
},
|
|
},
|
|
};
|
|
```
|
|
|
|
## Phase 7: Testing & Deployment
|
|
|
|
### 7.1 Testing Strategy
|
|
|
|
#### Unit Testing
|
|
```typescript
|
|
// tests/collections/users.test.ts
|
|
describe('Users Collection', () => {
|
|
it('should hash passwords on creation', async () => {
|
|
const user = await payload.create({
|
|
collection: 'users',
|
|
data: {
|
|
email: 'test@example.com',
|
|
password: 'plaintext',
|
|
name: 'Test User',
|
|
},
|
|
});
|
|
|
|
expect(user.password).not.toBe('plaintext');
|
|
expect(user.password).toMatch(/^\$2[aby]\$\d{2}\$/); // bcrypt pattern
|
|
});
|
|
|
|
it('should enforce unique email constraint', async () => {
|
|
await expect(payload.create({
|
|
collection: 'users',
|
|
data: {
|
|
email: 'duplicate@example.com',
|
|
password: 'password',
|
|
name: 'Duplicate User',
|
|
},
|
|
})).rejects.toThrow('Unique constraint violation');
|
|
});
|
|
});
|
|
```
|
|
|
|
#### Integration Testing
|
|
```typescript
|
|
// tests/integration/auth.test.ts
|
|
describe('Authentication Flow', () => {
|
|
it('should login and receive token', async () => {
|
|
const response = await request(app)
|
|
.post('/api/users/login')
|
|
.send({
|
|
email: 'user@example.com',
|
|
password: 'password',
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toHaveProperty('token');
|
|
expect(response.body.user.email).toBe('user@example.com');
|
|
});
|
|
});
|
|
```
|
|
|
|
### 7.2 Data Migration Script
|
|
|
|
```typescript
|
|
// scripts/migrate-to-payload.ts
|
|
import { PrismaClient } from '@prisma/client';
|
|
import payload from 'payload';
|
|
|
|
const prisma = new PrismaClient();
|
|
|
|
async function migrateUsers() {
|
|
console.log('Migrating users...');
|
|
const users = await prisma.user.findMany({
|
|
include: {
|
|
subscription: true,
|
|
settings: true,
|
|
},
|
|
});
|
|
|
|
for (const user of users) {
|
|
try {
|
|
await payload.create({
|
|
collection: 'users',
|
|
data: {
|
|
id: user.id,
|
|
email: user.email,
|
|
name: user.name,
|
|
role: user.role,
|
|
stripeCustomerId: user.stripeCustomerId,
|
|
favoriteVersion: user.favoriteVersion,
|
|
createdAt: user.createdAt,
|
|
// Note: Passwords need special handling
|
|
password: user.password, // Already hashed
|
|
},
|
|
});
|
|
|
|
if (user.subscription) {
|
|
await payload.create({
|
|
collection: 'subscriptions',
|
|
data: {
|
|
user: user.id,
|
|
stripeSubscriptionId: user.subscription.stripeSubscriptionId,
|
|
planName: user.subscription.planName,
|
|
status: user.subscription.status,
|
|
currentPeriodEnd: user.subscription.currentPeriodEnd,
|
|
},
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error(`Failed to migrate user ${user.email}:`, error);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function migrateBibleData() {
|
|
console.log('Migrating Bible data...');
|
|
// Similar migration for Bible verses, books, etc.
|
|
}
|
|
|
|
async function main() {
|
|
await payload.init({
|
|
local: true,
|
|
});
|
|
|
|
await migrateUsers();
|
|
await migrateBibleData();
|
|
// ... other migrations
|
|
|
|
console.log('Migration complete!');
|
|
process.exit(0);
|
|
}
|
|
|
|
main().catch(console.error);
|
|
```
|
|
|
|
### 7.3 Deployment Configuration
|
|
|
|
```yaml
|
|
# docker-compose.yml
|
|
version: '3.8'
|
|
|
|
services:
|
|
app:
|
|
build: .
|
|
ports:
|
|
- "3000:3000"
|
|
environment:
|
|
- DATABASE_URL=${DATABASE_URL}
|
|
- PAYLOAD_SECRET=${PAYLOAD_SECRET}
|
|
- STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY}
|
|
depends_on:
|
|
- postgres
|
|
- redis
|
|
|
|
postgres:
|
|
image: postgres:15
|
|
environment:
|
|
- POSTGRES_DB=biblical_guide
|
|
- POSTGRES_USER=${DB_USER}
|
|
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
|
volumes:
|
|
- postgres_data:/var/lib/postgresql/data
|
|
|
|
redis:
|
|
image: redis:7
|
|
ports:
|
|
- "6379:6379"
|
|
|
|
volumes:
|
|
postgres_data:
|
|
```
|
|
|
|
## Risk Analysis & Mitigation
|
|
|
|
### Technical Risks
|
|
|
|
| Risk | Impact | Probability | Mitigation |
|
|
|------|--------|-------------|------------|
|
|
| Data migration failures | High | Medium | Implement rollback mechanisms, test with staging data |
|
|
| Performance degradation | Medium | Low | Load testing, caching strategy, CDN implementation |
|
|
| Authentication issues | High | Low | Parallel auth systems during transition |
|
|
| Payment disruption | High | Low | Keep Stripe webhooks for both systems |
|
|
|
|
### Business Risks
|
|
|
|
| Risk | Impact | Probability | Mitigation |
|
|
|------|--------|-------------|------------|
|
|
| User experience disruption | High | Medium | Phased rollout, feature flags |
|
|
| SEO impact | Medium | Low | Maintain URL structure, redirects |
|
|
| Admin training needed | Low | High | Comprehensive documentation, video tutorials |
|
|
|
|
### Rollback Plan
|
|
|
|
1. **Database Backup**: Daily automated backups with 30-day retention
|
|
2. **Feature Flags**: Toggle between old and new systems
|
|
3. **Blue-Green Deployment**: Maintain both systems in parallel
|
|
4. **Data Sync**: Bidirectional sync during transition period
|
|
|
|
## Timeline & Resources
|
|
|
|
### Development Timeline (12 Weeks)
|
|
|
|
#### Weeks 1-2: Setup & Configuration
|
|
- [ ] Install Payload CMS
|
|
- [ ] Configure database adapter
|
|
- [ ] Set up development environment
|
|
- [ ] Create base collections
|
|
|
|
#### Weeks 3-4: Data Models
|
|
- [ ] Define all collections
|
|
- [ ] Set up relationships
|
|
- [ ] Configure validations
|
|
- [ ] Create hooks
|
|
|
|
#### Weeks 5-6: Authentication
|
|
- [ ] Implement auth system
|
|
- [ ] Migrate user accounts
|
|
- [ ] Set up access control
|
|
- [ ] Test authentication flows
|
|
|
|
#### Weeks 7-8: Payments
|
|
- [ ] Configure Stripe plugin
|
|
- [ ] Set up webhooks
|
|
- [ ] Migrate subscription data
|
|
- [ ] Test payment flows
|
|
|
|
#### Weeks 9-10: API & Frontend
|
|
- [ ] Update API endpoints
|
|
- [ ] Modify frontend API calls
|
|
- [ ] Test all features
|
|
- [ ] Performance optimization
|
|
|
|
#### Week 11: Admin Panel
|
|
- [ ] Customize admin UI
|
|
- [ ] Train admin users
|
|
- [ ] Create documentation
|
|
- [ ] Set up monitoring
|
|
|
|
#### Week 12: Deployment
|
|
- [ ] Production setup
|
|
- [ ] Data migration
|
|
- [ ] Go-live
|
|
- [ ] Post-launch monitoring
|
|
|
|
### Resource Requirements
|
|
|
|
#### Development Team
|
|
- **Lead Developer**: 1 (full-time, 12 weeks)
|
|
- **Backend Developer**: 1 (full-time, 8 weeks)
|
|
- **Frontend Developer**: 1 (part-time, 6 weeks)
|
|
- **QA Engineer**: 1 (part-time, 4 weeks)
|
|
|
|
#### Infrastructure
|
|
- **Staging Environment**: Payload CMS instance
|
|
- **Database**: PostgreSQL (existing)
|
|
- **Redis**: For caching (new)
|
|
- **CDN**: CloudFlare (recommended)
|
|
|
|
#### Budget Estimate
|
|
- **Development**: $25,000 - $35,000
|
|
- **Infrastructure**: $500/month (additional)
|
|
- **Training & Documentation**: $3,000
|
|
- **Contingency**: 20% ($6,000)
|
|
- **Total**: ~$40,000
|
|
|
|
## Success Metrics
|
|
|
|
### Technical Metrics
|
|
- [ ] Zero data loss during migration
|
|
- [ ] API response time < 200ms (p95)
|
|
- [ ] 99.9% uptime maintained
|
|
- [ ] All tests passing (>90% coverage)
|
|
|
|
### Business Metrics
|
|
- [ ] User retention > 95% post-migration
|
|
- [ ] Admin efficiency improved by 40%
|
|
- [ ] Content management time reduced by 50%
|
|
- [ ] Support tickets reduced by 30%
|
|
|
|
### Performance Benchmarks
|
|
- [ ] Page load time < 2 seconds
|
|
- [ ] Database queries < 50ms
|
|
- [ ] Authentication < 100ms
|
|
- [ ] Payment processing < 3 seconds
|
|
|
|
## Conclusion
|
|
|
|
The migration to Payload CMS represents a significant architectural improvement for Biblical Guide. The benefits include:
|
|
|
|
1. **Reduced Maintenance**: Built-in admin panel and auth system
|
|
2. **Improved Security**: Enterprise-grade authentication
|
|
3. **Better Developer Experience**: TypeScript, auto-generated APIs
|
|
4. **Enhanced Features**: Rich text editing, media management
|
|
5. **Future-Proof**: Active development, strong community
|
|
|
|
The phased approach ensures minimal disruption while providing rollback capabilities at each stage. With proper planning and execution, this migration will position Biblical Guide for sustainable growth and improved user experience.
|
|
|
|
## Appendices
|
|
|
|
### A. Useful Commands
|
|
|
|
```bash
|
|
# Start Payload development server
|
|
npm run dev
|
|
|
|
# Generate TypeScript types
|
|
npm run generate:types
|
|
|
|
# Run migrations
|
|
npm run payload migrate
|
|
|
|
# Create admin user
|
|
npm run payload create-admin-user
|
|
|
|
# Export/Import data
|
|
npm run payload export
|
|
npm run payload import data.json
|
|
```
|
|
|
|
### B. Environment Variables Reference
|
|
|
|
```env
|
|
# Required
|
|
DATABASE_URL=
|
|
PAYLOAD_SECRET=
|
|
PAYLOAD_PUBLIC_SERVER_URL=
|
|
|
|
# Stripe
|
|
STRIPE_SECRET_KEY=
|
|
STRIPE_WEBHOOK_SECRET=
|
|
STRIPE_PUBLISHABLE_KEY=
|
|
|
|
# Email (Mailgun)
|
|
MAILGUN_API_KEY=
|
|
MAILGUN_DOMAIN=
|
|
FROM_EMAIL=
|
|
|
|
# Azure OpenAI (for future chat re-enablement)
|
|
AZURE_OPENAI_KEY=
|
|
AZURE_OPENAI_ENDPOINT=
|
|
|
|
# Redis (caching)
|
|
REDIS_URL=
|
|
|
|
# Monitoring
|
|
SENTRY_DSN=
|
|
DATADOG_API_KEY=
|
|
```
|
|
|
|
### C. Migration Checklist
|
|
|
|
- [ ] Backup production database
|
|
- [ ] Set up staging environment
|
|
- [ ] Run migration scripts on staging
|
|
- [ ] Perform full QA testing
|
|
- [ ] Train admin users
|
|
- [ ] Prepare rollback plan
|
|
- [ ] Schedule maintenance window
|
|
- [ ] Execute production migration
|
|
- [ ] Monitor post-deployment
|
|
- [ ] Gather user feedback
|
|
|
|
### D. Support Resources
|
|
|
|
- **Payload CMS Documentation**: https://payloadcms.com/docs
|
|
- **GitHub Repository**: https://github.com/payloadcms/payload
|
|
- **Discord Community**: https://discord.gg/payload
|
|
- **Video Tutorials**: https://www.youtube.com/payloadcms
|
|
- **Stack Overflow**: Tag: payload-cms
|
|
|
|
---
|
|
|
|
*Document Version: 1.0*
|
|
*Last Updated: November 2024*
|
|
*Author: Biblical Guide Development Team* |