Overview
MinistryHub uses a hybrid architecture where:
Frontend : React SPA handles routing client-side
Backend : PHP API processes data requests
Web Server : Apache .htaccess routes requests intelligently
This page traces a request from the user’s browser through the entire system.
Folder Structure
The system separates public and private code for maximum security:
/ (Server Root)
├── backend/ # PRIVATE CORE (unreachable via URL)
│ ├── config/
│ │ └── database.env # DB credentials, JWT secret
│ ├── src/
│ │ ├── Controllers/ # API route handlers
│ │ ├── Repositories/ # Database queries
│ │ ├── Middleware/ # Security filters
│ │ ├── Helpers/ # Response, Logger, CORS
│ │ ├── Database.php # PDO connection manager
│ │ ├── Jwt.php # Token encoding/decoding
│ │ └── bootstrap.php # Autoloader + initialization
│ └── logs/ # Application logs (writable)
│
└── public_html/ # PUBLIC ACCESS (document root)
├── assets/ # React build output (JS/CSS)
├── api/
│ └── index.php # Single API entry point
├── .htaccess # Request routing logic
└── index.html # React SPA entry point
Security Benefits
The backend/ folder is a sibling of public_html/, not inside it. This means:
Sensitive configuration files cannot be accessed via URL
Source code is hidden from web requests
Only public_html/ is exposed to the internet
Request Types
MinistryHub handles two types of requests:
Page Navigation → Serve React SPA (index.html)
API Calls → Route to PHP backend (api/index.php)
Lifecycle 1: Page Navigation
When a user visits https://church.com/worship/songs:
Step 1: Browser Request
GET /worship/songs HTTP / 1.1
Host : church.com
Step 2: .htaccess Routing
Apache processes .htaccess rules:
RewriteEngine On
# API Routing (checked first)
RewriteRule ^api/?$ api/index.php [QSA,L]
RewriteRule ^api/(.*)$ api/index.php [QSA,L]
# SPA Routing (fallback for all non-file requests)
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
Logic:
If path starts with /api/, route to api/index.php
If path is a real file (e.g., /assets/main.js), serve it directly
Otherwise, serve index.html (React SPA)
Step 3: React Router Takes Over
The browser receives index.html which loads the React app:
<! DOCTYPE html >
< html >
< head >
< script type = "module" src = "/assets/main.js" ></ script >
</ head >
< body >
< div id = "root" ></ div >
</ body >
</ html >
React Router matches /worship/songs to the appropriate component:
// Client-side routing
< Route path = "/worship/songs" element = {<SongListPage />} />
Result: User sees the Songs page without a full page reload.
Lifecycle 2: API Request
When the Songs page needs data, it calls the API:
Step 1: Frontend API Call
import axios from 'axios' ;
const fetchSongs = async () => {
const response = await axios . get ( '/api/songs' , {
params: { church_id: 5 },
headers: {
Authorization: `Bearer ${ localStorage . getItem ( 'access_token' ) } `
}
});
return response . data . songs ;
};
Step 2: .htaccess Routes to API
GET /api/songs?church_id=5 HTTP / 1.1
Host : church.com
Authorization : Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
The .htaccess rule matches /api/songs and routes to api/index.php:
RewriteRule ^api/(.*)$ api/index.php [QSA,L]
Step 3: Bootstrap Initialization
The API entry point loads the backend:
public_html/api/index.php
<? php
// Load backend core (outside public_html)
require_once __DIR__ . '/../../backend/src/bootstrap.php' ;
use App\Controllers\ SongController ;
use App\Middleware\ AuthMiddleware ;
use App\Helpers\ Response ;
// Extract route from URL
$uri = parse_url ( $_SERVER [ 'REQUEST_URI' ], PHP_URL_PATH );
$uri = str_replace ( '/api/' , '' , $uri ); // "songs"
$parts = explode ( '/' , trim ( $uri , '/' ));
$resource = $parts [ 0 ]; // "songs"
$action = $parts [ 1 ] ?? '' ; // (empty for list)
$method = $_SERVER [ 'REQUEST_METHOD' ]; // "GET"
Bootstrap runs:
backend/src/bootstrap.php
<? php
// Define project root
define ( 'APP_ROOT' , dirname ( __DIR__ ));
// PSR-4 Autoloader
spl_autoload_register ( function ( $class ) {
$prefix = 'App \\ ' ;
$base_dir = __DIR__ . '/' ;
$relative_class = substr ( $class , strlen ( $prefix ));
$file = $base_dir . str_replace ( ' \\ ' , '/' , $relative_class ) . '.php' ;
if ( file_exists ( $file )) {
require $file ;
}
});
// Initialize CORS headers
\App\Helpers\ Cors :: init ();
Step 4: Authentication Middleware
Before routing to the controller, the request must be authenticated:
public_html/api/index.php
// Public routes (no auth required)
if ( $resource === 'auth' ) {
$controller = new \App\Controllers\ AuthController ();
if ( $action === 'login' && $method === 'POST' ) {
$controller -> login ();
}
exit ;
}
// Protected routes (auth required)
try {
$memberId = \App\Middleware\ AuthMiddleware :: handle ();
} catch ( \ Exception $e ) {
\App\Helpers\ Response :: error ( $e -> getMessage (), 401 );
exit ;
}
AuthMiddleware extracts and validates JWT:
backend/src/Middleware/AuthMiddleware.php
class AuthMiddleware
{
public static function handle ()
{
// Get Authorization header
$authHeader = $_SERVER [ 'HTTP_AUTHORIZATION' ] ?? '' ;
// Extract Bearer token
if ( ! preg_match ( '/Bearer \s ( \S + )/' , $authHeader , $matches )) {
throw new \Exception ( "Token missing" , 401 );
}
$token = $matches [ 1 ];
$decoded = Jwt :: decode ( $token ); // Verify signature + expiration
if ( ! $decoded ) {
throw new \Exception ( "Unauthorized" , 401 );
}
return $decoded [ 'uid' ]; // Return authenticated member ID
}
}
Step 5: Route to Controller
Now that we have an authenticated user ($memberId), route to the appropriate controller:
public_html/api/index.php
switch ( $resource ) {
case 'songs' :
( new \App\Controllers\ SongController ()) -> handle ( $memberId , $action , $method );
break ;
case 'people' :
( new \App\Controllers\ PeopleController ()) -> handle ( $memberId , $action , $method );
break ;
// ... other resources
default :
\App\Helpers\ Response :: error ( "Resource not found: " . $resource , 404 );
}
Step 6: Controller Processes Request
The SongController handles the GET request:
backend/src/Controllers/SongController.php
class SongController
{
public function handle ( $memberId , $action , $method )
{
$churchId = $_GET [ 'church_id' ] ?? null ;
if ( $method === 'GET' ) {
// Check permissions before proceeding
PermissionMiddleware :: require ( $memberId , 'song.read' , $churchId );
$this -> list ( $memberId , $churchId );
}
}
private function list ( $memberId , $churchId )
{
// Delegate to Repository for data access
$songs = \App\Repositories\ SongRepo :: getAll ( $churchId );
Response :: json ([ 'success' => true , 'songs' => $songs ]);
}
}
Step 7: Repository Queries Database
The Repository executes the database query:
backend/src/Repositories/SongRepo.php
class SongRepo
{
public static function getAll ( $churchId = null )
{
$db = \App\ Database :: getInstance ( 'music' );
$sql = " SELECT * FROM songs WHERE church_id = :church_id OR church_id = 0 " ;
$stmt = $db -> prepare ( $sql );
$stmt -> bindValue ( ':church_id' , $churchId , PDO :: PARAM_INT );
$stmt -> execute ();
return $stmt -> fetchAll (); // Returns array of songs
}
}
Database connection (from step 7):
class Database
{
public static function getInstance ( $configKey = 'main' )
{
// Load credentials from config/database.env
$env = parse_ini_file ( APP_ROOT . '/config/database.env' );
$host = $env [ 'DB_HOST' ];
$user = $env [ 'DB_USER' ];
$pass = $env [ 'DB_PASS' ];
$name = $env [ 'DB_NAME' ];
// Create PDO connection with security settings
$dsn = "mysql:host= $host ;dbname= $name ;charset=utf8mb4" ;
$conn = new PDO ( $dsn , $user , $pass );
$conn -> setAttribute ( PDO :: ATTR_ERRMODE , PDO :: ERRMODE_EXCEPTION );
return $conn ;
}
}
Step 8: Response Sent to Client
The Response helper formats the JSON output:
backend/src/Helpers/Response.php
class Response
{
public static function json ( $data , $status = 200 )
{
http_response_code ( $status );
header ( 'Content-Type: application/json' );
echo json_encode ( $data );
exit ;
}
}
HTTP Response:
HTTP/ 1.1 200 OK
Content-Type: application/json
{
"success" : true ,
"songs" : [
{
"id" : 1 ,
"title" : "Amazing Grace" ,
"artist" : "John Newton" ,
"key" : "G"
}
]
}
Step 9: Frontend Receives Data
Axios receives the response and React updates the UI:
const [ songs , setSongs ] = useState ([]);
useEffect (() => {
fetchSongs (). then ( data => setSongs ( data ));
}, []);
return (
< div >
{ songs . map ( song => (
< SongCard key = {song. id } song = { song } />
))}
</ div >
);
Complete Flow Diagram
Authorization Header Handling
Some PHP environments don’t expose the Authorization header by default. MinistryHub handles this.
.htaccess ensures header is visible:
# Ensure Authorization header is visible to PHP
RewriteCond %{HTTP:Authorization} ^(.*)
RewriteRule .* - [e=HTTP_AUTHORIZATION:% 1 ]
Middleware checks multiple sources:
backend/src/Middleware/AuthMiddleware.php
$authHeader = '' ;
// Try multiple methods to retrieve the header
if ( ! empty ( $_SERVER [ 'HTTP_AUTHORIZATION' ])) {
$authHeader = $_SERVER [ 'HTTP_AUTHORIZATION' ];
}
elseif ( ! empty ( $_SERVER [ 'REDIRECT_HTTP_AUTHORIZATION' ])) {
$authHeader = $_SERVER [ 'REDIRECT_HTTP_AUTHORIZATION' ];
}
elseif ( ! empty ( $_GET [ 'token' ])) {
// Fallback for Server-Sent Events (SSE) where headers can't be set
$authHeader = 'Bearer ' . $_GET [ 'token' ];
}
Error Handling
Global Exception Handler
All uncaught exceptions are logged and returned as JSON:
backend/src/bootstrap.php
set_exception_handler ( function ( $e ) {
$msg = "Uncaught Exception: " . $e -> getMessage () . " \n " .
"File: " . $e -> getFile () . ":" . $e -> getLine ();
\App\Helpers\ Logger :: error ( $msg );
\App\Helpers\ Response :: error (
"Ocurrió un error interno en el servidor." ,
500
);
});
Controlled Error Responses
// 400 Bad Request
Response :: error ( "Missing required field: email" , 400 );
// 401 Unauthorized
Response :: error ( "Invalid credentials" , 401 );
// 403 Forbidden
Response :: error ( "You don't have permission to delete songs" , 403 );
// 404 Not Found
Response :: error ( "Song not found" , 404 );
// 500 Internal Server Error
Response :: error ( "Database connection failed" , 500 );
CORS Configuration
Why CORS is needed:
During development, React runs on localhost:5173 while the API is on localhost or a different domain.
backend/src/Helpers/Cors.php
class Cors
{
public static function init ()
{
$origin = $_SERVER [ 'HTTP_ORIGIN' ] ?? '' ;
$allowed_origins = [
'http://localhost:5173' , // Vite dev server
'http://localhost:4173' , // Vite preview
'https://musicservicemanager.net'
];
if ( in_array ( $origin , $allowed_origins )) {
header ( "Access-Control-Allow-Origin: $origin " );
header ( "Access-Control-Allow-Credentials: true" );
}
header ( "Access-Control-Allow-Methods: GET, POST, OPTIONS, PUT, DELETE" );
header ( "Access-Control-Allow-Headers: Content-Type, Authorization" );
// Handle preflight OPTIONS request
if ( $_SERVER [ 'REQUEST_METHOD' ] == 'OPTIONS' ) {
http_response_code ( 200 );
exit ;
}
}
}
CORS is initialized in bootstrap:
backend/src/bootstrap.php
\App\Helpers\ Cors :: init (); // First thing after autoloader
Request Optimization
Single Entry Point : All API requests go through one file (api/index.php)
Lazy Loading : Frontend code-splits by route
Prepared Statements : Database queries are cached by MySQL
JWT Caching : Secret key is cached to avoid repeated file reads
Logging & Monitoring
Logger :: info ( "Auth success: User ( $email ) authenticated." );
Logger :: error ( "DB Connection Error: " . $e -> getMessage ());
Logs are written to backend/logs/app.log for debugging production issues.
Deployment Checklist
Build Frontend
cd frontend
npm run build
This creates frontend/dist/ with optimized assets.
Upload Files
Upload frontend/dist/* to public_html/
Upload backend/ folder to parent of public_html/
Configure Database
Create backend/config/database.env: DB_HOST =localhost
DB_USER =your_user
DB_PASS =your_password
DB_NAME =ministryhub
JWT_SECRET =your-random-secret-key
RECAPTCHA_SECRET_KEY =your-recaptcha-key
Set Permissions
chmod 755 backend/logs
chmod 644 backend/config/database.env
Test
Visit https://yourdomain.com/api/auth/login to ensure the API is accessible.
Next Steps
Technology Stack Explore the technologies powering MinistryHub
Database Learn about the database schema and queries
Security Deep dive into authentication and authorization
API Reference Complete API endpoint documentation