🎓 Solvo

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

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

Don't run 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:

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.

If your host doesn't support .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:

  1. Checks PHP version, required extensions, and that storage/ is writable.
  2. Checks the database connects.
  3. Applies the schema (db/01_schema.sql) if it hasn't been already — one click.
  4. 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.
Immediately after your Super Admin account is created, set 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:

IntegrationWhat it needsWhat 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.
Email 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

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