The Multi-Hub Concept
MinistryHub is built on the philosophy of “The Right Tool for the Right Task” . Instead of forcing all users into a single monolithic interface, the platform is divided into specialized hubs that serve specific ministry needs.
Ministry Hub Worship teams access song libraries, setlists, and musician assignments without cluttering their interface with administrative tools.
Social Media Hub Communication teams focus on media management and outreach without navigating unrelated features.
Church Center Administrators manage people, permissions, and reports with tools designed for organizational oversight.
Contextual Branding
The platform automatically adapts its branding based on:
The hub you’re currently using
Your role and permissions
The route you’re accessing
This creates a focused, purposeful experience for each user type.
Users only see the hubs they have permission to access. A worship team member won’t see administrative hubs unless they also have those roles.
Technology Stack
Frontend Architecture
MinistryHub’s frontend is built with modern, high-performance technologies:
{
"framework" : "React 19" ,
"bundler" : "Vite 7" ,
"language" : "TypeScript 5.9" ,
"routing" : "React Router 7" ,
"styling" : "Vanilla CSS with CSS Variables" ,
"internationalization" : "i18next" ,
"http-client" : "Axios" ,
"charts" : "Recharts" ,
"music-processing" : "ChordSheetJS 13.2"
}
Why Vanilla CSS? MinistryHub uses a custom design system with CSS variables for high performance, native dark mode support, and premium micro-animations without the overhead of CSS-in-JS libraries.
Backend Architecture
The backend is a professional, custom-built PHP micro-framework following PSR-4 standards:
<? php
// bootstrap.php - PSR-4 Autoloader
spl_autoload_register ( function ( $class ) {
$prefix = 'App \\ ' ;
$base_dir = __DIR__ . '/' ;
$len = strlen ( $prefix );
if ( strncmp ( $prefix , $class , $len ) !== 0 ) {
return ;
}
$relative_class = substr ( $class , $len );
$file = $base_dir . str_replace ( ' \\ ' , '/' , $relative_class ) . '.php' ;
if ( file_exists ( $file )) {
require $file ;
}
});
Key Backend Features:
JWT Authentication : Stateless token-based authentication
Controller-Repository Pattern : Clean separation of routing and data access
Middleware System : Centralized security filters (CORS, Auth, RBAC)
PDO with Prepared Statements : Complete SQL injection protection
Global Exception Handler : Centralized error logging and response
Database Design
MinistryHub uses MySQL with multiple specialized schemas:
User Schema Members, roles, permissions, authentication, and invitation tokens.
Music Schema Songs, chord sheets, setlists, instruments, and assignments.
Activity Schema Audit logs tracking all system actions for compliance.
Calendar Schema Meetings, services, notifications, and schedules.
Folder Structure
MinistryHub follows a security-first folder architecture that separates public assets from private business logic:
/ (Server Root)
├── backend/ # PRIVATE CORE (Inaccessible via URL)
│ ├── config/
│ │ └── database.env # DB credentials, API keys
│ ├── src/
│ │ ├── Controllers/ # API route handlers
│ │ │ ├── AuthController.php
│ │ │ ├── SongController.php
│ │ │ ├── ChurchController.php
│ │ │ └── ...
│ │ ├── Repositories/ # Data access layer
│ │ │ ├── SongRepo.php
│ │ │ ├── UserRepo.php
│ │ │ └── ...
│ │ ├── Helpers/ # Utilities
│ │ │ ├── Response.php
│ │ │ ├── Logger.php
│ │ │ └── Cors.php
│ │ ├── Middleware/ # Security filters
│ │ ├── Database.php # PDO connection manager
│ │ └── Jwt.php # Token encoding/decoding
│ ├── logs/ # Application logs
│ └── bootstrap.php # Autoloader & initialization
│
└── public_html/ # PUBLIC WEB ROOT
├── assets/ # Compiled React build
│ ├── index.[hash].js
│ └── index.[hash].css
├── api/
│ └── index.php # Single API entry point
├── .htaccess # URL rewriting rules
└── index.html # React SPA shell
Security Note : The backend/ folder must be placed above the web root (public_html/) to ensure it’s never directly accessible via URL. This protects credentials, source code, and business logic.
Request Lifecycle
Here’s how a typical request flows through MinistryHub:
Client Request
A user navigates to church.com/worship/songs or makes an API call to church.com/api/songs.
.htaccess Routing
The .htaccess file intercepts the request:
If it matches /api/*, route to api/index.php
Otherwise, serve index.html (React Router handles the route)
RewriteRule ^api/(.*)$ api/index.php?path=$1 [L,QSA]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.html [L]
API Entry Point
api/index.php loads the backend bootstrap and parses the route:<? php
require_once __DIR__ . '/../../backend/src/bootstrap.php' ;
$method = $_SERVER [ 'REQUEST_METHOD' ];
$pathParam = $_GET [ 'path' ] ?? '' ;
$parts = explode ( '/' , trim ( $uri , '/' ));
$resource = $parts [ 0 ] ?? '' ; // e.g., "songs"
$action = $parts [ 1 ] ?? '' ; // e.g., "123" or "new"
Authentication Middleware
For protected routes, the AuthMiddleware validates the JWT token: // Extract token from Authorization header
$token = str_replace ( 'Bearer ' , '' , $_SERVER [ 'HTTP_AUTHORIZATION' ] ?? '' );
$payload = \App\ Jwt :: decode ( $token );
$memberId = $payload [ 'uid' ];
Public routes like /api/auth/login bypass this middleware.
Controller Routing
The request is routed to the appropriate controller: switch ( $resource ) {
case 'songs' :
( new \App\Controllers\ SongController ()) -> handle ( $memberId , $action , $method );
break ;
case 'churches' :
( new \App\Controllers\ ChurchController ()) -> handle ( $memberId , $action , $method );
break ;
// ...
}
Business Logic (Controller)
The controller processes the request, validates permissions, and calls the repository: class SongController {
public function handle ( $memberId , $action , $method ) {
if ( $method === 'GET' && empty ( $action )) {
// Get all songs
$churchId = $this -> getCurrentChurchId ( $memberId );
$songs = SongRepo :: getAll ( $churchId );
Response :: json ([ 'songs' => $songs ]);
}
}
}
Data Access (Repository)
The repository executes database queries using PDO: class SongRepo {
public static function getAll ( $churchId = null ) {
$db = Database :: getInstance ( 'music' );
$sql = " SELECT * FROM songs WHERE is_active = 1 " ;
$params = [];
if ( $churchId !== null ) {
$sql .= " AND (church_id = ? OR church_id = 0 )" ;
$params [] = $churchId ;
}
$stmt = $db -> prepare ( $sql );
$stmt -> execute ( $params );
return $stmt -> fetchAll ( PDO :: FETCH_ASSOC );
}
}
All queries use prepared statements for SQL injection prevention.
JSON Response
The controller returns a JSON response: class Response {
public static function json ( $data , $status = 200 ) {
http_response_code ( $status );
header ( 'Content-Type: application/json' );
echo json_encode ( $data );
exit ;
}
}
Frontend Module Structure
The React application is organized by feature modules:
frontend/src/
├── modules/
│ ├── worship/ # Ministry Hub
│ │ ├── pages/
│ │ │ ├── WorshipDashboard.tsx
│ │ │ ├── SongList.tsx
│ │ │ ├── SongEditor.tsx
│ │ │ └── Playlists.tsx
│ │ └── components/
│ ├── mainhub/ # Church Center
│ │ ├── pages/
│ │ │ ├── PeopleList.tsx
│ │ │ ├── TeamsList.tsx
│ │ │ └── Reports.tsx
│ │ └── components/
│ └── social/ # Social Media Hub
│ └── pages/
├── components/
│ ├── layout/ # Shared layouts
│ │ ├── MainLayout.tsx
│ │ ├── ProtectedRoute.tsx
│ │ └── ModuleGuard.tsx
│ ├── ui/ # Reusable components
│ └── music/ # ChordPro rendering
├── context/ # React Context providers
│ ├── AuthContext.tsx
│ ├── ThemeContext.tsx
│ └── ToastContext.tsx
├── i18n/ # Internationalization
│ └── locales/
│ ├── es.json
│ ├── en.json
│ └── pt.json
└── App.tsx # Main routing
Route Protection
MinistryHub uses nested route protection:
< Route element = { < ProtectedRoute /> } >
< Route element = { < MainLayout /> } >
{ /* Worship Hub - Protected by ModuleGuard */ }
< Route path = "worship" element = { < ModuleGuard moduleKey = "worship" /> } >
< Route index element = { < WorshipDashboard /> } />
< Route path = "songs" element = { < SongList /> } />
< Route path = "songs/:id" element = { < SongDetail /> } />
</ Route >
{ /* Church Center - Protected by ModuleGuard */ }
< Route path = "mainhub" element = { < ModuleGuard moduleKey = "mainhub" /> } >
< Route index element = { < MainHubDashboard /> } />
< Route path = "people" element = { < PeopleList /> } />
</ Route >
</ Route >
</ Route >
ProtectedRoute : Ensures user is authenticated (has valid JWT)
ModuleGuard : Verifies user has permission to access the specific hub
MainLayout : Provides consistent navigation and theme
Authentication Flow
MinistryHub implements stateless JWT authentication:
User Login
User submits credentials to /api/auth/login: const response = await axios . post ( '/api/auth/login' , {
email: '[email protected] ' ,
password: 'securePassword' ,
recaptchaToken: '...' // reCAPTCHA v3 token
});
Server Validation
Backend verifies reCAPTCHA, checks credentials, and generates JWT: // Verify password
if ( ! password_verify ( $password , $user [ 'password_hash' ])) {
return Response :: error ( "Credenciales inválidas" , 401 );
}
// Generate JWT
$payload = [
'uid' => $user [ 'member_id' ],
'email' => $user [ 'email' ],
'iat' => time (),
'exp' => time () + 3600 // 1 hour expiration
];
$token = Jwt :: encode ( $payload );
Token Storage
Frontend stores the JWT in localStorage: localStorage . setItem ( 'auth_token' , response . data . access_token );
Authenticated Requests
All subsequent API calls include the JWT: axios . defaults . headers . common [ 'Authorization' ] =
`Bearer ${ localStorage . getItem ( 'auth_token' ) } ` ;
Security Note : JWTs expire after 1 hour. The frontend should handle token refresh or redirect to login when tokens expire.
Build & Deployment
Development Environment
# Frontend
cd frontend
npm install
npm run dev # Vite dev server on http://localhost:5173
# Backend
# Configure backend/config/database.env with MySQL credentials
# Run PHP development server or use Docker
Production Build
Build Frontend
cd frontend
npm run build
This creates optimized assets in frontend/dist/:
Code splitting for faster initial load
Tree-shaking to remove unused code
Asset minification and hashing
Deploy to Server
Upload files to your hosting: # Move built frontend to public web root
frontend/dist/* → public_html/
# Move backend above web root
backend/ → /backend/ (sibling of public_html )
Configure Database
Import SQL schemas and configure backend/config/database.env: DB_HOST =localhost
DB_NAME =ministryhub_main
DB_USER =your_user
DB_PASS =your_password
MUSIC_DB_NAME =ministryhub_music
Set Permissions
Ensure the web server can write to logs:
Frontend
Code Splitting : Dynamic imports reduce initial bundle size
Manual Chunks : Heavy libraries (React, ChordSheetJS) are bundled separately
Tree Shaking : Unused exports are eliminated during build
Asset Caching : Hashed filenames enable long-term browser caching
Backend
Database Indexing : Optimized queries on frequently accessed columns
Connection Pooling : Reuse of database connections via PDO
Query Optimization : Prepared statements with parameter binding
Error Logging : File-based logging instead of database writes
Multi-Tenancy Implementation
MinistryHub supports multiple churches with data isolation:
// Repository pattern with church context
public static function getAll ( $churchId = null ) {
$sql = " SELECT * FROM songs WHERE is_active = 1 " ;
if ( $churchId !== null ) {
// Include church-specific AND universal songs (id 0)
$sql .= " AND (church_id = ? OR church_id = 0 )" ;
$params [] = $churchId ;
}
$stmt = $db -> prepare ( $sql );
$stmt -> execute ( $params );
return $stmt -> fetchAll ( PDO :: FETCH_ASSOC );
}
Universal Resources : Songs, areas, and other resources with church_id = 0 are available to all churches in the system.
Internationalization System
All UI text is managed through JSON translation files:
// i18n configuration
import i18n from 'i18next' ;
import { initReactI18next } from 'react-i18next' ;
i18n
. use ( initReactI18next )
. init ({
resources: {
es: { translation: require ( './locales/es.json' ) },
en: { translation: require ( './locales/en.json' ) },
pt: { translation: require ( './locales/pt.json' ) }
},
fallbackLng: 'es' ,
interpolation: { escapeValue: false }
});
Usage in components:
import { useTranslation } from 'react-i18next' ;
function Component () {
const { t } = useTranslation ();
return < h1 > { t ( 'worship.songs.title' ) } </ h1 > ;
}
Translation updates don’t require code changes - just update the JSON files and redeploy.