How to Integrate FullCalendar in CodeIgniter 4: Build Your Events Calendar
Want to add a modern, interactive events calendar to your CodeIgniter 4 application? This comprehensive tutorial shows you exactly how to integrate FullCalendar with database-backed events using AJAX for instant create, update, delete, and drag-and-drop functionality — no page reloads required.
We'll walk through every step: setting up a fresh CodeIgniter 4 project, configuring the MySQL database, creating migrations and models, seeding sample data, building a controller for event handling, defining secure routes, and rendering the calendar in a Bootstrap-powered view with FullCalendar JavaScript. You'll learn to fetch events dynamically via JSON, handle user interactions securely (including CSRF protection), and display events with titles, dates, and colors.
Perfect for developers building appointment systems, event planners, task managers, or dashboards, this guide uses practical, copy-paste-ready code. While the original example uses FullCalendar v3, we'll include notes on upgrading to the latest v6+ for better performance and no-jQuery dependency. Follow along to have a fully working CodeIgniter 4 FullCalendar CRUD calendar in under an hour. Start transforming your app today!

Table Of Content
1 Prerequisites
2.) Composer
3.) Mysql
2 Introduction
This guide provides a complete, step-by-step example of building a dynamic events CRUD application in CodeIgniter 4 using FullCalendar — one of the most popular JavaScript libraries for interactive calendars. You'll learn how to set up a MySQL database, create migrations and models, seed sample data, build a controller for handling event operations, define secure routes, and render a responsive calendar view with AJAX-powered features like adding, editing, deleting, and drag-and-drop rescheduling of events — all without full page reloads.
By following this tutorial, you'll create a modern, user-friendly events calendar suitable for appointment booking systems, task schedulers, event management tools, or dashboards. The backend leverages CodeIgniter 4's powerful MVC architecture, while the frontend uses FullCalendar to deliver smooth interactions.
This hands-on project is beginner-to-intermediate friendly, includes full code snippets, and emphasizes best practices like CSRF protection for AJAX POSTs and proper input validation. Let's build your fully functional CodeIgniter 4 + FullCalendar events manager!
3 Create / Install a Codeigniter 4 Project
3.1 Install Codeigniter 4 Project
composer create-project codeigniter4/appstarter ci-4-fullcalendar-app
Then, navigate to your project directory:
cd ci-4-fullcalendar-app
3.2 Configure Environment and MySql Database
# CI_ENVIRONMENT = production
CI_ENVIRONMENT = development
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=ci4_fullcalender
DB_USERNAME=root
DB_PASSWORD=
4 Create Migration and Model
php spark make:migration CreateEventsTable
Edit the migration file to define the table structure:
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateEventsTable extends Migration
{
public function up()
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'constraint' => 5,
'unsigned' => true,
'auto_increment' => true,
],
'title' => [
'type' => 'VARCHAR',
'constraint' => '255',
],
'start' => [
'type' => 'DATETIME',
'null' => true,
],
'end' => [
'type' => 'DATETIME',
'null' => true,
],
'allDay' => [
'type' => 'BOOLEAN',
'default' => false,
],
'created_at' => [
'type' => 'DATETIME',
'null' => true,
],
'updated_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addKey('id', true);
$this->forge->createTable('events');
}
public function down()
{
$this->forge->dropTable('events');
}
}
Run the migration:
php spark migrate
Generate a migration for the events table and define its structure:
php spark make:model EventModel
Edit app/Models/EventModel.php to configure the model:
<?php
namespace App\Models;
use CodeIgniter\Model;
class EventModel extends Model
{
protected $table = 'events';
protected $allowedFields = ['title', 'start', 'end', 'allDay'];
protected $useTimestamps = true;
// Basic validation rules
protected $validationRules = [
'title' => 'required|min_length[3]',
'start' => 'required|valid_date',
];
protected $validationMessages = [
'title' => [
'required' => 'Event title is required.',
'min_length' => 'Title must be at least 3 characters.',
],
'start' => [
'required' => 'Start date/time is required.',
'valid_date' => 'Invalid start date format.',
],
];
}
5 Create Database Seeder
php spark make:seeder EventSeeder
Inside the EventSeeder.php file, use the Faker library to generate fake data:
<?php
namespace App\Database\Seeds;
use CodeIgniter\Database\Seeder;
use CodeIgniter\I18n\Time;
class EventSeeder extends Seeder
{
public function run()
{
$data = [
[
'title' => 'Team Meeting',
'start' => Time::now()->toDateTimeString(),
'end' => Time::now()->addHours(1)->toDateTimeString(),
'allDay' => false,
],
[
'title' => 'All Day Event',
'start' => Time::now()->addDays(1)->toDateString(),
'end' => null,
'allDay' => true,
],
[
'title' => 'Vacation',
'start' => Time::now()->addDays(2)->toDateString(),
'end' => Time::now()->addDays(5)->toDateString(),
'allDay' => true,
],
];
$this->db->table('events')->insertBatch($data);
}
}
Run this below command to insert the data.
php spark db:seed EventSeeder
6 Create New Controller - CalendarController
php spark make:controller CalendarController
In app/Controllers/CalendarController.php, define the index and ajax function:
<?php
namespace App\Controllers;
use App\Models\EventModel;
use CodeIgniter\API\ResponseTrait;
use CodeIgniter\HTTP\ResponseInterface;
class CalendarController extends BaseController
{
use ResponseTrait;
protected $eventModel;
public function __construct()
{
$this->eventModel = new EventModel();
}
public function index()
{
return view('calendar');
}
public function loadEvents()
{
$events = $this->eventModel->findAll();
$data = [];
foreach ($events as $event) {
$data[] = [
'id' => $event['id'],
'title' => $event['title'],
'start' => $event['start'],
'end' => $event['end'],
'allDay' => (bool) $event['allDay'],
];
}
return $this->respond($data);
}
public function createEvent()
{
$data = $this->request->getJSON(true);
if (!$this->validate($this->eventModel->validationRules, $this->eventModel->validationMessages)) {
return $this->failValidationErrors($this->validator->getErrors());
}
$insertData = [
'title' => $data['title'],
'start' => $data['start'],
'end' => $data['end'] ?? null,
'allDay' => $data['allDay'] ?? false,
];
$id = $this->eventModel->insert($insertData);
return $this->respondCreated(['id' => $id]);
}
public function updateEvent()
{
$data = $this->request->getJSON(true);
$id = $data['id'];
if (!$this->eventModel->find($id)) {
return $this->failNotFound('Event not found');
}
$updateData = [
'title' => $data['title'] ?? null,
'start' => $data['start'] ?? null,
'end' => $data['end'] ?? null,
'allDay' => $data['allDay'] ?? null,
];
$this->eventModel->update($id, array_filter($updateData));
return $this->respond(['success' => true]);
}
public function deleteEvent()
{
$data = $this->request->getJSON(true);
$id = $data['id'];
if (!$this->eventModel->find($id)) {
return $this->failNotFound('Event not found');
}
$this->eventModel->delete($id);
return $this->respondDeleted(['success' => true]);
}
}
?>
7 Create a View
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CodeIgniter 4 FullCalendar Demo</title>
<meta name="csrf-token" content="<?= csrf_hash() ?>">
<!-- FullCalendar CSS & JS (v6.1.15 via CDN) -->
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.15/index.global.min.js"></script>
<style>
#calendar {
max-width: 1100px;
margin: 40px auto;
}
</style>
</head>
<body>
<div id="calendar"></div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const calendarEl = document.getElementById('calendar');
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
const calendar = new FullCalendar.Calendar(calendarEl, {
initialView: 'dayGridMonth',
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'dayGridMonth,timeGridWeek,timeGridDay'
},
editable: true, // Drag, resize, drop
selectable: true, // Select dates to create events
events: '/calendar/load-events', // Fetch events
// Create new event
select: async function(info) {
const title = prompt('Enter event title:');
if (title) {
const response = await fetch('/calendar/create-event', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({
title: title,
start: info.startStr,
end: info.endStr,
allDay: info.allDay
})
});
if (response.ok) {
calendar.refetchEvents();
} else {
alert('Error creating event');
}
}
calendar.unselect();
},
// Update event (drag/resize)
eventResize: async function(info) {
await updateEvent(info.event);
},
eventDrop: async function(info) {
await updateEvent(info.event);
},
// Click to edit/delete
eventClick: async function(info) {
if (confirm('Delete this event?')) {
const response = await fetch('/calendar/delete-event', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({ id: info.event.id })
});
if (response.ok) {
info.event.remove();
}
} else {
const newTitle = prompt('Edit title:', info.event.title);
if (newTitle) {
await updateEvent({ ...info.event, title: newTitle });
}
}
}
});
calendar.render();
async function updateEvent(event) {
const response = await fetch('/calendar/update-event', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({
id: event.id,
title: event.title,
start: event.startStr,
end: event.endStr,
allDay: event.allDay
})
});
if (response.ok) {
calendar.refetchEvents();
} else {
alert('Error updating event');
}
}
});
</script>
</body>
</html>
8 Define a Route
use CodeIgniter\Router\RouteCollection;
/**
* @var RouteCollection $routes
*/
$routes->get('/calendar', 'CalendarController::index');
$routes->get('/calendar/load-events', 'CalendarController::loadEvents');
$routes->post('/calendar/create-event', 'CalendarController::createEvent');
$routes->post('/calendar/update-event', 'CalendarController::updateEvent');
$routes->post('/calendar/delete-event', 'CalendarController::deleteEvent');
9 Folder Structure
10 Run Web Server to Test the App
php spark serve
- Access at http://localhost:8080/calendar.
- Events load from DB via AJAX.
- Create: Select date range, enter title.
- Update: Drag/resize or click to edit title.
- Delete: Click and confirm.
- For production: Add authentication, more validation, error logging.
- If CSRF issues: Ensure app/Config/Filters.php has CSRF enabled (default in CI4).
- Extend: Add event colors, recurring events via FullCalendar plugins.
11 Conclusion
Written by Revathi M
PHP Developer & Technical Writer · 10+ years building web applications with CodeIgniter and Laravel
Revathi specializes in PHP backend development, authentication systems, and REST API design. She writes practical, production-tested tutorials at Get Sample Code to help developers build secure applications faster.
Frequently Asked Questions
You need PHP 8.2 or higher, Composer, a running MySQL server, and basic knowledge of CodeIgniter 4 migrations, models, and AJAX. The tutorial uses Bootstrap 5, jQuery, Moment.js, and Toastr for UI enhancements.
Add the required CDNs in your view's <head> section: <link href=\"https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.9.0/fullcalendar.min.css\" rel=\"stylesheet\" />, <script src=\"https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js\" />
Events are fetched via AJAX when FullCalendar requests a date range (start/end parameters). The controller queries the database using CodeIgniter's model and returns JSON data, which FullCalendar uses to render events.
Use FullCalendar callbacks: 'select' for adding (prompt title, POST type='add'), 'eventDrop' for updating (drag-and-drop, POST type='update'), and 'eventClick' for deleting (confirm, POST type='delete'). The controller handles each type with insert/update
The 'events' table has columns: id (BIGINT auto-increment), title (VARCHAR), start (DATE), end (DATE nullable), created_at and updated_at (TIMESTAMP). Create it via migration and seed fake data if needed.
Common issues: Incorrect routes, database connection errors in .env, missing event 'id' in AJAX payloads, or JavaScript console errors (e.g., libraries not loaded). Check browser console and ensure CSRF is disabled or handled for AJAX if enabled.
Yes, set 'editable: true' in FullCalendar options. The 'eventDrop' callback triggers an AJAX update with new start/end dates, and the controller updates the database record.
The tutorial uses Toastr. Include its CDN and call toastr.success('Event Created Successfully') (or similar) in AJAX success callbacks for add/update/delete.
This tutorial uses FullCalendar v3.9.0 (jQuery-based). Newer versions (v6+) have a different API without jQuery/Moment dependency. For modern projects, consider updating to v6, but this example works as-is with older setups.
The tutorial uses DATE fields (no time). To add times, change columns to DATETIME, update FullCalendar to handle times (displayEventTime: true), and adjust AJAX data formats.
