
Rebuilding a Live Literary Platform on Laravel and Vue
How we rewrote a live manuscript submission platform from legacy PHP into a type-safe Laravel 12 and Vue 3.5 system, then cut over without losing a customer.
Writers send their work into the world hoping it lands in the right journal. Behind that simple act sits a surprising amount of machinery: manuscripts to track, journals to match, editorial scores to compute, submissions to file, and payments to collect. Submitit runs that machinery as a managed service, and for years it ran on an aging codebase that had become expensive to change.
We rebuilt it in about three months. The new platform is a Laravel 12 API paired with a Vue 3.5 single-page application, every record carried over from the legacy system, and it went live at Submitit Platform while the old application kept serving paying customers right up to the switchover. This is how we did it, and why we made the architectural calls we made.
Key Challenges
A rewrite is only as strong as its grasp of what makes the existing system hard. Four problems defined this one, and each shaped how we could solve the others.
A codebase that resisted change
The original Submitit worked, and that is the trap with legacy software: it runs the business well enough that the cost of its limitations is easy to ignore until it compounds. Years of organic growth had fused business rules, data access, and presentation into a codebase where small changes carried large risk. Adding a feature meant touching code nobody wanted to touch, and onboarding a developer meant absorbing knowledge that lived in people rather than in structure.
Data that could not be lost
The platform held real customer accounts, manuscripts mid-submission, a curated catalog of literary journals, editorial scores accumulated over years, and live Stripe billing relationships. Every one of those records had to cross into the new system intact. There was no acceptable margin for data that arrived scrambled, orphaned, or silently dropped.
A platform that could never go dark
This was not a prototype sitting in a drawer. It was live, with paying customers depending on it every day, so the rewrite could not ask them to wait. The old application had to keep running until the exact moment the new one was ready to replace it, which ruled out any plan that needed a long maintenance window.
Rules that lived only in code
Submitit's real value, the journal-matching and scoring logic, lived inside the implementation with no specification beside it. There was nothing to check rebuilt behavior against except the running system and the people who knew it. Getting those rules even slightly wrong would change which journals a writer's work reaches, so they had to be preserved exactly, not approximately.
One thing was deliberately out of scope. The Submitit marketing site, a Wix build, would stay exactly as it was. Everything behind the login was what we set out to replace.
Taking Over Another Team's Work
Those undocumented rules are the sharp edge of a broader challenge: rebuilding software another team built. You inherit more than code. You inherit years of decisions whose reasons were never written down, and behavior that looks like a bug is sometimes load-bearing, something a customer quietly depends on. The dangerous move is to assume you understand a system just because you can read it.
So we did not start by writing code. We started by measuring, and that means two estimates, not one, taken in order. First comes an honest assessment of the state of what we inherited: which parts are healthy, where the real complexity hides, what carries genuine business logic worth preserving, and what is merely an accident of the old implementation. Only once that picture is clear can the second estimate, the effort to rebuild, rest on evidence instead of optimism. Estimating effort before you have estimated the state is guessing with a number attached to it. In practice that meant reading the legacy database schema and the real production data before the code, tracing the paths that actually carried business logic, and pinning down existing behavior in tests before we changed it, so we were measuring the system as it ran rather than as anyone assumed it ran.
That discipline is what let the matching logic survive the move intact, because we treated it as something to understand fully before touching it rather than something to reinvent from scratch. It is also what kept the project honest against its timeline. A measured start is slower for the first week and faster for every week after, because the surprises that derail rewrites are the ones nobody went looking for. We go deeper on that measured-takeover approach in taking over a legacy codebase you didn't write.
The Solution
The first decision shaped every one that followed: we stayed with PHP and moved to Laravel 12. Rewrites tempt teams to change everything at once, including the language, and a port to a JavaScript runtime was genuinely on the table. We ruled it out. The domain logic and years of data already lived in PHP and MySQL, and reimplementing subtle business rules in a new language while reconciling them against a live database would have multiplied the risk for no proportional gain. Laravel 12, running on PHP 8.2 with MySQL and Redis, gave us a modern, batteries-included foundation to rethink the architecture without paying that translation tax. Eloquent handled the data layer, Sanctum issued the API tokens the single-page app authenticates with, Reverb replaced fragile polling with real WebSocket updates, queues absorbed slow background work like imports and email, and a mature testing framework came in the box. We could spend our effort on the business, not on reinventing infrastructure. The full reasoning behind these choices, and the tradeoffs we weighed, is in choosing the right stack for a live rewrite.
On top of Laravel we drew a strict set of layers. A request flows from a route to a single-action controller, into a service that holds the business logic, down to a repository, and finally to a database model. Each controller does exactly one thing, which keeps it small and makes every endpoint a single unit we can test in isolation. Input is validated by a dedicated request class before it reaches the controller, services own the rules, and repositories wrap the database queries so business logic never depends on raw SQL. Records expose a ULID, a sortable random identifier, as their public key instead of a sequential number, so a URL never leaks how many customers or submissions exist. None of this is a diagram on a wiki that drifts out of date. Automated architecture tests fail the build if a controller grows a second responsibility or a data object skips its base class, and static analysis runs at its strictest setting, PHPStan level 10. The structure is enforced, not merely encouraged.
The front end became a Vue 3.5 single-page application written in TypeScript and built with Vite. Keeping it fully separate from the API, rather than rendering pages on the server, gave us a clean contract that the admin tools and the customer-facing app could both consume independently. Inside the app we drew a second deliberate line: TanStack Query owns server state, handling caching, background refetching, and request deduplication, while Pinia holds only local interface state. The cache stays the single source of truth for anything the server owns, which avoids the stale, duplicated data that creeps into single-page apps when those two concerns blur. The interface is built on Tailwind and shadcn-vue, and real-time changes arrive over WebSockets through Laravel Echo, so editors see updates without refreshing the page.
Magnum Code delivered all of this end to end: the legacy audit, the data migration strategy, the API, the single-page app, the deployment pipeline, and the production operations that keep it running today. One team owned the whole path from old system to live service.
Implementation Details
One contract, two languages
Every value that crosses the wire is a typed object, never a loose array. This is the job of Laravel Data, the spatie/laravel-data package, and it is the spine of the system. Each endpoint declares the exact shape it accepts and the exact shape it returns as a dedicated class. When a request arrives to save a manuscript's editorial scores, Laravel resolves it straight into a typed input object, runs that object's own validation rules, and hands the controller something it can trust. The controller stays small: it asks a service to do the work, then returns a typed response object built from the result. Validation lives on the data class itself, right beside the fields it describes, instead of scattered through the controller.
The decision that pays off for years is what happens next. A companion package, spatie/laravel-typescript-transformer, reads every data class marked for export and emits a matching TypeScript definition that the Vue application imports directly. A single command regenerates that file. The PHP class and the TypeScript type are not two artifacts kept in sync by discipline; one is generated from the other. Change a field in PHP, regenerate, and if the interface is still reading a field the API no longer sends, the build breaks on our machines long before a customer could ever reach it. Across roughly seventy of these typed contracts, that single source of truth removes an entire category of bugs that plague systems where the front end and back end quietly drift apart. We break this pattern down, with code, in type-safe Laravel APIs with Spatie Data and TypeScript.
A matching engine, not a filter
The heart of Submitit is matching a manuscript to the journals most likely to accept it. This is where a generic rewrite would have quietly destroyed value, because the matching is a real algorithm with years of editorial tuning baked in, not a database filter.
The mechanics are proprietary, so the specifics stay under the hood, but the shape of the approach is easy to describe. Every manuscript and every journal is distilled into a structured editorial profile, and the engine scores how well a given manuscript fits a given journal, expressed as a percentage. It weighs many signals at once rather than leaning on any single attribute, it treats some of those signals as more important than others, and it favors journals that are realistic targets over flattering long shots. That weighting reflects years of accumulated editorial judgment, the same intuition the service was built on, expressed so a computer can apply it consistently at scale.
The logic stays genre-aware throughout. Poetry, prose, and flash packages each follow their own acceptance rules before any score is computed. Recalculation is wired through a model observer, a piece of code that reacts automatically when a record changes, so a score edit anywhere fans out to the affected matches with no manual trigger, and a dedicated command can recompute the entire catalog when the rules themselves are tuned. We rebuilt all of this behind a single service with a focused test suite that pins the expected outcomes, so it can be tuned with confidence rather than fear. It is the product, and how we test something we deliberately keep this opaque is its own discipline, covered in testing a proprietary algorithm with confidence.
Refactoring for multi-work submissions
The legacy model assumed a submission was a single piece of writing. Real submissions are not always that simple. A poetry submission is often a set of poems sent together. A flash fiction submission is a package of short pieces. So part of the rebuild was restructuring the core data model into a parent submission with many child works, each carrying its own length and metadata, while the parent still behaves as one unit a customer uploads and an editor reviews. Existing records had to be reshaped into that parent-and-children form during the import, so legacy submissions arrived in the new model as though they had always lived there.
That change rippled straight into the matching engine, which had to become combination-aware. Journals set rules like a minimum and maximum number of poems and a cap on total pages, so the matcher now considers the possible groupings of a writer's poems and looks for one that fits a given journal's limits. Supporting multiple works per submission was not a cosmetic feature. It touched the data model, the matching logic, the editorial scoring screens, and the customer upload flow at the same time. Delivering it cleanly, without regressions in a live system, is exactly the kind of change the layered architecture and the shared type contracts were built to make safe. The data-model reshape behind it is detailed in one submission, many pieces.
Tasks and email that keep the work moving
Matching a manuscript is only the start. Each submission moves through a sequence of steps, and the platform tracks that work as tasks. Some tasks are created automatically: a scheduled command runs on a fixed cadence and surfaces them the moment they fall due, and others are spun up as queued side effects of domain events, the same background mechanism that sends mail and handles Stripe webhooks. The rest are created and assigned by hand, because real editorial work always has exceptions a rule cannot predict. Automatic and manual tasks sit side by side in the same queue, which gives the team one honest view of what needs doing and when.
Communication runs on the same single-source-of-truth principle as the rest of the system. The platform's emails, the welcome message a new client receives and the notifications that mark a submission's progress, are authored once as vue-email components in the front-end codebase and rendered into the Blade templates the API sends. The same design language the application uses shows up in the inbox, and a check in the build pipeline fails if a rendered template ever drifts out of sync with its source. Customers and staff stay informed without anyone hand-maintaining two copies of every message. How that email and the background work behind it are built and right-sized is in transactional email and queues in Laravel.
Going Live Without Going Dark
Cutover is where rewrites die, so we treat deployment as a first-class part of the engineering rather than an afterthought. Every environment is described as code. Ansible playbooks provision the servers from a known specification, so the test and production environments are built the same way and stay in sync, instead of drifting into the subtle differences that make a change pass in testing and then fail in production. Every change is exercised in a production-like test environment before it goes anywhere near real customers.
The data migration is engineered to the same standard. Rather than a one-time script, it is a repeatable, idempotent provisioning command, meaning it produces the same clean result no matter how many times it runs. In a defined order it rebuilds the schema, pulls the legacy records over a dedicated connection to the old database, layers in reference data, and restores customer manuscript files. Because it is safe to run end to end on demand, the cutover stopped being a nerve-wracking one-shot and became something we rehearsed until it was routine.
Deployments are automated through GitHub Actions. Every push runs the test suite and static analysis, and only a green pipeline is allowed to ship, first to the test environment and then, once validated, to production. On the servers, supervisord keeps the queue workers and the Reverb WebSocket server running across releases, so background jobs and real-time updates survive every deploy. Releases are meant to be routine and low-drama rather than events the team braces for.
Billing got particular care. At switchover we reconciled live Stripe customer records so existing subscribers kept their billing relationships intact rather than waking up to broken accounts, and the legacy application stayed online the entire time. When the new platform passed its checks, traffic moved to Submitit Platform with no interruption in service, while the public Submitit marketing site carried on unchanged. In production we run continuous error monitoring through Sentry, automated database backups, and health checks, and we watch real usage closely, because the first weeks of live traffic always surface things no staging environment can. The environments, pipeline, and security posture behind all of this are covered in environments, CI/CD, and security for a live rewrite.
A Roadmap, Not a Finish Line
Launching was a milestone, not the finish line. From the start we planned Submitit as a long-term, iterative engagement rather than a single hand-off, because that is how software a business depends on stays healthy over time.
A big-bang rewrite that tries to perfect every screen before anyone can use it is the riskiest path there is. So we sequenced the work. The first goal was a solid, type-safe foundation live in production with the core flows modernized. From there the platform advances in deliberate increments, each one shipped, used, and validated before the next begins.
That plan is written down, not improvised. We keep a technical roadmap of the surfaces still carrying older patterns, the components still built on the previous interface toolkit, and the data contracts still being tightened, each scheduled as its own phase with a clear scope. Concretely, that means migrating the last interface surfaces off the older Bootstrap styling onto the Tailwind and shadcn-vue component system one screen at a time, finishing the Laravel Data adoption on the endpoints that predate it, and shrinking a static-analysis baseline that gets a little smaller every time we open a file. Older patterns retire as we touch the code around them, so the system gets cleaner through ordinary feature work instead of demanding a separate, expensive cleanup project.
Two things keep that pace sustainable. The enforced architecture and the shared type contracts mean each increment lands without breaking what came before. And real customer feedback since launch feeds straight into what we build next. The destination is a platform where every part has been deliberately redesigned, reached not through one heroic push but through a steady, planned sequence the business can see coming and budget for.
Results
The headline is the timeline. We took Submitit from legacy audit to a live cutover in roughly three months, replacing an aging, hard-to-change application without a gap in service.
Moving that fast only counts if the result holds up, and the architecture is what makes both true at once. Every endpoint shares a typed contract between the API and the interface, so a whole class of integration bugs fails the build instead of reaching a customer. Business logic lives in services that are tested, and the conventions that keep it there are enforced by automated checks rather than by code review alone. The matching engine the business depends on was preserved faithfully and locked down with tests, so it can be tuned with confidence instead of fear. And the platform now supports submission types the old one could not, including multi-poem sets and multi-piece flash packages.
Most importantly, the Submitit Platform is in production today, serving the same customers the legacy system served.
Months, Not Years
A rebuild of this scope, a full platform with a proprietary matching engine, a typed API, and a live data migration, would have been a year-plus undertaking only a few years ago. The reason we compressed it into three months is not a single trick. It is the combination of deep engineering experience with a new generation of AI-assisted tooling, and the discipline to use the second without compromising the first.
We build with Claude Code and the latest AI development tools woven directly into the workflow. They accelerate the parts of a project that used to consume the most time: reading and mapping an unfamiliar legacy codebase, generating the controllers, data objects, and tests that follow an established pattern, migrating code to new conventions at scale, and keeping the type contracts in lockstep across the stack. Work that once took days of mechanical effort now takes hours.
What does not change is where the judgment lives. Experience decides the architecture, interprets the undocumented business rules, shapes the matching logic, and reviews every line that ships. The tooling handles the volume; senior engineers handle the decisions. The quality gates that run throughout, the test suite, the static analysis, the enforced architecture, exist precisely so that moving this fast never means trusting generated code blindly. That pairing, seasoned expertise steering modern tooling, is how three months replaced what used to take years. The exact toolchain and the guardrails that keep it safe are in AI-assisted development, fully under control.
Working With Us
Submitit is a case study in a specific kind of project: replacing software a business already depends on, without disruption, and coming out the other side with a platform that is faster to change and safe to extend. The hard part was rarely any single technology. It was sequencing the work so the lights never went out, preserving rules and data that took years to accumulate, and choosing an architecture that makes the next ten features cheaper than the last.
Staying on Laravel let us modernize without a risky language migration. Laravel Data gave us one contract across PHP and TypeScript. A strict, enforced architecture keeps the system honest as it grows. If you want to see the result, it is live at Submitit Platform.
If you have an aging application that still runs the business but has become expensive to change, that is exactly the problem we like to solve. For more on how we structure systems to absorb change, read our take on clean architecture and changing requirements. When you are ready to talk about your own rebuild, contact us.