Why we rewrote Ticket Foundry from Python to PHP in a weekend
I started building Ticket Foundry in late April. Two days of building, hit a wall, and rewrote the entire thing in PHP over a weekend.
This is the story of that wall.
Day 1-2: Flask, SQLite, everything is great
I'm most comfortable in Python, so the MVP got built in Flask. SQLite for the database because I just wanted something working. Two days in:
- Login / logout
- Ticket CRUD
- A basic kanban board with drag-and-drop
- Comments and notes on tickets
- File attachments
- Projects module
- A "Tech" model for assignees
- Status workflow (New → In Progress → Waiting → Done)
Everything was running fine on my laptop. I'd called it TicketTrack at this point. The name was bad. Everything else was working.
I felt great.
Day 3: Try to deploy
The goal from day one was that this had to run on cheap shared hosting. Not a VPS, not Heroku, not a Docker container someone has to maintain. Shared hosting. The kind a 12-person IT team can buy for $10/mo and forget about.
I have a DreamHost account I've had for years. I figured: shared hosting, supports Python, this should be easy.
It is not easy.
The actual problem
DreamHost shared hosting supports CGI Python, not WSGI Python. The difference matters more than it sounds.
CGI launches a fresh Python interpreter for every HTTP request. New process, new memory, new everything. It is unbearably slow for anything that does database work. A page load that takes 50ms in dev takes 2-3 seconds via CGI because every request is paying the startup cost of the entire Python runtime.
WSGI keeps Python in memory between requests like a normal modern app server. This is how anyone actually runs Flask in production. Gunicorn, uWSGI, mod_wsgi — these all do basically the same thing.
DreamHost does not support any of them on their shared plans.
Options at this point:
- Move to a VPS. Costs more, requires maintenance, defeats the "your existing IT team can just install this" promise.
- Use a Python-friendly host like PythonAnywhere. Locks customers into a specific hosting provider, which I'm philosophically against.
- Rewrite in PHP. Native language of shared hosting since the 90s. Runs everywhere.
Option 3 it was.
The rewrite
Saturday morning I opened a new folder and started over. By Sunday night I had:
- Full PHP/MySQL rewrite with the same feature set
- Setup wizard for first-run config
- Sessions and bcrypt password hashing
- The kanban board working with the same drag-drop UX
- Email notifications via SMTP
- A "this works on DreamHost" deployment
48 hours, give or take. I didn't sleep great.
What I expected to be hard, but wasn't
Templates. I was worried about losing Jinja2. Turns out plain PHP with a sane convention (one file per "view," variables passed via include scope) is fine for this scale. The hour I spent picking a templating library would have been wasted; I just write <?= h($post['title']) ?> and move on.
Database layer. PDO with prepared statements is fine. Not as nice as SQLAlchemy, but the queries I need aren't complicated.
Sessions. PHP literally has session_start() built in. I had spent half a day setting up Flask-Login. Not Flask's fault, but the contrast was funny.
File uploads. $_FILES. Done. No middleware, no library, just a global variable. PHP gets a lot of grief for this style of API but for shared-host CRUD apps it is actually correct.
What I expected to be easy, but wasn't
Modern syntax. I'm used to f-strings, list comprehensions, match statements, type hints that actually work. PHP has most of this now (8.1+ is genuinely fine), but the muscle memory was off for the first few hours. I kept writing for x in items and then having to back up.
The standard library. Python's stdlib is enormous and pleasant. PHP's is huge and inconsistent. Function names are random — str_replace (no underscore) vs strpos, mb_substr vs substr, array_map vs array_filter argument order. You learn the gotchas eventually but it's annoying for a while.
Error handling philosophy. PHP loves to return false from functions that fail. Python raises an exception. Switching back to "did this function return false or just an empty string?" was a tax I'm still paying.
What I'd do again
Honestly? Pick PHP first if I were starting over. Not because PHP is "better" than Python — they're different tools — but because the goal of this project is to run on the boring host that the user already has. PHP wins that constraint by a mile.
There's a class of software where you start with the perfect language and language ecosystem, and there's a class where you start with the language your users can run. Ticket Foundry is the second kind. I should have known that from the beginning.
What's in the codebase now
100% PHP/MySQL. No Composer (intentional, wanted zero install steps beyond uploading files). Single-file Parsedown for the blog. PHPMailer for email. Quill (JS) for rich text. That's the entire dependency list.
The whole app is around 25k lines of PHP and JS, including the markup. Boots in <100ms on a $10 shared host.
I'm never building anything serious in Flask again unless I know I control the deployment environment. And I'll be honest about that constraint a lot earlier next time.