Portal SDK & PHP Kit

Build custom parent/student portals with the OptiCRM Portal Auth API and PHP Portal Kit

Overview

OptiCRM provides two ways for parents and students to access their data:

  1. Built-in Portal — the /portal/* pages inside the Next.js app (no setup needed)
  2. External Portal via PHP Kit — a separate PHP MVC application that connects to OptiCRM via REST API

The PHP Portal Kit is for institutions or implementors who want a fully customizable portal without touching the OptiCRM core codebase. Any PHP developer can modify the templates, add pages, and change the look — all powered by OptiCRM's data through the Portal Auth API.

Architecture

┌──────────────────────────┐      ┌────────────────────────┐
│   PHP Portal Kit         │      │   OptiCRM Core         │
│   (Implementor's code)   │─────▶│   (Your closed code)   │
│                          │ REST │                        │
│   portal.opticrm.app     │ API  │   school.opticrm.app   │
└──────────────────────────┘      └────────────────────────┘
         ↑                                    ↑
   Parent/Student                      Staff/Admin
   browser                            browser
  • The PHP app handles all presentation (HTML, CSS, templates)
  • OptiCRM handles all data, auth, and business logic
  • API tokens stay server-side — never exposed to the browser
  • One PHP deployment serves all institutions (multi-tenant)

Portal Auth API

The foundation for all external portal integrations. Returns a JWT token that grants access to all portal data endpoints.

Login

POST https://{slug}.opticrm.app/api/portal/auth
Content-Type: application/json
{
  "slug": "rashtrothana",
  "email": "parent@example.com",
  "password": "their-password"
}

Response:

{
  "token": "eyJhbGciOiJIUzI1NiJ9...",
  "expiresIn": "24h",
  "contact": {
    "id": "clx...",
    "firstName": "Rajesh",
    "lastName": "Sharma",
    "email": "parent@example.com",
    "contactType": "CONTACT_PARENT"
  },
  "institution": {
    "id": "clx...",
    "name": "Rashtrothana Vidhya Kendra",
    "slug": "rashtrothana"
  },
  "students": [
    {
      "id": "clx...",
      "firstName": "Aarav",
      "lastName": "Sharma",
      "studentCode": "STU001",
      "relation": "FATHER"
    }
  ]
}

Key fields:

  • token — JWT valid for 24 hours. Store server-side, never in browser.
  • students — linked children. Use students[].id as studentId in data calls.
  • contactType — CONTACT_PARENT or CONTACT_STUDENT

Verify Token

GET https://{slug}.opticrm.app/api/portal/auth
Authorization: Bearer {token}

Returns contact info if token is valid, 401 if expired or invalid.

Data Endpoints

All endpoints require Authorization: Bearer {token} header. Most require ?studentId={id}.

EndpointMethodDescription
/api/portal/my-childrenGETLinked students with class/section
/api/portal/fees?studentId=xxxGETInvoices, amounts, payment status
/api/portal/attendance?studentId=xxxGETAttendance records
/api/portal/timetable?studentId=xxxGETWeekly timetable grid
/api/portal/results?studentId=xxxGETExam results, marks, grades
/api/portal/assignments?studentId=xxxGETAssignments with due dates
/api/portal/transport?studentId=xxxGETRoute, vehicle, driver info
/api/portal/health?studentId=xxxGETHealth profile, screenings, vaccinations
/api/portal/eventsGETUpcoming school events
/api/portal/grievancesGET/POSTSubmit or view grievances
/api/portal/profileGET/PATCHView or update contact profile
/api/portal/change-passwordPOSTChange portal password
/api/portal/analyticsGETSummary statistics
/api/portal/announcementsGETSchool announcements
/api/portal/appointmentsGETPTM appointment slots
/api/portal/consentGET/POSTConsent form submissions
/api/portal/paymentsGETPayment history

Access Control

  • Parents can only access data for their linked students (via ContactStudentLink)
  • Students can only access their own data
  • Accessing another student's data returns 403 Access Denied
  • The students array in the login response lists all accessible student IDs

Money Values

All amounts are stored in paise (1 rupee = 100 paise). Divide by 100 for display:

  • 625000 = ₹6,250.00
  • 2500000 = ₹25,000.00

PHP Portal Kit

Repository

github.com/hemanthopti/opticrm-portal

Requirements

  • PHP 8.0+ with curl extension
  • Any web server (Apache, Nginx) or PHP built-in server
  • No Composer, no framework dependencies

Quick Start

git clone https://github.com/hemanthopti/opticrm-portal.git
cd opticrm-portal
cp .env.example .env
# Edit .env — set OPTICRM_DOMAIN=opticrm.app
php -S localhost:8080 -t public
# Open http://localhost:8080

Project Structure

opticrm-portal/
├── public/
│   ├── index.php          ← Front controller (routes all requests)
│   └── .htaccess          ← Apache URL rewriting
├── app/
│   ├── config.php         ← Loads .env, defines constants
│   ├── Router.php         ← Maps URLs to controllers
│   ├── Controller.php     ← Base class (render, redirect, SDK)
│   ├── Middleware.php      ← Session auth check
│   ├── models/
│   │   └── OptiCRM.php    ← SDK (API client, single file)
│   ├── controllers/
│   │   ├── AuthController.php
│   │   ├── DashboardController.php
│   │   ├── FeesController.php
│   │   ├── AttendanceController.php
│   │   ├── TimetableController.php
│   │   ├── ResultsController.php
│   │   ├── ProfileController.php
│   │   ├── GrievanceController.php
│   │   └── AnnouncementController.php
│   └── views/
│       ├── layout.php     ← Base layout (header + sidebar + content)
│       ├── dashboard.php
│       ├── fees.php
│       ├── attendance.php
│       └── partials/
│           ├── header.php
│           ├── sidebar.php
│           └── student-picker.php
├── .env.example
├── Dockerfile             ← For Docker/Coolify deployment
└── README.md

Configuration

.env file:

OPTICRM_DOMAIN=opticrm.app
APP_NAME=OptiCRM Portal
APP_PRIMARY_COLOR=#2563eb

The portal is multi-tenant — the institution slug is entered at login, not configured in .env. One deployment serves all institutions.

How It Works

  1. Parent visits portal.opticrm.app/login
  2. Enters institution slug (e.g., rashtrothana), email, and password
  3. PHP calls POST https://rashtrothana.opticrm.app/api/portal/auth server-side
  4. JWT token stored in PHP session (never sent to browser)
  5. All subsequent pages call OptiCRM API with the token
  6. PHP renders HTML using Tailwind CSS templates
  7. On logout, session is destroyed

The SDK (OptiCRM.php)

One file, zero dependencies. Available in every controller as $this->crm.

// Authentication
$result = $this->crm->login($email, $password);
$valid = $this->crm->verify();
 
// Student data
$fees = $this->crm->getFees($studentId);
$attendance = $this->crm->getAttendance($studentId);
$timetable = $this->crm->getTimetable($studentId);
$results = $this->crm->getResults($studentId);
$transport = $this->crm->getTransport($studentId);
$health = $this->crm->getHealth($studentId);
 
// Portal actions
$this->crm->submitGrievance(['title' => '...', 'description' => '...']);
$this->crm->changePassword($currentPassword, $newPassword);
$this->crm->updateProfile(['mobile' => '9876543210']);
 
// OptiQL queries
$result = $this->crm->query("SELECT firstName, lastName FROM Student LIMIT 10");
 
// Generic API calls
$data = $this->crm->get('/api/portal/any-endpoint', ['param' => 'value']);
$result = $this->crm->post('/api/portal/any-endpoint', ['key' => 'value']);

Customization Guide

Change the Look

Edit the view files in app/views/. They're plain PHP + HTML with Tailwind CSS.

  • layout.php — overall page structure (header, sidebar, content area)
  • partials/header.php — top navigation bar
  • partials/sidebar.php — left sidebar menu
  • Individual page files — fees.php, attendance.php, etc.

Replace <script src="https://cdn.tailwindcss.com"> with your own CSS framework (Bootstrap, custom CSS, etc.).

Add a New Page

Create a Controller

Create app/controllers/MyPageController.php:

<?php
class MyPageController extends Controller
{
    public function index(): void
    {
        // Fetch data from OptiCRM
        $result = $this->crm->query(
            "SELECT className, COUNT(id) as count FROM Student GROUP BY className"
        );
 
        $this->render('my-page', [
            'classes' => $result['data']['rows'] ?? [],
        ]);
    }
}

Create a View

Create app/views/my-page.php:

<h2 class="text-xl font-bold mb-4">Students by Class</h2>
<table class="w-full text-sm">
    <thead>
        <tr class="bg-gray-50">
            <th class="text-left px-4 py-2">Class</th>
            <th class="text-right px-4 py-2">Count</th>
        </tr>
    </thead>
    <tbody>
        <?php foreach ($classes as $c): ?>
            <tr class="border-b">
                <td class="px-4 py-2"><?= htmlspecialchars($c['className']) ?></td>
                <td class="px-4 py-2 text-right"><?= $c['count'] ?></td>
            </tr>
        <?php endforeach; ?>
    </tbody>
</table>

Register the Route

In public/index.php, add:

$router->get('/my-page', 'MyPageController', 'index');

Add to Sidebar

In app/views/partials/sidebar.php, add to the $navItems array:

['label' => 'My Page', 'href' => '/my-page', 'icon' => '📊'],

Create a Custom Form

Forms POST to a controller which calls the OptiCRM API:

// In the view (my-form.php):
<form method="POST" action="/my-form">
    <input type="text" name="title" placeholder="Title" required>
    <textarea name="description" placeholder="Description"></textarea>
    <button type="submit">Submit</button>
</form>
 
// In the controller:
public function submit(): void
{
    $result = $this->crm->post('/api/portal/grievances', [
        'title' => $_POST['title'],
        'description' => $_POST['description'],
    ]);
 
    if (isset($result['error'])) {
        $_SESSION['flash_error'] = $result['error'];
    } else {
        $_SESSION['flash_success'] = 'Submitted successfully!';
    }
 
    $this->redirect('/my-form');
}

Use OptiQL for Custom Reports

// Any controller can run OptiQL queries
$fees = $this->crm->query("
    SELECT feeHeadName, SUM(paidAmount) as collected, COUNT(id) as count
    FROM Invoice
    WHERE status IN ('PAID', 'PARTIAL')
    GROUP BY feeHeadName
    ORDER BY collected DESC
");
 
$overdue = $this->crm->query("
    SELECT invoiceNo, studentName, totalAmount, dueDate
    FROM Invoice
    WHERE status = 'OVERDUE' AND totalAmount > 1000000
    ORDER BY totalAmount DESC
    LIMIT 20
");

Deployment

Docker / Coolify

The repo includes a Dockerfile. In Coolify:

  1. Add new resource > connect to hemanthopti/opticrm-portal
  2. Build method: Dockerfile
  3. Port: 80
  4. Domain: portal.opticrm.app
  5. Environment variable: OPTICRM_DOMAIN=opticrm.app
  6. Deploy

Shared Hosting (cPanel)

  1. Upload files to your hosting
  2. Point document root to the public/ folder
  3. Create .env file with OPTICRM_DOMAIN=opticrm.app
  4. Ensure mod_rewrite is enabled (Apache)

Nginx

server {
    listen 80;
    server_name portal.yourschool.com;
    root /var/www/opticrm-portal/public;
    index index.php;
 
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }
 
    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }
}

Security

ConcernHow It's Handled
API token exposureToken stored in PHP server-side session, never sent to browser
XSSAll output uses htmlspecialchars()
CSRFSession-based (implement token in forms for production)
SQL injectionNo direct database access — all data via API
Password storagePasswords never stored locally — auth delegated to OptiCRM
HTTPSRequired in production for both portal and API

Integration with Other Apps

The Portal Auth API isn't limited to the PHP kit. Any application can use it:

  • Mobile apps — call the API from iOS/Android
  • OptiLearn LMS — authenticate students against OptiCRM
  • Custom dashboards — build React/Vue apps that consume the API
  • Chatbots — authenticate parents and fetch student data

The JWT token works with all 18 portal endpoints. See the complete endpoint reference in the table above.