# EduSol K-12 — Setup Guide

End-to-end bring-up for the EduSol K-12 SaaS platform on a Linux server that **already runs MySQL and Apache**. PostgreSQL 16 is installed alongside MySQL (different port, different data directory — they don't conflict). Apache continues to serve other vhosts; EduSol is developed against `php artisan serve` and `vite dev`, with optional Apache vhosts for production.

---

## 0. Prerequisites

You should already have:
- Ubuntu 22.04+ or Debian 12+ with `sudo` access
- Apache 2.4+ running on port 80
- MySQL/MariaDB running on port 3306
- PHP 8.2+ with Composer
- Node 20+ with npm
- Git

Verify versions:
```bash
php --version          # ≥ 8.2
composer --version     # ≥ 2.x
node --version         # ≥ 20
npm --version
mysql --version        # informational; we won't touch MySQL
apache2 -v
```

---

## 1. Install PostgreSQL 16 alongside MySQL

PostgreSQL listens on **5432** by default; MySQL is on **3306**. They coexist with no extra configuration. Apache is irrelevant here — Postgres talks TCP, not HTTP.

### 1.1 Add the official PostgreSQL APT repository

The Ubuntu/Debian default repo ships an older Postgres. We want 16+ for `jsonb`, `citext`, and `pg_trgm` features the schema relies on.

```bash
# Add the PostgreSQL APT signing key + repository
sudo install -d /usr/share/postgresql-common/pgdg
sudo curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc \
    --fail https://www.postgresql.org/media/keys/ACCC4CF8.asc

echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] \
https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" \
| sudo tee /etc/apt/sources.list.d/pgdg.list

sudo apt update
```

### 1.2 Install PostgreSQL server + client + contrib

```bash
sudo apt install -y \
    postgresql-16 \
    postgresql-client-16 \
    postgresql-contrib-16
```

What each package gives you:
- `postgresql-16` — the server itself (creates a `postgres` system user, initialises a cluster on port 5432, starts a `postgresql.service` systemd unit).
- `postgresql-client-16` — the `psql` CLI plus dump/restore tools.
- `postgresql-contrib-16` — bundles the extensions EduSol uses: **`citext`**, **`pg_trgm`**, **`uuid-ossp`**.

### 1.3 Install the PHP PostgreSQL extensions

These are mandatory for Laravel + Stancl tenancy. Match the version to your installed PHP.

```bash
# Substitute 8.2 with whatever `php --version` reports (8.2 / 8.3 / 8.4)
sudo apt install -y \
    php8.2-pgsql \
    php8.2-pdo-pgsql \
    php8.2-mbstring \
    php8.2-xml \
    php8.2-bcmath \
    php8.2-curl \
    php8.2-intl \
    php8.2-zip \
    php8.2-gd
```

If only `php-pgsql` is offered (no version suffix), that's fine — it'll match the active PHP-FPM/CLI binary.

Verify both extensions loaded:
```bash
php -m | grep -E "^(pgsql|pdo_pgsql)$"
# expected output:
# pdo_pgsql
# pgsql
```

If either is missing, restart PHP-FPM and the Apache module:
```bash
sudo systemctl restart php8.2-fpm
sudo systemctl reload apache2
```

### 1.4 Confirm the cluster is running

```bash
sudo systemctl status postgresql       # should be "active (exited)" — wrapper unit
sudo systemctl status postgresql@16-main   # the actual cluster process
sudo -u postgres psql -c "SELECT version();"
```

You should see `PostgreSQL 16.x …`.

### 1.5 Create the EduSol database + role

```bash
sudo -u postgres psql <<'SQL'
CREATE ROLE skills WITH LOGIN PASSWORD 'gr8skillz';
ALTER ROLE skills CREATEDB CREATEROLE;
CREATE DATABASE edusol_k12 OWNER skills;
GRANT ALL PRIVILEGES ON DATABASE edusol_k12 TO skills;
SQL
```

Substitute `skills` / `gr8skillz` with your own credentials in production. The role needs **CREATEDB** because Stancl tenancy uses Postgres role privileges to create per-tenant schemas inside the database.

### 1.6 Enable the extensions EduSol relies on

EduSol's first migration enables `citext`. The `pg_trgm` extension is enabled in Phase 12 with `WITH SCHEMA public` so every tenant's `search_path` can resolve `gin_trgm_ops`. You don't need to enable them manually — the migrations do it — but the `postgres` superuser must have permission. The `postgresql-contrib-16` package above is the only requirement.

### 1.7 Verify connectivity from PHP

```bash
php -r '
$dsn = "pgsql:host=127.0.0.1;port=5432;dbname=edusol_k12";
$pdo = new PDO($dsn, "skills", "gr8skillz");
echo $pdo->query("SELECT current_database(), current_user, version()")->fetchColumn(2)."\n";
'
```

If this prints the Postgres version, you're done with section 1.

---

## 2. Coexistence notes — MySQL + Apache stay untouched

| Resource | MySQL | PostgreSQL | Conflict? |
|---|---|---|---|
| TCP port | 3306 | 5432 | No |
| Data dir | `/var/lib/mysql` | `/var/lib/postgresql/16/main` | No |
| Service | `mysql.service` | `postgresql@16-main.service` | No |
| Default socket | `/var/run/mysqld/mysqld.sock` | `/var/run/postgresql/.s.PGSQL.5432` | No |

EduSol never touches your existing MySQL databases. Apache continues to serve its existing vhosts on port 80; EduSol's frontend dev server runs on port 5173 (Vite) and the API on port 8000 (`php artisan serve`) so they don't compete with Apache.

If you ever want to run Postgres on a different port (because something else holds 5432, or you want to firewall it), edit `/etc/postgresql/16/main/postgresql.conf` and change `port = 5432`, then `sudo systemctl restart postgresql@16-main`.

---

## 3. Clone the repos

EduSol is two repos:

```bash
sudo mkdir -p /var/www/html/blouza
sudo chown $USER: /var/www/html/blouza
cd /var/www/html/blouza

git clone <backend-repo-url>  k12-edusol-api
git clone <frontend-repo-url> k12-edusol
```

If they're already cloned, skip to section 4.

---

## 4. Backend setup

```bash
cd /var/www/html/blouza/k12-edusol-api
composer install --no-interaction --prefer-dist
cp .env.example .env
php artisan key:generate
```

### 4.1 Configure `.env`

Open `.env` and set:

```ini
APP_NAME="EduSol K-12"
APP_ENV=local
APP_DEBUG=true
APP_URL=http://localhost:8000

DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=edusol_k12
DB_USERNAME=skills
DB_PASSWORD=gr8skillz

# Session / cache / queue — leave on file/sync until you're ready for Redis.
CACHE_STORE=file
QUEUE_CONNECTION=sync
SESSION_DRIVER=file

# Mail (Mailgun SMTP — replace with your sandbox creds during dev)
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailgun.org
MAIL_PORT=587
MAIL_USERNAME=postmaster@your-sandbox.mailgun.org
MAIL_PASSWORD=...
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=no-reply@blouza.local
MAIL_FROM_NAME="EduSol"

# CBS payment gateway (only needed when you want to test payments)
CBS_BASE_URL=...
CBS_CLIENT_ID=...
CBS_CLIENT_SECRET=...
IGR_PERCENT=15
PROVIDER_PERCENT=70
MDA_PERCENT=15
MDA_ID=...
MDA_NAME="EduSol"
FRONTEND_URL=http://localhost:5173
```

The `FRONTEND_URL` value is shared between mail templates (welcome links) and CBS payment callbacks; keep them aligned.

### 4.2 Run landlord migrations + seed the platform tables

```bash
php artisan migrate          # runs everything under database/migrations (landlord)
php artisan db:seed           # runs Database\Seeders\DatabaseSeeder, which chains the three Landlord seeders
```

This creates the platform-wide tables (`tenants`, `domains`, `subscription_plans`, `platform_users`, `payment_invoices_index`, etc.) and seeds the subscription features, subscription plans, and Blouza super-admin (`admin@blouza.com` / `password`).

**Note:** The landlord entry point is `Database\Seeders\DatabaseSeeder` (the file directly under `database/seeders/`), not `Database\Seeders\Landlord\DatabaseSeeder` (which doesn't exist — the `Landlord/` directory only holds the per-domain sub-seeders that the root file chains).

### 4.3 Set up the public storage symlink

```bash
php artisan storage:link
```

Required for uploaded logos, receipts, report cards, expense receipts.

### 4.4 Provision your first tenant

```bash
php artisan institution:provision \
    "Blouza Academy" \
    blouza \
    info@blouza.edu.ng \
    --domain=blouza.edusol.test \
    --plan=premium
```

Behind the scenes this:
1. Creates a row in `tenants`.
2. Creates a Postgres schema `tenant_<uuid>` inside `edusol_k12`.
3. Runs every migration under `database/migrations/tenant/` against that schema.
4. Runs `Database\Seeders\Tenant\DatabaseSeeder` (210 permissions × 8 system roles + default programs/grades/subjects/houses/CBT vault/admission stages).
5. Creates the first school-admin user with a temporary password (printed in the command output).

---

## 5. Frontend setup

```bash
cd /var/www/html/blouza/k12-edusol
npm install
```

Create `.env.local`:

```ini
VITE_API_BASE_URL=http://localhost:8000/api
```

That's all the frontend needs — `apiClient` injects the bearer token + tenant slug from `localStorage` per request.

---

## 6. Day-to-day launch

You need two terminals.

**Terminal 1 — backend:**
```bash
cd /var/www/html/blouza/k12-edusol-api
php artisan serve --port=8000
```

**Terminal 2 — frontend:**
```bash
cd /var/www/html/blouza/k12-edusol
npm run dev
```

Open http://localhost:5173 → click any role on the login screen (handler auto-fills demo credentials + tenant slug).

### Demo credentials (already seeded on `blouza`)

| Role | Email | Password | Notes |
|---|---|---|---|
| Blouza super-admin (platform) | `admin@blouza.com` | `password` | Logs into `/blouza/*` via the platform guard, no tenant slug needed |
| School admin | `info@blouza.edu.ng` | `password` | Tenant slug: `blouza` |
| Teacher | `teacher@edusol.com` | `password` | |
| Student | `student@edusol.com` | `password` | Admission `DEMO-001` |
| Parent | `parent@edusol.com` | `password` | Linked to `DEMO-001` |
| Accountant | `finance@edusol.com` | `password` | |

---

## 7. Useful side channels

| Task | Command |
|---|---|
| Provision another tenant | `php artisan institution:provision "Demo Academy" demo admin@demo.test --domain=demo.edusol.test --plan=premium` |
| Drop a tenant (irreversible) | `php artisan institution:destroy <slug> --force` |
| Re-seed the demo data on an existing tenant | `php artisan tenants:seed --tenants=<uuid>` |
| List tenants | `php artisan tenants:list` |
| Anchor smoke (28 endpoints) | `./tests/smoke.sh <bearer-token> blouza` |
| Mint a token without UI | `php artisan tinker` then `Stancl\Tenancy\Facades\Tenancy::initialize(\App\Models\Landlord\Institution::where('slug','blouza')->first()); echo \App\Models\Tenant\User::where('email','info@blouza.edu.ng')->first()->createToken('cli')->plainTextToken;` |
| Tail backend logs | `tail -f storage/logs/laravel.log` |
| Connect to a tenant schema | `PGPASSWORD=gr8skillz psql -h localhost -U skills -d edusol_k12 -c "SET search_path TO \"tenant_<uuid-with-dashes>\"; \dt"` |
| Frontend type-check | `cd /var/www/html/blouza/k12-edusol && npx tsc --noEmit -p tsconfig.app.json` |
| Backend lint | `./vendor/bin/pint` |

---

## 8. What you do NOT need

- **Redis** — default `CACHE_STORE=file`. Flip to `redis` only after `php artisan cache:verify` passes.
- **A WebSocket / Reverb / Pusher process** — communications uses DB notifications + REST polling.
- **Subdomain DNS for local dev** — frontend sends `X-Tenant-Id: <slug>`; subdomain resolution is a production-only path.
- **Apache vhost for development** — `php artisan serve` is the canonical local API; Apache is only relevant when you cut a production deploy.

---

## 9. Production notes (when you're ready)

- Move `php artisan serve` → an Apache or Nginx vhost pointing at `public/index.php`. Apache's existing config is untouched; just add a new vhost on a different port (e.g. `8080`) or a new subdomain.
- Switch `QUEUE_CONNECTION=database` (or `redis`) and run `php artisan queue:work --tries=3` under systemd — needed for CSV bulk imports, report-card PDF jobs, and welcome mails.
- Switch `CACHE_STORE=redis` after running `php artisan cache:verify --tenant=<slug>`. The default `file` driver can't satisfy Stancl's taggable proxy.
- Add a wildcard subdomain DNS record (`*.edusol.app → server-ip`) so each tenant gets `<slug>.edusol.app` for production tenant routing.
- Set up a daily `pg_dump` cron + monthly base backup. The whole platform sits in one database (`edusol_k12`), so a single dump captures every tenant.

---

## 10. Troubleshooting

**"could not find driver" when running `php artisan migrate`**
The `php-pgsql` / `php-pdo-pgsql` extensions aren't loaded against the CLI binary. Run `php -m | grep pgsql` to check; if missing, re-install per section 1.3 and restart `php-fpm`.

**"operator class \"gin_trgm_ops\" does not exist"**
The `pg_trgm` extension landed in a tenant-private schema instead of `public`. Fix:
```sql
ALTER EXTENSION pg_trgm SET SCHEMA public;
```
The migrations now create it `WITH SCHEMA public`, so this only affects clusters provisioned before the fix.

**"This cache store does not support tagging."**
You set `CACHE_STORE=redis` without finishing the verification flow. Either flip back to `file` or run `php artisan cache:verify` until all three checks pass.

**"No query results for model [MessageThread]"**
You passed a numeric thread id to a route that resolves on ULID. Use the `ulid` field returned by `/v1/messages/conversations`, not the `thread_id`.

**Postgres won't start: "could not bind IPv4 address … Address already in use"**
Another service holds 5432. Either change Postgres's port (`/etc/postgresql/16/main/postgresql.conf`) or stop the conflicting service.

**Stancl tenancy: "tenant has no domain"**
You provisioned without `--domain=…`. Add a row manually:
```bash
php artisan tinker
> $t = \App\Models\Landlord\Institution::where('slug','blouza')->first();
> $t->domains()->create(['domain' => 'blouza.edusol.test']);
```

---

## 11. Where things live

| Concern | Location |
|---|---|
| Phase status | `PROGRESS.md` |
| Architecture & conventions | `CLAUDE.md` |
| Restructure plan | `/home/temitope/.claude/plans/dynamic-launching-kahan.md` |
| CSV seed data | `database/data/{defaults,landlord,demo,setup-templates}/` |
| Permission catalog | `database/data/defaults/permission_modules.csv` |
| Tenant base seeder | `database/seeders/Tenant/DatabaseSeeder.php` |
| Anchor smoke runner | `tests/smoke.sh` |
| Frontend repo | `/var/www/html/blouza/k12-edusol` |
