Deployment Runbook

PFMIS
Deployment Guide

Standing up the Poultry Farm Management Information System end to end — Vercel frontend, cPanel PHP backend, and MySQL database — with the fix for every issue encountered during the real deployment.
SystemPFMIS — Poultry Farm Management Information System
FrontendReact + Vite on Vercel  ·  https://farms.houseofloveafrica.org
BackendPHP 8 REST API on cPanel  ·  https://api.houseofloveafrica.org
DatabaseMariaDB / MySQL via phpMyAdmin
AudienceOperator deploying the system (no backend-developer experience assumed)

0How the pieces fit together

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.

TierTechnologyLives onPublic address
DatabaseMariaDB / MySQLcPanel (phpMyAdmin)— (internal only)
Backend APIPHP 8, no frameworkcPanel subdomainapi.houseofloveafrica.org
FrontendReact + Vite (static build)Vercelfarms.houseofloveafrica.org
Golden rules

Three things cause the large majority of deployment pain. Get these right and most problems never appear:

Before you begin — record these values

You will reuse these across several steps. Fill in the right column now.

WhatExampleYours
API subdomainapi.houseofloveafrica.org____________
Database nameacct_pfmis____________
Database useracct_pfmis____________
Database password(strong, random)____________
Admin emailyou@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.

1Database (phpMyAdmin)

1.1  Create the database and user

  1. cPanel → MySQL® Databases.
  2. Under Create New Database, name it pfmis → Create. Note the full prefixed name it returns (e.g. acct_pfmis).
  3. Under MySQL Users → Add New User, create a user with a strong password. Record both.
  4. Under Add User To Database, select the user + database → grant ALL PRIVILEGES.

1.2  Import the schema

  1. cPanel → phpMyAdmin. In the left sidebar, click your database so it is selected.
  2. Top menu → Import → choose sql/schema.sqlGo.
  3. Confirm ~26 tables appear in the left sidebar with no red error.
Issue we hit

#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.

Fixed in the repo: schema.sql no longer creates or switches databases, so it imports into whatever database you have selected. If you ever see #1044, confirm the file has no CREATE DATABASE / USE lines and that your prefixed database is selected first.

1.3  Seed the dropdown lists (recommended)

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.

Issue we hit

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.

Fixed in the app: it now tolerates empty lists and shows "No items yet" instead of crashing. Seeding is optional but gives you working dropdowns on day one.

1.4  Create your first admin login

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:

  1. In cPanel File Manager, inside pfmis-backend/public, create a file hash.php containing:
    <?php echo password_hash('YOUR-ADMIN-PASSWORD', PASSWORD_BCRYPT);
  2. Visit https://api.houseofloveafrica.org/hash.php and copy the $2y$… string it prints.
  3. Delete hash.php immediately.
Do not skip

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');

2Backend API (cPanel)

2.1  Upload the code

  1. In cPanel File Manager, go to your home folder (above public_html).
  2. Upload the pfmis-backend ZIP, then Extract it.
  3. Rename the resulting folder to pfmis-backend. Delete the ZIP.
Why above public_html

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.

2.2  Create the API subdomain

  1. cPanel → Domains (or Subdomains) → create api under houseofloveafrica.org.
  2. Set the Document Root to exactly:
    pfmis-backend/public
Most important setting in this guide

The document root must end in /public. Pointing it at pfmis-backend alone exposes your config and source code to the public internet.

2.3  Create the config file

  1. In pfmis-backend/config, copy config.example.php to config.php.
  2. Edit 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',
CORS origin

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.

2.4  Fix "logged out one second after logging in"

Issue we hit

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.)

The fix

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).

2.5  Enable HTTPS

cPanel → SSL/TLS Status → tick api.houseofloveafrica.orgRun AutoSSL. Usually a certificate is issued automatically for new subdomains.

2.6  Test the API

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.

3Frontend (Vercel)

3.1  Point the frontend at the API

  1. Vercel → your project → SettingsEnvironment Variables.
  2. Add:
    VITE_API_URL = https://api.houseofloveafrica.org
    (no trailing slash; scope: Production)
  3. Vercel → Deployments → open the latest → Redeploy (the variable only applies to a fresh build).

3.2  Fix "404: NOT_FOUND" when refreshing an inner page

Issue we hit

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.

The fix

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/…).

3.3  Custom domain

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.

4Go live & verify

Once the Vercel redeploy is green, open https://farms.houseofloveafrica.org and work down this checklist.

CheckExpected result
Sign in with your admin email + passwordLands on the Dashboard (no bounce back to login)
Refresh while on Flocks / SalesStays on that page — no 404
Open ConfigurationsShows the managed lists — not a blank page
Open Settings → Users & RolesYou can add more staff (no more SQL needed)
Create a test sale, open its PDF, scan the QRThe public verification page loads
Browser tabShows the farm's logo and name once a logo is uploaded

5Troubleshooting quick reference

SymptomCauseFix
#1044 Access denied … to database 'pfmis'; no tables after importSchema tried to create/switch a bare database nameImport a schema with no CREATE DATABASE/USE, with your prefixed DB selected (§1.2)
Login works, then logs out after 1–2 sApache strips the Authorization headerAdd the header-forwarding lines to .htaccess (§2.4)
404: NOT_FOUND on refreshing an inner pageNo SPA fallback on VercelAdd vercel.json rewrite (§3.2)
Configurations page is blankEmpty managed_lists on a fresh DBUpdate the app (fixed) and/or import seed-lists.sql (§1.3)
/healthz shows raw PHP or a file listDocument root not at /publicReset the subdomain document root (§2.2)
App loads but data calls fail with a CORS errorcors_allow_origin ≠ frontend URL, or bad VITE_API_URLMatch both exactly, no trailing slash (§2.3 / §3.1)
Blank page or 500 from the APIWrong DB credentials, or schema not importedRe-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 loginRate limiter (8 tries / 10 min) protecting the accountWait a few minutes — expected behavior

AAutomatic future deploys (optional)

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.

  1. cPanel → FTP Accounts — note the host, username, password.
  2. GitHub → pfmis-backendSettings → Secrets and variables → Actions → add these four secrets:
SecretValue
FTP_SERVERyour FTP host (e.g. ftp.houseofloveafrica.org)
FTP_USERNAMEthe FTP username
FTP_PASSWORDthe FTP password
FTP_SERVER_DIR/pfmis-backend/ — the code folder, not /public
Database changes are still manual

Auto-deploy syncs code only. Any new table or column (a schema change) must still be imported through phpMyAdmin, exactly like §1.2.

BOngoing operation