The Big Admin Revamp: Moving to a Codeigniter 4 and VueJS Stack
It was around two in the morning on a sluggish Tuesday when I finally admitted defeat with my old server setup. I manage a handful of totally unrelated web projects—everything from a local bakery’s order system to a niche entertainment portal where visitors basically just hang around to play HTML5 arcade games. The frontend of these sites worked fine. They loaded fast enough, the users weren't complaining, and the traffic was steady. But the backend? The central admin dashboard where I actually manage the users, tweak the site settings, and check the daily logs? It was a complete disaster zone. I had bolted together pieces of raw PHP, ancient jQuery plugins, and inline CSS over the span of about five years. It was doing my head in. Every time I needed to update a simple database row, I had to navigate through a sluggish, disorganized interface that looked like it belonged in 2008.
I realized I couldn't keep patching this spaghetti code. It was time for a complete teardown and rebuild. I wanted something modern, but I didn't want to learn an entirely new ecosystem from scratch just for a management panel. My server environments are mostly built around PHP, and I’ve been using Codeigniter since the 3.x days. On the frontend, I’ve been messing around with VueJS for interactive bits, finding it way less of a headache than React for my specific brain wiring. So, the logical decision was to stick to what I somewhat knew but upgrade the architecture: Codeigniter 4 for the backend API and VueJS for a reactive, single-page-application (SPA) feel on the admin side.
The Tipping Point: Why the Old System Had to Go
Before I get into the actual rebuild process, it helps to understand the mess I was running away from. The legacy dashboard was built on a really old MVC pattern where every single click resulted in a full page reload. If I wanted to ban a spam user on the arcade site or update the price of a croissant on the bakery site, I clicked a button, the screen went white for a second, and then the page re-rendered. It doesn't sound like a big deal, but when you are managing hundreds of micro-transactions or user logs a day, those seconds add up. It gets exhausting.
Worse than the speed was the code maintainability. I had no strict separation of concerns. The database queries were sometimes written directly into the view files because I was too lazy to route them through the controller properly during late-night bug fixes. The CSS was a massive 5,000-line file of overrides upon overrides. Mobile responsiveness was basically non-existent. If I tried to check the server logs on my phone while waiting in line for coffee, I had to pinch and zoom horizontally to read anything, and usually ended up fat-fingering the wrong button anyway.
I needed a clean break. I needed a structured layout, a sidebar that made sense, data tables that could sort instantly without hitting the server for a full reload, and a mobile view that didn't make me want to throw my phone into a river.
Defining the New Architecture
I sat down with a blank notepad and mapped out the new flow. I didn't want to build the UI from scratch. I’m a backend guy mostly; my CSS skills usually involve aggressively typing !important until the box moves where I want it. I knew I needed a pre-designed structure to save me a hundred hours of frontend fiddling.
The plan was simple in theory:
- Set up Codeigniter 4 to act purely as a RESTful API. No HTML views rendered by PHP. Just clean JSON endpoints.
- Use VueJS to handle all the routing, state management, and UI rendering on the client side.
- Find a solid HTML/CSS base that already had the layout, sidebars, and basic form inputs styled, so I could just drop my Vue components into it.
I spent a few days scouring the web for a starting point. Most admin layouts I found were strictly React, or they were tied to Laravel (which is great, but I didn't want to migrate my entire backend logic to Laravel just for the admin panel). Eventually, after a lot of digging through forums and template directories, I stumbled across the Pick - HTML, VueJS & Codeigniter 4 Responsive Admin Dashboard Template. It was exactly the weird, specific stack I was looking for. It wasn't just a flat HTML file; it actually had the CI4 and Vue wiring started. I grabbed it, extracted the zip file, and stared at the directory structure.
This is where the real work began.
Phase 1: Dissecting the Base and Ripping Out the Fluff
Whenever you start with a pre-built structure, the first few days are just you figuring out how the original author’s brain worked. You have to trace the routing, understand the build process, and, most importantly, delete the stuff you don't need.
I opened the project in VS Code. The folder structure was somewhat familiar. The CI4 app folder was sitting there, alongside a src folder for the Vue frontend. First things first, I spun up my local server using XAMPP (yeah, I still use XAMPP locally, don't judge). I pointed the virtual host to the public directory, ran the Node package manager to install the Vue dependencies, and fired up the dev server.
The default screen popped up. It looked sharp. Clean lines, a nice dark mode toggle, and a sidebar packed with about fifty different demo pages—charts, maps, crypto dashboards, e-commerce layouts.
My immediate task was aggressive pruning. I don't run a crypto exchange, so I didn't need real-time candlestick charts. I don't need three different types of map plugins. When adapting a new UI, if you leave all that demo bloat in the project, your final build size gets massive, and your Vue router file becomes a nightmare to navigate.
I spent an entire afternoon just deleting files. I stripped out the chart libraries from the package.json. I went into the Vue router configuration and deleted dozens of routes. I cleared out the dummy components. I stripped it back to the absolute bare bones: a login page, a blank dashboard page, a generic table view, and a user settings form.
This pruning process is crucial. It forces you to understand the component tree. By breaking the template a few times (and getting those lovely red Vue compilation errors in the terminal), I figured out exactly how the global state was managing the sidebar toggle and the dark mode theme. Once I had a skeletal, working version with zero errors, I felt ready to start hooking up my own data.
Phase 2: Wrestling with Codeigniter 4
Moving from Codeigniter 3 to Codeigniter 4 is basically like moving to a different framework. It’s entirely rewritten. Namespaces are everywhere, the routing is stricter, and the way it handles database models is much more rigid.
My old backend was heavily reliant on session variables stored on the server to check if I was logged in. Since the new architecture was going to be an API talking to a Vue frontend, traditional PHP sessions were going to be clunky, especially if I ever wanted to manage things from a mobile app later. I needed to switch to JWT (JSON Web Tokens).
Setting up the Auth API
I created a new controller in CI4: App\Controllers\Api\Auth.php. I wrote a simple login method that took an email and password from a POST request, checked it against my admin database table, and generated a JWT.
Sounds simple, right? Well, getting the CORS (Cross-Origin Resource Sharing) headers right took me about three hours of pulling my hair out. Because I was running the Vue dev server on localhost:8080 and the CI4 backend on a local .test domain, the browser kept blocking the API requests. I had to create a custom CI4 filter to attach the correct Access-Control-Allow-Origin headers to every incoming API request.
Once CORS was finally behaving, I could hit my login endpoint from Postman and get a token back. Victory.
Rebuilding the Models
Next, I had to migrate my database logic. In my old setup, I just wrote raw SQL queries in the controllers. "Select * from users where status = active." It was dirty.
CI4 has this really nice Entity system. I spent a solid week going through my old database tables and mapping them to CI4 Models and Entities. I created a UserModel, a GameScoreModel (for the arcade portal), and an OrderModel (for the local client sites).
I set up the API routes in Routes.php. I decided to use resource routing, which is a neat feature in CI4 that automatically maps standard HTTP verbs (GET, POST, PUT, DELETE) to controller methods (index, create, update, delete).
$routes->group('api/v1', ['filter' => 'jwt'], function($routes) {
$routes->resource('users', ['controller' => 'Api\UserController']);
$routes->resource('scores', ['controller' => 'Api\ScoreController']);
});
This single block of code replaced hundreds of lines of messy routing from my old dashboard. It felt incredibly satisfying. I now had a secure, token-protected API that could spit out clean JSON data for any of my web properties.
Phase 3: The VueJS Frontend Integration
With the backend APIs humming along locally, I shifted my focus back to the frontend. The base layout I was using had a really solid HTML structure. The CSS flexbox grids were tight, and the styling was consistent. But I had to make it actually do something.
State Management and Authentication
The first major hurdle on the frontend was handling that JWT token I mentioned earlier. When I log in via the Vue interface, the CI4 API returns the token. I needed to store this token and attach it to every subsequent request I made to the server.
I set up Axios (my preferred HTTP client) and created a global interceptor. Every time Axios sends a request, it checks the local storage for the token. If it’s there, it shoves it into the Authorization header.
axios.interceptors.request.use(config => {
const token = localStorage.getItem('admin_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
I also had to handle the reverse scenario. What if the token expires? If CI4 sends back a 401 Unauthorized status, the Axios response interceptor catches it, wipes the local storage, and kicks the Vue router back to the login screen. Getting this auth flow airtight took a bit of testing. There is nothing worse than an admin panel that silently fails when your session times out, leaving you clicking a "Save" button that does nothing.
Building the Data Tables
The heart of any admin dashboard is the data table. I needed a way to list out hundreds of users, sort them by registration date, filter them by status, and paginate them.
In the old days, I’d use DataTables.net with jQuery. It worked, but it felt heavy. Now, I was using Vue. I created a reusable DataTable.vue component.
Here is where the separation of frontend and backend really shined. In my Vue component, I just define the columns I want. When the component mounts, it hits the CI4 endpoint: GET /api/v1/users?page=1&sort=created_at.
CI4 grabs the data, handles the pagination logic natively (which is super fast), and returns a JSON object containing the user array and the pagination metadata (total pages, current page). Vue takes that JSON, loops through it using v-for, and renders the table instantly.
No page reloads. No white screens. I click "Page 2", and the rows just swap out in milliseconds. The first time I got this working with my actual live database data locally, I leaned back in my chair and actually smiled. It was so fast.
The Mobile Responsiveness Reality Check
I mentioned earlier that my old dashboard was useless on a phone. The layout I was adapting claimed to be fully responsive. And mechanically, it was. The CSS media queries correctly collapsed the sidebar into a hamburger menu, and the header shrank down nicely.
However, "responsive HTML" and "usable mobile UX" are two different beasts.
The problem was my data tables. If I have a user table with columns for ID, Name, Email, Registration Date, Status, and Action Buttons, that looks great on a 27-inch monitor. On a 6-inch iPhone screen, the HTML/CSS tries to squeeze all six columns in, resulting in text that wraps awkwardly, making the table about a mile long vertically and impossible to read.
I had to get creative. I didn't want to just hide columns with CSS display: none on mobile, because the browser is still rendering that data in the DOM. Instead, I used Vue’s reactivity.
I set up a listener for the window width. If the width drops below 768px, Vue dynamically changes the array of columns passed to the table component. On mobile, it only renders the "Name", "Status", and an "Expand" button. If I need to see the email or registration date on my phone, I click "Expand", and it drops down a little card layout below the row.
This took a few days to get right. It required diving deep into the layout's grid system and overriding a few of the default CSS variables to ensure the spacing didn't look totally borked when the row expanded. But it was worth the effort. Now, if the arcade site goes down while I'm at the pub, I can actually pull out my phone, navigate the dashboard comfortably, and restart the server instance without squinting.
Phase 4: Form Handling and Validation Nightmares
Displaying data is the easy part. Mutating data is where dashboards get messy.
Let's talk about editing a user profile. I needed a form with text inputs, dropdowns for user roles, and a toggle switch for account suspension.
The HTML template provided beautiful UI components for these. The form inputs had nice focus animations, and the toggle switches looked modern. But tying these visual elements to Vue’s v-model and ensuring the data synced correctly with the CI4 backend was tricky.
The biggest headache was validation. If I try to save a user, but the email address is already taken, the backend needs to reject it.
In my old setup, the PHP controller would catch the error, reload the entire page, and inject a red text string above the form.
Now, the flow is entirely asynchronous.
- I hit "Save".
- Vue sends a PUT request via Axios.
- CI4 runs its native validation library. It sees the email is a duplicate.
- CI4 returns a
422 Unprocessable EntityHTTP status, along with a JSON payload containing the specific field errors. - Vue catches the 422 error. It parses the JSON, and dynamically adds a red border and an error message strictly under the email input field, without touching the rest of the form.
Getting the CI4 validation errors to map perfectly to the Vue UI state required writing a bit of utility code. I created an Errors class in JavaScript that holds the error state. Whenever an Axios request fails with a 422, it populates this class. The Vue template checks errors.has('email') and conditionally renders the warning text.
It felt like a lot of initial boilerplate to write just to handle a bad email address, but once I had this system in place, I could drop it into every single form on the dashboard. Adding a new settings page went from taking a whole day to taking about forty-five minutes.
Phase 5: The UI Polish and Dark Mode
I’m not a designer, but I know what looks bad. After about three weeks of building out the core functionality—managing users, viewing logs, updating site configurations—the dashboard worked flawlessly. But it still looked a bit like a generic template.
I spent a weekend just messing around with the CSS. The layout used SCSS, which made tweaking the global color scheme pretty painless. I swapped out the primary brand colors to match my own weird branding. I adjusted the border radii on the cards because they felt a bit too rounded and soft for my liking; I prefer sharper, more utilitarian edges for backend tools.
Then there was the dark mode. The base code had a built-in dark mode toggle, which was one of the reasons I chose it. When you are staring at server logs at 11 PM, a white screen is like staring into the sun.
However, integrating dark mode with custom data visualizations was a bit dodgy. I had implemented a few simple bar charts to show daily visitors across my different sites. When I toggled dark mode, the background turned a nice charcoal gray, but the chart axes and grid lines stayed black, making them completely invisible.
I had to hook into the Vue state that tracked the current theme. When the theme state changed from 'light' to 'dark', I set up a watcher in my chart component that dynamically updated the chart library’s configuration object, changing the grid line colors to a soft gray and redrawing the canvas. It’s these tiny, highly specific integration details that pre-made templates can't do for you automatically. You always have to get your hands dirty in the logic.
The Deployment Reality
Eventually, the local build was complete. The Codeigniter 4 API was secure and fast. The VueJS frontend was snappy, responsive, and stripped of all the bloat.
It was time to push it to the live server.
Deploying an API/SPA split architecture is slightly more complex than just FTP-ing a bunch of PHP files.
For the backend, I uploaded the CI4 files to a secure directory above the public web root. I routed an api.mydomain.com subdomain to the CI4 public folder. I updated the .env file with the production database credentials and set the environment to production so it wouldn't leak debug backtraces if an error occurred.
For the frontend, I ran npm run build locally. Vue took all my beautifully separated components, stripped out the development warnings, minified the CSS and Javascript, and spat out a single, highly optimized dist folder. It was incredibly small—just a few hundred kilobytes of JavaScript.
I uploaded these static HTML/JS files to the main admin directory on my server. I configured my Nginx server block so that any route hit on the frontend would fall back to the index.html file (a requirement for Vue Router's history mode to work properly without throwing 404 errors when you refresh the page).
The "Oh Crap" Moments Post-Launch
You never really know how a system works until you use it in anger. The first week of using the new dashboard, I ran into a few minor but annoying snags.
The Caching Issue: I pushed an update to the Vue frontend to fix a typo in the sidebar. I refreshed my browser, but the typo was still there. The browser had aggressively cached the minified JavaScript file. I had to go back into the Vue build configuration and ensure it was appending unique hash strings to the filenames on every build (e.g., app.b4c3d2.js). This forces the browser to download the new file when I deploy an update.
The Session Timeout Ghost: I was writing a long response to a customer support ticket on the bakery site from within the new dashboard. I took a phone call halfway through. When I came back and hit send, the API rejected it because my JWT token had expired during the call. But the Vue UI didn't handle that specific rejection gracefully in that one form, so my text just vanished into the void. I lost twenty minutes of typing. I immediately went back to the code and updated the Axios interceptor to catch timeouts on every single endpoint, not just the page load endpoints, and save the form state locally before kicking me out.
Server Load Surprise: This was actually a good surprise. I checked my digital ocean droplet analytics after a few days. The CPU load had noticeably dropped. Because the CI4 backend was no longer rendering massive HTML views on every click, and was instead just parsing small JSON strings, the server was doing a fraction of the work. The heavy lifting of rendering the UI had been entirely offloaded to my browser. It was a massive efficiency gain that I hadn't fully anticipated.
Final Thoughts on the Rebuild
Looking back at the whole process, rewriting an entire admin infrastructure is not something you do on a whim. It took me the better part of a month, working evenings and weekends, to get the CI4 and Vue stack to talk to each other perfectly, format the HTML structures to my liking, and migrate all the old logic.
But sitting here now, using the new dashboard, the difference is night and day. There is a sense of calm that comes from having a clean, organized workspace. When I click a user profile, it slides in instantly. When I search the logs, the results filter as I type. The whole thing feels solid.



