Installation
Plain PHP + MySQL/MariaDB — no Composer, no build step, no framework. Runs on shared cPanel hosting, a VPS, a dedicated server, or local development.
Requirements
- PHP 8.1 or later, with these extensions (standard on virtually every
host):
pdo_mysql,curl,mbstring,json - MySQL 5.7+ or MariaDB 10.2.1+ — the schema uses
CHECKconstraints (for ElimuPay's dual-scoped tables) that need at least these versions - A way to set environment variables, or create a
.envfile — both work, covered below - Enough disk space for uploaded photos, generated PDFs, and documents
(
storage/) — scales with usage, no fixed minimum
You do not need: Composer, Node/npm, SSH access (though it helps), or any particular web server — Apache, LiteSpeed, and Nginx are all covered below.
1Get the files onto your server
Upload the entire project (unzip it if you downloaded it as a zip) to your server. On
cPanel, this is usually via File Manager or FTP into a directory outside public_html
(see step 5 for why).
2Create the database
Using phpMyAdmin, cPanel's "MySQL Databases" tool, or the mysql CLI, create an
empty database and a database user with full privileges on it. Note the database name,
username, password, and host (usually 127.0.0.1 or localhost —
cPanel sometimes uses a different host for MySQL specifically; check cPanel's MySQL page if
unsure).
db/01_schema.sql manually yet. The installer (step
4) does this for you in one click. If you'd rather do it by hand (phpMyAdmin's SQL tab, or
mysql yourdb < db/01_schema.sql), that's fine too — the installer detects
an already-applied schema and skips straight past that step.
3Configure .env
Copy .env.example to .env (same directory as
bootstrap.php, i.e. the project root) and fill in real values:
cp .env.example .env
At minimum, set:
DB_HOST,DB_NAME,DB_USER,DB_PASS— from step 2INSTALL_ENABLED=true— temporarily, just for the next step
Everything else (M-Pesa, SMS, ElimuPay secrets, etc.) has safe placeholder defaults and can be configured later, once you're ready to actually use those features — the platform runs fine without them.
.env files (some locked-down
shared hosts don't allow dotfiles, or your control panel sets environment variables its own
way): set the same variable names as real environment variables through your hosting
control panel instead. The app only reads .env if it exists — if it doesn't,
getenv() calls fall through to whatever's set at the server level, with no
code changes needed either way.
4Run the installer
Visit https://your-domain.example.com/install.php in a browser (adjust the path
if your document root isn't set yet — see step 5 first if you're unsure where that URL should
point).
The installer:
- Checks PHP version, required extensions, and that
storage/is writable. - Checks the database connects.
- Applies the schema (
db/01_schema.sql) if it hasn't been already — one click. - Lets you create your Super Admin account — name, email, password. This is the one and only account that can onboard schools and manage the platform; nothing else in the system can create it, which is exactly why this installer exists.
INSTALL_ENABLED=false in .env (or delete
public/install.php entirely — either works). The installer refuses to create
a second Super Admin once one exists, but there's no reason to leave the door open at all
once you're done.
If you ever need to run it again on a fresh database (a new staging environment, for
example), the same three steps apply: set INSTALL_ENABLED=true, visit the URL,
turn it back off when done.
5Point your web server at public/
This is the single most important configuration step. The public/ directory is
the only one meant to be reachable by a browser — everything else
(core/, config/, db/, storage/,
vendor/) contains application code, credentials, or uploaded files that must never
be served directly.
.htaccess files are already in place in every non-public directory as
defense-in-depth (they deny all access outright, so even a misconfigured document root won't
expose them on Apache/LiteSpeed) — but the correct setup is still to point your document root
at public/ directly, not rely on that safety net alone.
cPanel
If this is an addon domain or subdomain, cPanel's "Domains" page lets you set the
Document Root directly to .../mojamoza/public when you create it.
If it's your primary domain and you can't easily change its document root, either move the
whole project up one level and point the domain at a folder that only contains what's in
public/ today, ask your host to set the document root for you, or use a subdomain
for the app instead of the primary domain's root.
Apache / LiteSpeed (VPS or dedicated server)
<VirtualHost *:443>
ServerName your-domain.example.com
DocumentRoot /var/www/mojamoza/public
<Directory /var/www/mojamoza/public>
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
Nginx
.htaccess files do nothing under Nginx — use this instead:
server {
listen 443 ssl;
server_name your-domain.example.com;
root /var/www/mojamoza/public;
index index.php;
# Deny the non-public directories outright, mirroring what the
# .htaccess files do on Apache.
location ~ ^/(core|config|db|storage|vendor|scripts)/ {
deny all;
return 404;
}
location ~ /\.(env|git) {
deny all;
return 404;
}
location / {
try_files $uri $uri/ =404;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.1-fpm.sock; # adjust to your PHP-FPM socket/version
}
}
Local development (XAMPP/MAMP/php -S)
Quickest option — from the project root:
php -S localhost:8000 -t public
Then visit http://localhost:8000/install.php.
6Set up cron jobs
Five background scripts need to run on a schedule (overdue-invoice marking, fee reminders, attendance alerts, etc.) — see Cron Jobs for the full list and exact syntax for both cPanel and a standard crontab.
7Configure optional integrations (whenever you're ready)
None of these block getting the platform running — configure them when you actually need the feature:
| Integration | What it needs | What happens without it |
|---|---|---|
| M-Pesa Daraja | MPESA_* variables in .env. Ships with Safaricom's own
published sandbox test credentials — real values needed before production.
MPESA_CALLBACK_BASE_URL must be a real public HTTPS URL. |
Fee payments and ElimuPay instalment collection won't work until credentialed. |
| SMS (Africa's Talking) | AT_* variables in .env. |
Notifications queue as pending rather than failing outright — nothing
breaks, they just don't send until credentialed. |
Nothing extra — uses PHP's built-in mail(), works out of the box on
most hosts. |
mail() only confirms local acceptance, not delivery. If emails aren't
arriving, check your host's mail logs or SPF/DKIM setup. |
Post-install security checklist
INSTALL_ENABLED=falsein.env(orpublic/install.phpdeleted).envhas real, non-CHANGE_MEvalues for everything you're actually using- Document root points at
public/, not the project root storage/is writable by the web server user but not itself web-reachable — verify: visitinghttps://your-domain/storage/should 403 or 404, never list files- HTTPS is enabled — required for M-Pesa callbacks regardless, and strongly recommended
everywhere else since session cookies are marked
secureby default, which means login simply won't work over plain HTTP unless you deliberately change that - Cron jobs are set up (step 6)
- You've signed in as your Super Admin and onboarded your first school
Troubleshooting
"This site can't be reached" / blank page
Check your PHP error log (cPanel: "Errors" under Metrics, or error_log in the
same directory as the failing script). Almost always a PHP version mismatch or a missing
extension — re-run install.php's requirements check to confirm.
Installer says "Could not connect to the database"
Double-check DB_HOST; on some hosts this is a specific hostname (like
localhost:3306 or a dedicated internal hostname) rather than
127.0.0.1. cPanel's MySQL Databases page usually states the correct host.
Login page loads but signing in does nothing / redirects back to login
Check that session.secure_cookie in config.php matches reality: if
you're testing over plain HTTP (e.g. local dev without HTTPS), the secure-cookie flag will
prevent the session cookie from ever being set. For local development without HTTPS,
temporarily set it to false in config.php — never do this in
production.
"Installation already complete" but you don't have Super Admin credentials
Either use Forgot your password?, or if that's not viable
(e.g. the email on file is no longer accessible), connect directly to the database and update
the relevant row's email in the users table so a reset can be sent to
an address you control, then use the reset flow. There is deliberately no UI path around the
"one super admin creation" safeguard.
403 Forbidden on every URL, including ones that don't exist
This means your document root is pointing at the project root rather than
public/ (e.g. you're reaching the app at
https://yourdomain.com/projectname/... instead of the domain's own root or a
subdomain rooted at public/). The project root's own .htaccess
deliberately denies all access as a safety net for core/, config/,
etc. — and Apache cascades that deny into every subdirectory, including public/,
unless something explicitly overrides it there. public/.htaccess does exactly
that override, so this should already work correctly out of the box. If you're still seeing
it, make sure you have the current version of public/.htaccess (it should contain
a Require all granted block near the top) and that your host has
AllowOverride All enabled for your account (ask your host if unsure — some very
locked-down shared hosts disable .htaccess processing entirely, which needs a
different fix specific to that host). Either way, the properly supported setup is still to
point your document root directly at public/ — see step 5 above.
Updating an existing installation
New features occasionally add columns or tables. Each one ships as a numbered file under
db/migrations/ — run any you haven't already applied, in filename order, the same
way you'd run any SQL file. Every migration file states clearly at the top whether it's needed
for your installation (skip it if you set up fresh from the current
db/01_schema.sql, which already includes everything).