Colin's Shudderfly is a secure, private content management system designed to create a safe digital environment for children. Zero ads, no social media links, no external tracking — just your curated content in a beautiful, modern interface.
🛡️ Child-Safe by Design - Complete control over all content with role-based permissions
🎨 Beautiful & Responsive - Modern UI that works seamlessly on phones, tablets, and desktops
📚 Digital Photo Albums - Organize memories into themed books with categories and geolocation
🎵 Distraction-Free Music - YouTube integration without recommendations or ads
🖼️ PDF Collage Generator - Create printable photo books from your digital collections
🎮 Accessible Games - Fun, self-contained games with no ads, no tracking, and full keyboard/screen-reader support
🔍 Lightning-Fast Search - Powered by Meilisearch with typo-tolerance and instant results
🚀 Production-Ready - Built on Laravel 12 with enterprise-grade security and scalability
- 📖 Books: Digital photo albums with categories, geolocation tags, and read tracking
- 📸 Photos: Standalone image galleries with infinite scroll and bulk management
- 🎵 Music: YouTube videos presented as audio tracks with custom thumbnails
- 🎨 Collages: Generate beautiful PDF photo books for printing
- 🎮 Games: Accessible, ad-free games playable within the authenticated app (Poop Boom, Cockroach Fart, Big Poop)
- 💬 Messages: Internal communication system with reactions and threading
- Framework: Laravel 12 (PHP 8.3+)
- Database: MySQL 8.0 with Eloquent ORM
- Authentication: Laravel Sanctum with role-based permissions via Spatie Laravel Permission
- Search: Meilisearch via Laravel Scout for fast, typo-tolerant search
- Media Processing:
- Images: Intervention Image (automatic WebP conversion with 30% quality compression)
- Videos: FFmpeg integration via pbmedia/laravel-ffmpeg (H.264 encoding with poster generation)
- PDF Generation: DomPDF for collage exports
- Storage: AWS S3 with CloudFront CDN support
- Queue System: Amazon SQS for asynchronous media processing jobs
- Real-time: Laravel Echo with Pusher for live notifications and reactions
- Testing: PHPUnit with Laravel Nightwatch for debugging
- Framework: Vue 3 with Composition API and
<script setup> - Routing: Inertia.js for SPA experience without REST API overhead
- Styling: Tailwind CSS 3 with custom themes (Christmas, Halloween, Fireworks)
- Rich Text: TipTap editor with link support for content management
- File Uploads: FilePond with drag-and-drop, image preview, and MIME validation
- Icons: RemixIcon (4,000+ icons)
- Maps: Leaflet.js with geocoding for location features
- Build Tool: Vite 6 with hot module replacement
- Testing: Vitest with Vue Test Utils and jsdom
- Containerization: Docker via Laravel Sail for local development
- CI/CD: GitHub Actions for automated testing
- Deployment: Laravel Forge with zero-downtime deployments
- Monitoring: Laravel Nightwatch for error tracking
- Role-Based Access Control: Three permission levels (viewer, editor, admin)
- Media Optimization Pipeline:
- Images automatically converted to WebP format with compression
- Videos processed with FFmpeg for web-optimized playback
- Asynchronous job processing with retry logic and exponential backoff
- Automatic thumbnail generation for videos
- Categories & Taxonomy: Hierarchical organization with slug-based routing
- Read Tracking: Analytics for book and song engagement
- Bulk Operations: Mass edit, delete, or organize content
- Responsive Design: Mobile-first approach with Tailwind CSS
- Dark Mode Support: System preference detection
- Progress Indicators: Visual feedback during uploads and processing
- Form Validation: Client and server-side validation with Vuelidate
- Contact System: Email notifications to administrators
- Weekly Stats: Automated engagement reports
- Video Snapshot Tool: Generate page snapshots from video content
- PDF Collage Generator: Create printable photo books with custom layouts
- YouTube Integration: Safe music playback via vue-lite-youtube-embed
- Archive System: Soft delete and restore functionality for collages
- Settings Management: Dynamic site configuration via database
Before you begin, ensure you have the following installed:
-
PHP 8.3+ with extensions:
mbstring,xml,curl,zip,gd,mysql -
Composer (latest version)
-
Node.js 20+ and npm 10+
-
Docker Desktop (for Laravel Sail)
-
FFmpeg (for video processing)
# macOS brew install ffmpeg # Ubuntu/Debian sudo apt-get install ffmpeg
git clone https://github.com/isAdamBailey/shudderfly.git
cd shudderfly# Install PHP dependencies
composer install
# Install JavaScript dependencies
npm install# Copy the example environment file
cp .env.example .env
# Generate application key
php artisan key:generateEdit .env and configure the following sections:
Database (handled by Docker):
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=shudderfly
DB_USERNAME=sail
DB_PASSWORD=passwordQueue System (use sync for local development):
QUEUE_CONNECTION=sync
# For production with SQS:
# QUEUE_CONNECTION=sqs
# AWS_ACCESS_KEY_ID=your-access-key
# AWS_SECRET_ACCESS_KEY=your-secret-key
# SQS_PREFIX=https://sqs.us-east-1.amazonaws.com/your-account-id
# SQS_QUEUE=your-queue-nameFile Storage (use local for development):
FILESYSTEM_DISK=local
# For production with S3:
# FILESYSTEM_DISK=s3
# AWS_BUCKET=your-bucket-name
# CLOUDFRONT_URL=https://your-cloudfront-urlMeilisearch (handled by Docker):
SCOUT_DRIVER=meilisearch
MEILISEARCH_HOST=http://meilisearch:7700
MEILISEARCH_KEY=
FORWARD_MEILISEARCH_PORT=7700Mail (optional for local development):
MAIL_MAILER=log
# For production with AWS SES:
# MAIL_MAILER=ses
# AWS_ACCESS_KEY_ID=your-access-key
# AWS_SECRET_ACCESS_KEY=your-secret-keyPusher (optional for real-time features):
# Leave blank to disable real-time features locally
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_HOST=
PUSHER_APP_CLUSTER=mt1Web Push Notifications (optional):
# Generate VAPID keys
npx web-push generate-vapid-keys
# Add the generated keys to .env
VAPID_PUBLIC_KEY=your-public-key
VAPID_PRIVATE_KEY=your-private-key# Start all services (MySQL, Meilisearch, PHP)
./vendor/bin/sail up -d
# Create an alias for convenience (optional but recommended)
alias sail='./vendor/bin/sail'# Run migrations
sail artisan migrate
# Seed database with default roles, permissions, and sample data
sail artisan db:seed
# Or run both commands together
sail artisan migrate:fresh --seedThis creates:
- 3 Permission Roles: Viewer, Editor, Admin
- Default User: Check the seeder output for credentials
- Sample Books, Pages, and Songs: Test data to explore features
sail artisan scout:import "App\Models\Book"
sail artisan scout:import "App\Models\Page"
sail artisan scout:import "App\Models\Song"# Development mode with hot reload
npm run dev
# Or in a separate terminal if using Sail
sail npm run dev
# Production build
npm run build- Application: http://localhost
- Meilisearch Dashboard: http://localhost:7700
- MySQL Database: localhost:3306 (username:
sail, password:password)
For processing media uploads (images/videos) and generating PDFs:
# Development (synchronous - processes immediately)
# Already configured with QUEUE_CONNECTION=sync
# Production (asynchronous with queue worker)
sail artisan queue:work --tries=3 --timeout=1800Note: Video processing can take 15-30 minutes depending on file size. The StoreVideo job has a 30-minute timeout.
# Start all services
sail up -d
# Watch for frontend changes (hot reload)
npm run dev
# Run tests
sail artisan test
npm run test
# Run linters
npm run lint
npm run format
# Stop all services
sail downCore Models:
books- Photo album containers with categories and geolocationpages- Individual photos/videos belonging to bookssongs- YouTube music tracks with thumbnailscategories- Hierarchical organization for bookscollages- Generated PDF collectionsmessages- Internal messaging systemusers- Authentication with role-based permissions
StoreImage: Optimizes images to WebP format (30% quality), uploads to S3, cleans up old filesStoreVideo: Processes videos with FFmpeg (H.264 encoding), generates posters, uploads to S3 (30-minute timeout)CreateVideoSnapshot: Captures video frames at specific timestamps for page creationGenerateCollagePdf: Creates printable PDF collages from selected images, emails download linkIncrementBookReadCount/IncrementPageReadCount/IncrementSongReadCount: Tracks engagement analytics
- Viewer: Browse books, pages, music; basic read access
- Editor: Create, edit, and delete content; manage books and pages
- Admin: Full system access including user management, settings, and permissions
- Public:
/login,/register(registration requires secret token) - Authenticated (
authmiddleware): All content routes - Editor (
can:edit pages): Content management routes - Admin (
can:admin): User management, settings, system configuration
- Development: Local filesystem (
storage/app/public) - Production: AWS S3 with CloudFront CDN
- Media Processing: Temporary files in system temp directory, cleaned up after upload
- Automatic Cleanup: Old media deleted when pages are updated
This application uses Meilisearch via Laravel Scout for fast, typo-tolerant search with autocomplete functionality in the search bar.
Meilisearch is included in the Docker Compose setup. When using Laravel Sail:
-
Start the services (Meilisearch will start automatically):
sail up -d
-
Configure environment variables in
.env:SCOUT_DRIVER=meilisearch MEILISEARCH_HOST=http://meilisearch:7700 MEILISEARCH_KEY= FORWARD_MEILISEARCH_PORT=7700
Note: For local development,
MEILISEARCH_KEYcan be left empty (Meilisearch runs without authentication in development mode). -
Index existing data: Already covered in step 7 of the installation guide above.
-
Install Meilisearch on your server:
sudo docker run -d \ --name meilisearch \ --restart unless-stopped \ -p 7700:7700 \ -v /opt/meilisearch/data:/meili_data \ -e MEILI_MASTER_KEY="your-master-key-here" \ -e MEILI_ENV="production" \ getmeili/meilisearch:v1.5
Generate a secure master key:
openssl rand -base64 32
-
Configure environment variables in Forge:
SCOUT_DRIVER=meilisearch MEILISEARCH_HOST=http://localhost:7700 MEILISEARCH_KEY=your-generated-master-key-here
-
Index data manually (first time only):
php artisan scout:import "App\Models\Book" php artisan scout:import "App\Models\Page" php artisan scout:import "App\Models\Song"
The following models are automatically indexed when created or updated:
- Book: Indexes
titleandexcerpt - Page: Indexes
contentand related booktitle - Song: Indexes
titleanddescription
- Connection refused: Ensure Meilisearch is running (
sudo docker ps | grep meilisearch) - Index not found: Run
php artisan scout:importfor the relevant model - Permission denied: Add your user to the docker group:
sudo usermod -aG docker forge - Tests failing: Tests use
SCOUT_DRIVER=null(configured inphpunit.xml) to avoid requiring Meilisearch in CI
- Create an S3 bucket for media storage
- Enable public access for uploaded media
- Configure CORS policy:
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "PUT", "POST", "DELETE"],
"AllowedOrigins": ["*"],
"ExposeHeaders": []
}
]The bucket uses a flat top-level folder structure:
| Folder | Contents |
|---|---|
books/{book-slug}/ |
Book page images and videos |
collages/ |
Collage images and PDFs |
sounds/ |
Sound effect audio files (MP3 / AAC) |
Turn on sounds_enabled in Settings to show the Sounds page (audio from S3 / CloudFront). Edit pages can upload clips (FFmpeg converts uploads to M4A in a queue job — run a queue worker in production). Uploaded files are stored as sounds/{uuid}.m4a, not a fixed filename. Everyone can use the tile menu to hear a title read aloud; edit/delete stays editor-only. Games use the bundled public/fart.m4a for effects unless you point fartSoundUrl at a specific URL yourself.
- Create a CloudFront distribution pointing to your S3 bucket
- Add
CLOUDFRONT_URLto your.env - Reduces latency and improves media loading speed
- The app automatically uses CloudFront in production and direct S3 in local dev
- Create an SQS queue for background jobs
- Set visibility timeout to at least 1900 seconds (for video processing)
- Configure dead-letter queue for failed jobs
- Add credentials to
.env:
QUEUE_CONNECTION=sqs
SQS_PREFIX=https://sqs.us-east-1.amazonaws.com/your-account-id
SQS_QUEUE=shudderfly-production- Verify your domain in AWS SES
- Move out of sandbox mode for production sending
- Configure in
.env:
MAIL_MAILER=ses
MAIL_FROM_ADDRESS=noreply@yourdomain.com- Ubuntu 22.04 LTS
- PHP 8.3 with required extensions
- MySQL 8.0
- FFmpeg installed
- Sufficient disk space for temporary video processing
Add to your Forge deployment script:
cd /home/forge/yourdomain.com
# Maintenance mode
php artisan down
# Pull latest code
git pull origin main
# Install dependencies
composer install --no-dev --optimize-autoloader
# Clear caches
php artisan cache:clear
php artisan config:clear
php artisan route:clear
php artisan view:clear
# Optimize
php artisan config:cache
php artisan route:cache
php artisan view:cache
# Build frontend assets
npm ci
npm run build
# Run migrations
php artisan migrate --force
# Exit maintenance mode
php artisan up
# Restart queue workers
php artisan queue:restartIn Forge, set up daemon for queue processing:
php artisan queue:work sqs --tries=3 --timeout=1800 --sleep=3 --max-time=3600Important: Set supervisor stopwaitsecs to at least 1900 seconds to allow video processing to complete.
Add to Forge scheduler (runs every minute):
php artisan schedule:runThis handles:
- Weekly engagement statistics emails
- Cleanup of old failed jobs
- Cache warming
Critical variables to set in Forge:
APP_ENV=production
APP_DEBUG=false
APP_URL=https://yourdomain.com
# Database
DB_HOST=localhost
DB_DATABASE=your_database
DB_USERNAME=your_user
DB_PASSWORD=secure_password
# AWS Services
AWS_ACCESS_KEY_ID=your_access_key
AWS_SECRET_ACCESS_KEY=your_secret_key
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=your-bucket-name
CLOUDFRONT_URL=https://d1234567890.cloudfront.net
# Queue
QUEUE_CONNECTION=sqs
SQS_PREFIX=https://sqs.us-east-1.amazonaws.com/your-account-id
SQS_QUEUE=shudderfly-production
# Mail
MAIL_MAILER=ses
MAIL_FROM_ADDRESS=noreply@yourdomain.com
# Search
SCOUT_DRIVER=meilisearch
MEILISEARCH_HOST=http://localhost:7700
MEILISEARCH_KEY=your_production_master_key
# Pusher (for real-time features)
PUSHER_APP_ID=your_app_id
PUSHER_APP_KEY=your_app_key
PUSHER_APP_SECRET=your_app_secret
PUSHER_HOST=your_pusher_host
PUSHER_APP_CLUSTER=your_cluster
# Web Push
VAPID_PUBLIC_KEY=your_public_key
VAPID_PRIVATE_KEY=your_private_key
# Registration Protection
REGISTRATION_SECRET=your_secret_token# Run all PHPUnit tests
sail artisan test
# Run specific test file
sail artisan test tests/Feature/BookTest.php
# Run with coverage
sail artisan test --coverage# Run all Vitest tests
npm run test
# Watch mode for development
npm run test:watch
# Run with UI
npm run test:ui
# Run once (for CI)
npm run test:run# Run ESLint
npm run lint
# Format code with Prettier
npm run format
# PHP CS Fixer (if configured)
./vendor/bin/pintProblem: Videos fail to process or timeout Solutions:
- Check FFmpeg is installed:
which ffmpeg - Increase PHP memory limit in
php.ini:memory_limit = 512M - Increase queue timeout:
QUEUE_CONNECTION=syncfor local testing - Check video codec: FFmpeg requires H.264 compatible videos
- Review logs:
tail -f storage/logs/laravel.log
Problem: Images upload but don't show in browser Solutions:
- Check S3 bucket permissions (must be publicly readable)
- Verify
CLOUDFRONT_URLin.envmatches your distribution - Check browser console for CORS errors
- Verify S3 CORS policy is configured correctly
- Test direct S3 URL access
Problem: Search returns no results Solutions:
- Verify Meilisearch is running:
docker ps | grep meilisearch - Re-index models:
sail artisan scout:import "App\Models\Book" - Check Meilisearch logs:
docker logs meilisearch - Test Meilisearch directly:
curl http://localhost:7700/health
Problem: Jobs remain in queue and don't process Solutions:
- Restart queue worker:
sail artisan queue:restart - Check failed jobs:
sail artisan queue:failed - Retry failed jobs:
sail artisan queue:retry all - For video processing, ensure timeout is sufficient (1800 seconds)
Problem: Cannot create/edit content Solutions:
- Check user roles:
sail artisan tinker→User::with('roles')->get() - Verify permissions seeded:
sail artisan db:seed --class=RolesAndPermissionsSeeder - Assign role to user in UI: Settings → Users → Edit User
# Laravel application logs
tail -f storage/logs/laravel.log
# Queue worker logs (production)
tail -f storage/logs/worker.log
# Docker container logs
docker logs -f laravel.test
# Meilisearch logs
docker logs -f meilisearchThis project is open-sourced software licensed under the MIT license.
Adam Bailey
- Website: adambailey.io
- GitHub: @isAdamBailey
Built with:
- Laravel - The PHP Framework for Web Artisans
- Vue.js - The Progressive JavaScript Framework
- Inertia.js - Build single-page apps without building an API
- Tailwind CSS - A utility-first CSS framework
- Meilisearch - Lightning-fast search engine
⭐ If you find this project useful, please consider giving it a star on GitHub!