+44 (0) 7512 808928 [email protected]
A Better Bookstore

A Better Bookstore

A direct-to-reader bookstore, built on our own platform rather than a hosted cart

This is a custom digital bookstore that pays authors 75-80% on direct sales, which only works if the storefront, the fulfilment and the royalty accounting are all parts of the same system. It runs on the custom Symfony platform rather than a third-party store product, so the catalogue, checkout, customer accounts, author payouts and delivery share one data model instead of being stitched together through webhooks.

Two further pieces shape the build. Merchandising is editorial rather than algorithmic curated collections managed by people, with no sponsored placement and no recommender in the loop, which is a deliberate architectural choice as much as a positioning one. And alongside direct sales, each title carries managed affiliate links out to Amazon, Apple Books, Google Books, Kobo, Booktopia, Libro.fm and Fishpond, so the store can serve readers who want to buy elsewhere without losing attribution. Payments run through card and PayPal, and author-reader contact sharing is gated by a consent flag the reader controls from their own dashboard.

bookstore script
Bespoke Legacy Symfony App

Bespoke Legacy Symfony App

Nine years, 20 modules, one codebase

I’ve been developing this legacy Symfony app since 2017. In that time I’ve built 20 of the platform’s 23 modules myself, and more than 50 businesses have run on the platform.

That’s the part worth reading twice. Not a project I passed through – a system I’ve owned through nine years of feature work, on a stack that was already ageing when I started.

The legacy Symfony app

This platform is an all-in-one business platform for entrepreneurs and course creators – the kind of system that would otherwise be assembled from a dozen separate subscriptions. Twenty-three modules ship with it today:

Commerce & fulfilment Integrated E-Commerce & Shipping · Advanced Shipping · Bookstore · Affiliate Program · Subscribers

Learning & media Integrated E-Learning · On-Demand Shows · Podcasts · Blogs

Marketing & sales Marketing · Call-to-Action Buttons · Analytics · Contact Management · Surveys & Quizzes

Customer operations Customer Support Tickets · My Zone (customer self-service) · Knowledge Base · Social Community

Business operations Project & Time Billing Manager · Tasks · Website Manager

Each one is a product in its own right. Shipping a checkout is a project. Shipping a checkout, an LMS, a podcast host, a ticketing desk, an affiliate engine and a time-billing system that all share one customer record, one permissions model and one database is a different order of problem – and every new module has to work with the twenty-two that came before it.

The hard part: a codebase older than its own dependencies

The platform runs on PHP 7.2 and Symfony 2.8. Both are long past end-of-life:

ComponentSecurity support ended
Symfony 2.8 LTSNovember 2019
PHP 7.230 November 2020

That single fact shapes every piece of work on the system:

  • Modern libraries won’t install. Current SDKs for payment providers, AI APIs and cloud services require PHP 7.4 or 8.x. On 7.2 you can’t composer require your way out – integrations get built against the raw HTTP API, by hand, with the error handling and retry logic written from scratch.
  • The language is missing its modern safety rails. No typed properties, no union types, no enums, no match, no named arguments, no constructor property promotion. Correctness that a modern codebase gets from the type system has to come from discipline and review instead.
  • Framework patterns are two generations old. Symfony 2.8’s service container, form component, security layer and Twig 1.x templates work differently from anything documented in the last decade. Every Stack Overflow answer and every AI coding assistant will confidently suggest the Symfony 5/6 way of doing it – which doesn’t exist here.
  • It can’t just be shut down for a rebuild. The platform has live tenants running real businesses on it. Their courses, orders, subscriptions and support queues don’t pause for a migration.

How I work on it

I treat the legacy stack as a constraint to engineer around, not an excuse.

  • New features ship into the existing system – no big-bang rewrite, no feature freeze, no “we’ll get to your request after the migration.” Twenty modules went in this way.
  • Modern capability, old runtime. Where the platform needs to talk to services that assume a modern PHP, I build the bridge myself rather than telling the client it’s impossible.
  • Changes are isolated. In a system where 23 modules share one data model, the risk isn’t writing the feature – it’s what the feature touches. Scoping the blast radius is most of the job.
  • Continuity is the real deliverable. On a codebase this old, knowing why a strange decision was made in 2018 is worth more than any framework certification. Nine years in, that context lives in one head, and the client doesn’t have to pay for it to be rediscovered every time a contractor rotates off.

Most developers quietly hope a prospect doesn’t mention their old codebase. If you’re reading this because you have one – a system that earns real money and that nobody wants to touch – that’s the work I actively want.

I’ll tell you honestly whether the right move is to modernise in place, strangle it out module by module, or rebuild, and what each option actually costs, before you commit to one.

Tell us about your project
Custom Podcasting Platform

Custom Podcasting Platform

A multi-tenant podcast platform

We built a hosted podcast platform that takes a show from signup to live in a single system: media hosting, a public show page, feed generation, distribution to the directories, subscriber email and guest booking. It is multi-tenant SaaS, so every show runs on its own domain or a shared one, with storage and email allowances metered per plan and enforced at the point of upload and send rather than reconciled after the fact.

The technically unforgiving part is the feed. A podcast is not consumed through the application at all – Apple, Spotify and every podcast client read a generated RSS document, and once that document is in their index it becomes a contract you cannot safely break. Episode GUIDs have to stay stable for the life of the show or listeners get a backlog re-delivered as new episodes; enclosure URLs have to keep resolving, with byte-range support so players can seek and resume; and the feed has to satisfy several directories’ validation rules at once, from the iTunes namespace tags through to artwork dimensions. The platform generates that feed and guarantees the show owner keeps it, which means the hosting can be left without the audience being lost.

Audio delivery drives the rest of the infrastructure. Storage is allocated as a lifetime quota per plan, so files have to be accounted against a tenant at upload; and because podcast clients never call home, listener statistics can only be derived server-side from requests against the enclosure URLs. Everything the analytics dashboard shows is reconstructed from that traffic rather than from an SDK reporting back.

Around the feed sit the two workflows that make the product more than a host. Email runs per tenant with the show owner’s own SMTP credentials, so sending reputation and deliverability stay with the show rather than pooling across the platform, and new episode alerts fire to the subscriber list off the publish event with monthly send volume metered per plan. Guest booking is a full intake pipeline rather than a contact form: applications arrive through a public form, move through states in a Guest Hub, and connect to the episodes they end up on. Custom domain support, per-tenant mail configuration and plan-level quota enforcement are what make all of this hold up with many shows running side by side on one codebase.

Amazon Book Tracking & Category Toolkit

Amazon Book Tracking & Category Toolkit

A Symfony app that tracks Amazon’s category tree on an hourly cadence

On Amazon, a book’s visibility depends as much on which categories it sits in as on how many copies it sells, and none of the data needed to make that choice well is available in one place. The official Product Advertising API gives canonical product metadata but not the live competitive picture inside a category, and the category tree itself runs to thousands of nodes that are reorganised, renamed and added to without notice. We built a Symfony application that closes that gap by running two data sources side by side: the official API for authoritative book data, and our own in-house scraper for everything the API doesn’t return.

The scraper walks the full category hierarchy and the bestseller lists inside it, storing the tree rather than a flat list so that parent and child relationships, depth and node changes are all preserved between runs. Every pass is diffed against the last, which is what makes new, renamed and disappearing categories visible instead of silently corrupting the dataset. The same pipeline captures the top-selling titles in each tracked category and the author’s own book alongside them, so rank is always recorded in competitive context rather than as a bare number.

Running that hourly across every tracked book and category is the part that shapes the architecture. It is a scheduled, queued workload with retry and partial-failure handling rather than a single cron job: requests have to be paced, responses validated against markup that changes underneath you, and a failed fetch for one node must not take out the rest of the run. Rank and category data is stored as time series, so the history is queryable rather than overwritten, and it is that history the toolkit reads from when it ranks candidate categories by how competitive they actually are.

The system also watches for Best Seller and Hot New Release badges and renders hourly screenshots of every tracked category page and the book’s own listing. Badge detection is treated as a state transition, not a snapshot, so an achievement that appears at 2am and is gone by morning is still recorded, with a timestamped image to evidence it. For the author that means category decisions made on real data and a permanent archive of every ranking they have ever held, captured without anyone needing to be watching.

Health Care Portal

Health Care Portal

A practising cardiologist, bestselling author and international speaker with a growing online business and no single place to run it. Sales funnels sat on one platform, the podcast on another, the blog on a third, and the books were scattered across several more. Everything worked in isolation – but every new subscriber, product and media appearance added admin instead of momentum. The bigger the business got, the more of the owner’s week it consumed.

We migrated years of existing content into one integrated system and built out the pieces that didn’t exist yet: a membership site with an online course and member-only content, the podcast and blog with subscriber capture feeding a single contact list, book sales pages handling physical books, ebooks, audiobooks and a TV show DVD, automated worldwide fulfillment, a centralised support desk, a free-and-paid knowledge base, and a media centre tracking every appearance. Two pieces went further than a standard build – a virtual heart-risk survey that serves different outcome pages depending on the respondent’s risk score, and a printed course journal replaced with interactive survey questions built directly into the lessons, so the exercise now happens in the course and the answers are stored against the member’s account.

The result is a Health Care Portal: one login, one customer record and one place to publish, in place of a stack of disconnected subscriptions. The owner’s time moved off platform admin and onto the three things that actually grow the business: creating content, driving traffic and serving the members already there.

Health Care Portal