PFMIS is three separate deployables that talk to each other over HTTPS. Deploy them in the order below — the database first, because the backend needs it; the backend next, because the frontend points at it; the frontend last.
| Tier | Technology | Lives on | Public address |
|---|---|---|---|
| Database | MariaDB / MySQL | cPanel (phpMyAdmin) | — (internal only) |
| Backend API | PHP 8, no framework | cPanel subdomain | api.houseofloveafrica.org |
| Frontend | React + Vite (static build) | Vercel | farms.houseofloveafrica.org |
Three things cause the large majority of deployment pain. Get these right and most problems never appear:
/public — never the repository root.https://, no trailing slash.acct_pfmis) — always use the prefixed name your panel shows you.You will reuse these across several steps. Fill in the right column now.
| What | Example | Yours |
|---|---|---|
| API subdomain | api.houseofloveafrica.org | ____________ |
| Database name | acct_pfmis | ____________ |
| Database user | acct_pfmis | ____________ |
| Database password | (strong, random) | ____________ |
| Admin email | you@houseofloveafrica.org | ____________ |
| Admin password | (you sign in with this) | ____________ |
You will also need: your cPanel login, your Vercel login, and the two code repositories (pfmis-backend and pfmis-web) — download each as a ZIP from GitHub via Code → Download ZIP.
pfmis → Create. Note the full prefixed name it returns (e.g. acct_pfmis).sql/schema.sql → Go.#1044 - Access denied for user '…'@'localhost' to database 'pfmis' — the import "succeeded" but no tables were created.
Cause: the schema began with CREATE DATABASE pfmis; USE pfmis;. On shared hosting your database is pre-created with a prefix and your user has no rights to a bare pfmis, so the USE failed and every CREATE TABLE after it ran against the wrong database.
With your database still selected, Import sql/seed-lists.sql. This fills the app's dropdowns (houses, breeds, feed types, vaccines, sale items, etc.) with sensible defaults. It is safe to re-run and everything is editable later in the app under Configurations.
The Configurations page opened as a blank white screen in production.
Cause: a fresh database has an empty managed_lists table, so the page received no list keys and crashed reading them. It only appeared in production because local testing already had list data.
No user accounts ship with the system, and you can't create one without logging in — so the first admin is inserted by hand, once. First generate a scrambled ("hashed") password:
pfmis-backend/public, create a file hash.php containing:
<?php echo password_hash('YOUR-ADMIN-PASSWORD', PASSWORD_BCRYPT);https://api.houseofloveafrica.org/hash.php and copy the $2y$… string it prints.hash.php immediately.Leaving hash.php on the server is a security hole. Delete it the moment you've copied the hash.
Then in phpMyAdmin → your database → SQL tab, run (substitute your name, email, and the hash):
-- 1) your farm INSERT INTO farms (name) VALUES ('House of Love Africa'); -- 2) an Admin role with full access INSERT INTO roles (farm_id, name, description, is_system, permissions) VALUES (1, 'Admin', 'Full access to every module', 1, '{"dashboard":"edit","flocks":"edit","daily":"edit","eggs":"edit","feed":"edit","health":"edit","vaccination":"edit","inventory":"edit","sales":"edit","expenses":"edit","hr":"edit","payroll":"edit","reports":"edit","settings":"edit"}'); -- 3) you (paste your $2y$ hash where shown) INSERT INTO users (farm_id, role_id, name, email, password_hash, status) VALUES (1, 1, 'Your Name', 'you@houseofloveafrica.org', 'PASTE-THE-HASH-HERE', 'active');
public_html).pfmis-backend ZIP, then Extract it.pfmis-backend. Delete the ZIP.Only the public/ subfolder should be web-reachable. Keeping the code above the web root means your database password (in config.php) and source files can never be downloaded from the internet.
api under houseofloveafrica.org.pfmis-backend/public
The document root must end in /public. Pointing it at pfmis-backend alone exposes your config and source code to the public internet.
pfmis-backend/config, copy config.example.php to config.php.config.php — after each ?: put your real values:'host' => getenv('DB_HOST') ?: 'localhost',
'name' => getenv('DB_NAME') ?: 'acct_pfmis',
'user' => getenv('DB_USER') ?: 'acct_pfmis',
'pass' => getenv('DB_PASS') ?: 'your-db-password',
...
'cors_allow_origin' => getenv('CORS_ORIGIN') ?: 'https://farms.houseofloveafrica.org',
Must match your frontend URL exactly — https://, no trailing slash. This is what lets your site call the API and blocks every other site. If it is wrong, the app loads but every data request fails with a CORS error in the browser console.
Login succeeded, then the app bounced back to the login screen after 1–2 seconds, repeatedly.
Cause: Apache / FastCGI / PHP-FPM on cPanel strips the Authorization: Bearer … header before PHP sees it. Every request after login arrived with no token, returned 401, and the app treated that as an expired session and logged out. (It works locally because the built-in PHP dev server forwards the header.)
Edit pfmis-backend/public/.htaccess (enable Show Hidden Files in File Manager settings first) so it forwards the header:
RewriteEngine On
RewriteCond %{HTTP:Authorization} .
RewriteRule ^ - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [QSA,L]
Already committed to the repo. If a session still drops on an unusual host, add CGIPassAuth On at the top of the same file (remove it again if that triggers a 500 error).
cPanel → SSL/TLS Status → tick api.houseofloveafrica.org → Run AutoSSL. Usually a certificate is issued automatically for new subdomains.
Open https://api.houseofloveafrica.org/healthz. You should see exactly:
{"ok":true,"service":"pfmis-api"}
If you see that, the backend is live. If not, see Troubleshooting before continuing.
VITE_API_URL = https://api.houseofloveafrica.org(no trailing slash; scope: Production)
Refreshing any page other than the home page showed Vercel's 404: NOT_FOUND screen.
Cause: the app changes the URL (e.g. /flocks) entirely in the browser. On refresh, Vercel looks for a real file at that path, finds none, and serves its 404. It only works on / because that maps to a real file.
Add vercel.json to the frontend repo root so every non-file path serves the app:
{
"rewrites": [
{ "source": "/(.*)", "destination": "/index.html" }
]
}
Already committed. Static assets (JS/CSS/images) are still served normally — the rule only catches app routes. This also fixes the public invoice-verification links (/verify/sale/…).
Vercel → project → Settings → Domains → add farms.houseofloveafrica.org, then create the CNAME record Vercel shows you (pointing to cname.vercel-dns.com) in your DNS. Wait for it to verify and issue SSL.
Once the Vercel redeploy is green, open https://farms.houseofloveafrica.org and work down this checklist.
| Check | Expected result |
|---|---|
| Sign in with your admin email + password | Lands on the Dashboard (no bounce back to login) |
| Refresh while on Flocks / Sales | Stays on that page — no 404 |
| Open Configurations | Shows the managed lists — not a blank page |
| Open Settings → Users & Roles | You can add more staff (no more SQL needed) |
| Create a test sale, open its PDF, scan the QR | The public verification page loads |
| Browser tab | Shows the farm's logo and name once a logo is uploaded |
| Symptom | Cause | Fix |
|---|---|---|
#1044 Access denied … to database 'pfmis'; no tables after import | Schema tried to create/switch a bare database name | Import a schema with no CREATE DATABASE/USE, with your prefixed DB selected (§1.2) |
| Login works, then logs out after 1–2 s | Apache strips the Authorization header | Add the header-forwarding lines to .htaccess (§2.4) |
404: NOT_FOUND on refreshing an inner page | No SPA fallback on Vercel | Add vercel.json rewrite (§3.2) |
| Configurations page is blank | Empty managed_lists on a fresh DB | Update the app (fixed) and/or import seed-lists.sql (§1.3) |
/healthz shows raw PHP or a file list | Document root not at /public | Reset the subdomain document root (§2.2) |
| App loads but data calls fail with a CORS error | cors_allow_origin ≠ frontend URL, or bad VITE_API_URL | Match both exactly, no trailing slash (§2.3 / §3.1) |
| Blank page or 500 from the API | Wrong DB credentials, or schema not imported | Re-check config.php (§2.3) and the import (§1.2) |
{"error":"Route not found…"} | None — this is the API working (it returns JSON) | Test with /healthz instead |
| "Too many attempts" on login | Rate limiter (8 tries / 10 min) protecting the account | Wait a few minutes — expected behavior |
Vercel already redeploys the frontend on every push to main. The backend repo includes a GitHub Action that does the same over FTPS — it just needs your server's FTP details, added once. It is configured to never overwrite your config.php or uploaded logos.
pfmis-backend → Settings → Secrets and variables → Actions → add these four secrets:| Secret | Value |
|---|---|
| FTP_SERVER | your FTP host (e.g. ftp.houseofloveafrica.org) |
| FTP_USERNAME | the FTP username |
| FTP_PASSWORD | the FTP password |
| FTP_SERVER_DIR | /pfmis-backend/ — the code folder, not /public |
Auto-deploy syncs code only. Any new table or column (a schema change) must still be imported through phpMyAdmin, exactly like §1.2.
.sql of your farm's data (excludes login credentials). Grab one periodically. Restore by importing it into a schema-applied database.config.php above the web root and never commit it.AuthController::forgotPassword() (send the token by email instead of discarding it).