OpenCart · WooCommerce · Laravel — Ukraine

I fix online storesother people built.

Slow ones, broken ones, hacked ones — and whatever has been bolted on over the years. You describe the symptom. I find the cause and show you the numbers.

Measured first, then fixed

Category page, full responsebusiest page · 19 306 visits that month
before2,205 ms
after292 ms
−87%
A shop of 24 579 products whose category pages took over two seconds. One missing database index, plus three calculations being run for the whole catalogue and then thrown away. Found by reading the query plan, not by guessing.
What people come with

Four things you can hand me

Your store got slow

Pages take seconds to open. Checkout drags. The speed report in Google has gone red.

I measure before changing anything — query plans, access logs, real page timings — so the work goes into whatever is actually costing the seconds, not whatever looks suspicious. You get the before and after figures, not an assurance that it feels faster now.

Your store got hacked

Strange pages in Google results, an email from your host, or visitors being sent somewhere else.

Cleaning the files is the easy half. The job is finding how they got in and closing that — a cleanup without it lasts until they come back the same way, usually within the week. You get the entry point named, not just the mess removed.

Your store needs to talk to something

Marketplace listings, a CRM, prices and stock that somebody is still updating by hand.

Feeds for Rozetka and Prom, CRM sync, imports that run on a schedule. Built so the second run updates your catalogue instead of quietly duplicating it — which is how most imports eventually fail.

Your store needs something that does not exist yet

A feature nobody sells as a ready module, a tool for your staff, an internal system.

Custom modules for OpenCart and WooCommerce, or a separate application when the job has outgrown the shop. Built to a written spec you approve before I start, so nobody is guessing halfway through.

Case files

Four jobs, with the numbers

Client names withheld. Every figure was measured on the live system, before and after.

01Sports and chess retailer · OpenCart 3 · 24 579 products

One request in seven was failing. The cause was a table nobody had opened.

  • OpenCart 3
  • MySQL
  • One day

Symptom

Roughly 15% of requests returned HTTP 500 — 825 of them in a ninety-minute sample. The daily count was doubling: 17 fatal errors on 4 August, 413 on the 18th, 915 on the 21st. The hosting company pointed at the connection limit and stopped there.

Measured

Every fatal error was the same line: the database connection inside the session handler. The host confirmed the account was hitting its cap of 30 concurrent connections.

The session table held 18 715 802 rows and 2 284 MB — 89% of the entire database. Sessions from 2023 were still in it.

Every request, including every bot, writes to that table. Against 2.3 GB each write holds its connection long enough for thirty of them to overlap. The server itself was healthy: 40 cores at 40% load, 67 GB RAM free. The neighbours were not the problem.

Cause

OpenCart 3 compares an integer against a datetime column in its session garbage collector, so the cleanup query matched nothing — not once, in three years. Sessions simply accumulated until the connection cap did the rest.

Result

Database, total size
before2,562 MB
after301 MB
−88%
Session table
before2,284 MB
after22 MB
−99%
Busiest category page
before2,205 ms
after292 ms
−87%
Home page
before370 ms
after116 ms
−69%
HTTP 500s per 90 minutes
before825
after0
−100%

22 839 142 rows deleted in 17 minutes, in batches of 5 000 with a pause between them, while the shop stayed open. The server load average went down during the cleanup, not up.

02Wall art and photo wallpaper retailer · three sites, one infected

Only Googlebot saw the casino page.

  • Incident response
  • PHP object injection
  • SEO

Symptom

Nothing looked wrong. Visitors got the real shop. So did curl, PageSpeed, Ahrefs, the uptime monitor and every scanner the owner tried. Meanwhile the homepage was being reindexed as a Turkish gambling page, carrying a canonical tag that pointed back at the client’s own domain.

Measured

The cloak shipped a decoy list of user agents — curl, wget, python, ahrefsbot, semrushbot, lighthouse, pagespeed, gtmetrix, uptimerobot, gptbot — and switched itself off for every one of them. That is why no scan had ever found it.

index.php reported a modification time of August 2025 and a change time of 1 September 2026. The attacker had reset mtime with touch; ctime cannot be forged without root. That one mismatch is what exposed the file — searching by modification date would have missed it entirely.

Two independent footholds, not one: an injected block in index.php, and a .user.ini setting auto_prepend_file, which PHP loads before every request in the directory no matter what index.php says.

Cause

The access log named it exactly. A theme controller ran unserialize(base64_decode($_POST[…])) — PHP object injection, in a file dated 2022 that had never been patched. A scanner walked through known-vulnerable endpoints of other OpenCart themes, collected a 404 from each, hit this one, and had a web shell 24 seconds after its first request. The same line existed in five places; one of them was overridden by an OCMOD build, so patching the original file alone would have left the busiest endpoint wide open.

Result

Page Googlebot receivedgambling page → the real shop
before38,367 B
after232,782 B
Mentions of the casino brand
before1
after0
−100%
Unpatched injection points
before5
after0
−100%

The cloak went off in one move — renaming the lock file both copies check — which is instant, reversible, and touches nothing the attacker was watching. Then all five injection points were closed and verified from outside: an object payload now returns 200 with an empty result and no fatal error, while the live “load more” still returns its 31 products. Four shells, the .user.ini and the injected block removed; the cleaned index.php came out byte-identical to the clean reference, confirmed two independent ways.

03Electronics retailer · 48 GB hosting quota

19.8 GB freed — and the cause removed, so it stays freed.

  • OpenCart
  • Images
  • Two sessions

Symptom

The account had grown to 35 GB of a 48 GB quota and was still climbing. The obvious move was to buy a bigger plan.

Measured

The product originals were not what was growing: 18 to 19 GB over four days. The thumbnail cache went from 8.8 to 16 GB in the same window.

OpenCart’s stock image library writes JPEG thumbnails at quality 90 and applies no compression to PNG at all. So the cache was not a symptom of too many products — it was the engine, producing oversized files for every new size it was asked for.

Cause

Compressing what was already there would have bought a few weeks. The engine had to stop producing it, which is a small patch to the image library plus one pass over the existing files.

Result

Product originals
before30,264 MB
after18,281 MB
−40%
Thumbnail cache
before16,384 MB
after8,397 MB
−49%
Account totalquota 48 GB
before35 GB
after28 GB
−20%

74 649 originals and roughly 83 000 thumbnails processed — quality-capped, never-grow, formats and database untouched. 794 files spot-checked for integrity afterwards, zero corrupted. Because the engine was patched, new thumbnails come out compressed and the figure holds instead of drifting back.

04Marketing agency · internal CRM · Laravel + Inertia + React

“Nowhere near the prototype” turned out to start with two lines of CSS.

  • Laravel
  • React
  • Inertia

Symptom

The client’s words were that the live CRM was far from the prototype, on every page and every block. The expectation in the room was a rebuild.

Measured

The components were right. The stylesheet order was not: the design kit loaded before Tailwind, so preflight was resetting every heading, and a single body class was overriding the kit’s typeface. Two structural fixes put most screens back where they belonged and reduced the real gap to scope.

Cause

From there it was work, not archaeology: 21 screens, 12 report tabs, 8 finance sections. Everything the prototype had faked in browser storage moved into real tables — proposals, client credentials with encrypted secrets, a company activity log, contracts, a call journal. Plus the parts a prototype never has to answer for: a period picker that drives flows but deliberately leaves point-in-time figures alone, because “overdue invoices as of April” is not something anyone can act on today.

Result

Screens brought to the prototype
before0
after21
Report tabs
before0
after12
Finance sections
before0
after8
My own

Things I run, not just ship

  • MeoniQ

    NestJS · PostgreSQL · React · Tauri · Android

    A personal knowledge and media application. Ships as a web app, a desktop build and an Android app from one release pipeline, with migrations, a health endpoint that reports the live commit, and everything else you only build once you have to operate the thing yourself.

  • nonogramhub.com

    Node · nginx · Umami

    A puzzle site I own end to end: generation, content, self-hosted analytics, nginx, deployment. A useful place to be my own client and find out what is annoying about my own decisions.

  • Universal Import

    PHP 8 · OpenCart 3 · MySQL

    An import module for OpenCart 3, built to a written specification: nine tables, a profile wizard, dry runs, field mapping, value mapping and snapshots. Written because every off-the-shelf importer duplicates hand-added products on the second run.

Platforms

What I work on

  • OpenCart 3

    Most of my work: the shop, the database under it, and the pile of third-party modules on top that nobody has opened in years.

  • WooCommerce and WordPress

    Shops on WordPress, including the ones where a plugin is doing something nobody can explain any more.

  • Laravel and React

    For when the job has outgrown a shop: CRMs, tools for your staff, internal systems people use all day.

  • Your server

    Linux, nginx, Apache, backups, scheduled jobs, certificates. I can work on your hosting without needing a third person in the conversation.

For the technical reader

PHP 8 · MySQL / MariaDB · PostgreSQL · query plans and indexing · OCMOD · XML / YML feeds · SalesDrive · Node · NestJS · TypeScript · Inertia · Astro · Tauri · pm2 · cron · GitHub Actions · access-log forensics · ImageMagick, jpegoptim, pngquant

Who you would be working with

I like the half day before the fix.

Most of my work arrives already broken and already someone else’s. A shop set up five years ago, extended with thirty modules from four vendors, returning 500s at lunchtime on a Tuesday. The code I end up writing is rarely the interesting part — it is usually small. The interesting part is the half day in front of it, spent proving which of the thirty suspects is the actual one.

That is also why every case above carries numbers on both sides. A fix you cannot measure is a fix you are asking someone to take on faith, and on a system you did not build, faith is expensive.

Based in Ukraine, working with clients here and abroad. Available for one-off diagnostics, ongoing maintenance, and builds.

Contact

Tell me what is broken.

A description of the symptom is enough to start. If you can say when it began and what changed around then, better still — that is usually half the diagnosis.

I use what you send here to reply to you, and nothing else.