| name | payload-migration-workflow |
| description | Guide for migrating from PHP/MySQL to Payload CMS with PostgreSQL. Use when working on migration tasks, understanding data models, or implementing Payload collections. Covers migration strategy, architecture decisions, and step-by-step tasks for the Y-Not Radio site migration. |
Payload CMS Migration Workflow
Project Goal: Migrate from homemade PHP/MySQL to Payload CMS with PostgreSQL on Netlify + Neon, then build a modern responsive site redesign.
Quick Start
When starting a migration task:
- Find your task: See Migration Tasks or
docs/payload-migration/04-migration-tasks.md - Check context: Review Architecture Decisions below
- Verify completion: Follow success criteria for your collection
- Get help: See Quick Reference for commands and queries
Why Payload + PostgreSQL
- Relational continuity: MySQL → PostgreSQL is simpler than MySQL → NoSQL
- Direct database access: PHP can query PostgreSQL directly without learning GraphQL/GROQ
- Code-first configuration: Collections defined in TypeScript with full type safety
- Built-in APIs: REST + GraphQL without learning GROQ
- Self-hosted flexibility: Full control over CMS and deployment
Key Architecture Decisions
Database Strategy
- Single PostgreSQL database for all data (including votes)
- Neon serverless PostgreSQL for production
- Direct SQL access from PHP using PDO
- ACID transactions for data consistency
Collection Patterns
- Base collections: People, DJs, Artists, Venues, Media
- Content collections: Concerts, Shows, Posts, Songs, Records
- Interactive collections: Top11, YearEndPolls, ModernRockMadness (with votes)
Migration Approach
- Two-phase: (1) Migrate data and CMS, (2) Redesign frontend
- Feature flags for gradual cutover
- PHP site continues querying PostgreSQL directly
- Payload Admin UI for content management
Core Data Models (Priority Order)
| Priority | Collection | Status | Dependencies |
|---|---|---|---|
| 1 | People | 🔲 Todo | None |
| 2 | DJs | 🔲 Todo | People |
| 3 | Artists | 🔲 Todo | People (many-to-many) |
| 4 | Venues | 🔲 Todo | None |
| 5 | Media | 🔲 Todo | Cloudinary integration |
| 6 | Ads | 🔲 Todo | Media |
| 7 | Concerts | 🔲 Todo | Artists, Venues |
| 8 | Songs | 🔲 Todo | Artists |
| 9 | Records | 🔲 Todo | Artists |
| 10 | CdOfTheWeek | 🔲 Todo | Records |
| 11 | OnDemand | 🔲 Todo | Artists, DJs |
| 12 | Shows | 🔲 Todo | DJs |
| 13 | Posts | 🔲 Todo | Media (unified Story + CustomText) |
| 14-23 | Interactive (Top11, YearEnd, MRM) | 🔲 Todo | Various |
Full details: docs/payload-migration/03-core-data-models.md
Migration Tasks
Task 0: Setup Environment ✅ Complete
- Payload Admin UI at http://localhost:3000/admin
- PostgreSQL connection (Neon)
- Netlify deployment configured
- Cloudinary for media uploads
Details: docs/payload-migration/04-migration-tasks.md#task-0
Task 1: MySQL to PostgreSQL Schema
Convert MySQL schema to PostgreSQL:
- Data type conversions (DATETIME → TIMESTAMP, TINYINT → BOOLEAN)
- Foreign key constraints
- Indexes for performance
- Soft delete columns (
deleted_at)
Details: docs/payload-migration/04-migration-tasks.md#task-1
Task 2: Data Migration
Export MySQL data and import to PostgreSQL:
- Use
mysqldumpfor data export - Transform data during import
- Validate relationships
- Test with sample queries
Details: docs/payload-migration/04-migration-tasks.md#task-2
Task 3+: Collection Implementation
For each collection:
- Define Payload collection schema in TypeScript
- Configure fields, relationships, access control
- Run migrations:
yarn payload migrate - Seed with sample data
- Test CRUD operations in Admin UI
- Verify PHP can query via PostgreSQL
Per-collection checklists: docs/payload-migration/07-success-criteria.md
PHP PostgreSQL Integration
PHP can query PostgreSQL directly without learning GraphQL:
// Connect to PostgreSQL (same as MySQL but different driver)
$pdo = new PDO(
"pgsql:host=localhost;port=5432;dbname=ynot_payload_dev",
"username",
"password"
);
// Query Payload tables directly
$stmt = $pdo->prepare("
SELECT c.id, c.date, c.venue_id, v.name as venue_name
FROM concerts c
JOIN venues v ON c.venue_id = v.id
WHERE c.date >= NOW()
ORDER BY c.date ASC
LIMIT 10
");
$stmt->execute();
$concerts = $stmt->fetchAll(PDO::FETCH_ASSOC);
Full guide: docs/payload-migration/03.5-php-postgresql-querying.md
Cloudinary Media Storage
For image uploads:
- Configure Cloudinary plugin in
payload.config.ts - Set environment variables (
CLOUDINARY_CLOUD_NAME, etc.) - Use Media collection for all uploads
- Images auto-optimized and delivered via CDN
Detailed setup: docs/payload-migration/12-cloudinary-integration.md
Success Criteria Per Collection
Each collection must meet:
- Payload schema defined in TypeScript
- Migrations run successfully
- Admin UI shows collection with correct fields
- Can create/edit/delete records
- Relationships work correctly
- PHP can query via PostgreSQL
- Sample data seeded for testing
Complete checklists: docs/payload-migration/07-success-criteria.md
Quick Reference
Development Commands
# Start Payload dev server
yarn payload:dev
# Run migrations
yarn payload migrate
# Seed sample data
yarn seed:payload
# Access Admin UI
open http://localhost:3000/admin
PostgreSQL Connection
DATABASE_URI=postgresql://user:pass@localhost:5432/ynot_payload_dev
DATABASE_SSL=disable # For local development
Useful Queries
-- List all collections (tables)
SELECT tablename FROM pg_tables
WHERE schemaname = 'public'
ORDER BY tablename;
-- Check collection structure
\d concerts
-- Sample data query
SELECT * FROM concerts
ORDER BY date DESC
LIMIT 5;
More examples: docs/payload-migration/08-quick-reference.md
Common Patterns
Many-to-Many Relationships
Use Payload's relationship field with hasMany: true:
{
name: 'artists',
type: 'relationship',
relationTo: 'artists',
hasMany: true,
}
Soft Deletes
Payload handles this automatically with timestamps: true in collection config.
Rich Text Content
Use Payload's lexical editor for Posts:
{
name: 'content',
type: 'richText',
}
File Uploads
Configure cloudinary plugin for Media collection:
plugins: [
cloudinaryPlugin({
collections: {
media: true,
},
}),
]
Troubleshooting
Migration Fails
- Check PostgreSQL connection
- Verify all dependencies installed
- Review migration logs:
yarn payload migrate --debug
Admin UI Not Accessible
- Ensure
yarn payload:devis running - Check port 3000 is available
- Verify DATABASE_URI in
.env.local
PHP Can't Query
- Confirm PostgreSQL credentials
- Test connection:
psql -h localhost -U user -d ynot_payload_dev - Check table names match schema
Reference Documentation
Comprehensive details in these chapters:
- Project Overview - Goals, current state, strategy
- Architecture Decisions - Data handling, patterns
- Core Data Models - All collections with status
- PHP PostgreSQL Querying - Direct SQL access
- Migration Tasks - Step-by-step tasks
- Shared Utilities - File structure, patterns
- Frontend Cutover - Feature flags, deployment
- Success Criteria - Per-collection checklists
- Quick Reference - Commands, queries, templates
- Relational Advantages - Why PostgreSQL
- CMS Comparison - Sanity vs Payload
- Capacity Planning - Limits, pricing
- Cloudinary Integration - Media storage setup
Each chapter is self-contained for cold-start agent conversations.
Workflow Tips
- Start small: Begin with base collections (People, Venues)
- Test incrementally: Verify each collection before moving to next
- Seed data: Use sample data to test relationships
- Document issues: Note any migration problems for team review
- Follow priority order: Collections are ordered by dependencies
Remember: This is a two-phase project. Phase 1 (current) focuses on data migration and CMS setup. Phase 2 will handle frontend redesign.