Most migration write-ups describe a clean, linear path: export here, import there, flip a switch, done. In my experience, real migrations of systems people depend on every day look nothing like that. They’re layered, defensive, and full of small decisions whose only reward is that nothing bad happens.
This is the story of one such project at Pretius. The system wasn’t a client deliverable. It was our own internal PMT (project and people management tooling): a cluster of nine Oracle APEX applications we use daily for time logging, invoicing, availability planning, self-service and HR-adjacent workflows. On a normal day, a few dozen people use it. At month-end, when invoices go out, that number climbs to roughly 300.
It was a team effort. I owned the database migration and the production-data work; Bartek Bobrek handled the infrastructure (provisioning servers, networking, the reverse proxy, certificates) and Jacek Limanówka and Damian Rybicki kept development moving with the features and fixes people had been asking for. Bartek Zacharuk drove the go-live scheduling and the business-side rollout. I’m writing it up because I was the one closest to the migration itself, but almost nothing here happened in isolation.
The goal was to lift the whole thing, database, applications, and a web of external integrations, off our aging privately-hosted infrastructure and onto Oracle Cloud Infrastructure (OCI), while upgrading to the newest generation of the Oracle stack. The hard constraint: we could not lose a single invoice, and the outage window had to be measured in hours, not days.
This is a behind-the-scenes account of an internal cloud migration, the kind of unglamorous, high-stakes work that never makes it onto a marketing slide, but that I think is worth writing down honestly. Here’s how we did it.
On paper, PMT is a modest application. In practice, it was a textbook case of accumulated technical debt, the kind that builds up in any tool that grows alongside a company.
The database underneath it started life as an Oracle Database Express Edition (XE) instance, stood up years ago when Pretius was much smaller. Someone had reasonably decided back then that a free, APEX-friendly database was more than enough for “a few reports that people who know a bit of SQL can click through.” Over the years, though, PMT stopped being a reporting toy. It became something real developers built on and real people depended on, built by our devs now, not by folks who happened to know their way around a bit of SQL.
XE’s defining characteristic is also its central liability here: it caps the total volume of user data (12 GB in modern XE releases). For a system that logs time, stores invoice data, and keeps a growing operational history, that ceiling isn’t theoretical. I’d been raising this for a long time. The failure mode was blunt and recurring: the database would fill up, run out of room, and simply stop letting people in. When that coincides with month-end invoicing (300 people each trying to log in once and issue invoices, and something’s broken) a full database isn’t a minor inconvenience. It’s a business incident.
So the primary driver for the move was never “the cloud is trendy.” It was capacity and reliability: getting off a size-constrained free edition and onto infrastructure we actually control.
Timing helped too. Oracle APEX 26.1 had just landed with all the hype around its AI capabilities (APEXlang, AI-assisted Interactive Reports, AI Agents) and that made it a natural moment to modernize. Our existing PMT ran on APEX 22, already several years old, against a 19c database.
The target architecture was chosen deliberately, and a decision was made higher up to migrate to OCI. Two choices I want to call out:
That last one is worth explaining for anyone weighing the same choice. Oracle’s Autonomous Database removes most of the admin. You barely need a DBA, everything happens for you. The trade-off is control: when Oracle decides to raise your version, they raise it, and you simply get a notice. With a self-managed database, the admin burden stays on us, but we decide when we move versions. It has its pluses and minuses, and for a system with as many fragile integrations as ours, I wanted the control.
Here’s the part that turns a “small app” into a genuinely hard migration. PMT is small in code but dense in external connections, and every one of them is a place the migration can silently break. My inventory:
Each of these needs its own permissions, network paths, firewall openings, and security grants to work from a new server in a new cloud. And the critical problem: most of them can’t be meaningfully tested outside production. I can’t spin up a test Jira. I can’t have a test Gmail, or a test Google Drive, just to check something. Azure auth I could test; the rest, not really. That single fact shaped the entire strategy that follows, because if I can’t test the integrations safely beforehand, I have to design the cutover so that discovering a broken integration is survivable.
Compounding it: PMT had a development environment and a production environment, but no dedicated test environment. That was a problem in itself, and I’ll come back to how we plan to fix it.
Our first principle was to leave production completely untouched while we learned. Nobody should even notice anything was happening. Bartek Bobrek stood up a fresh, fully configured server in the new OCI tenancy: a clean development environment, entirely independent of everything running the old way. That was his piece, and it’s not a small one. The whole approach depends on having a safe, isolated place to work.
The data and schema move was mine, and it relied on one tool I want to highlight for any Oracle team doing this work: SQLcl’s Data Pump.
SQLcl is Oracle’s free command-line tool, the one everyone’s talking about now in the context of MCP and so on. Data Pump is one of its many features, essentially a wrapper over the DBMS_DATAPUMP package that lets you export and import whole schemas (and, in later versions, individual tables) with simple commands. I used it to take all of our development schemas (there were around ten, maybe more) with their tables, packages, functions, and data, and move them one-to-one onto the new database, which runs the far newer Oracle release (26ai) that APEX 26 needs.
One property of Data Pump I really appreciated: when you import a single dump containing multiple interdependent schemas, the tool is smart about ordering. Our schemas reference each other, so a native object-by-object load would trip over unresolved dependencies. Data Pump internally sequences the load so those dependencies resolve themselves, and by the end it pretty much just works, rather than leaving me to untangle invalid objects by hand. That’s what made moving everything at once practical.
The APEX applications themselves I exported from the old development instance and imported into the new APEX 26 workspace.
This is the detail that separates “run Data Pump and it works” from reality. On the old XE database, schemas had been created over the years pointing at whatever tablespace someone happened to pick at the time, inconsistent, because for years people just clicked around. The new OCI database, by contrast, had tablespaces laid out correctly and deliberately from the start.
The consequence: source tablespaces and target tablespaces didn’t match, so the import needed explicit tablespace remapping to route each schema’s objects to the right home. This is documented in my migration scripts. It wasn’t “fire off Data Pump and walk away”. I had to dig around a bit and set up the mapping properly.
I also had to account for inter-schema dependencies. Because the database validates the relationships between schemas (say, PMT_DATA‘s packages depending on objects in the Chronos schema) moving everything into a brand-new place can surface “something’s missing here” errors until the full set is present. Data Pump’s ordering helped a lot, but the dependency web still had to be understood.
Alongside PMT, we ran Chronos, a time-logging tool, on a completely separate server. Chronos has an interesting history: it was originally meant to be a management tool for Pretius Low-Code, with some ambitions around user management that never ended up being used. When the low-code product wound down, Chronos survived as a focused time-logging tool, used instead of, or alongside, Jira.
Keeping a whole separate server alive for one application and one schema made no sense; it was pointless environment maintenance. So as part of the migration, I pulled Chronos (its single schema and all its data) onto the new development server and consolidated it into the same PMT workspace. Two servers became one; two operational footprints became one. That’s the quiet kind of win a migration lets you bank: not a new feature, just less to maintain.
The new APEX 26 runtime broke a handful of things, but, importantly, only cosmetic, non-blocking ones. It really went very smoothly. The kinds of things that broke:
Fixing these was largely a team job on the development environment, the kind of work I handed off to Jacek and Damian while I stayed on the migration. Over the preceding months we’d also used the occasion to delete dozens of unused application pages that had piled up over the years, clear out junk, and (this one matters) rebuild access control to ensure full compliance with GDPR standards.
With development proven out, it was time for the genuinely dangerous part: production. This is where the problems stop being technical and start being about business continuity and coordination.
The core dilemma of any live-data migration: you can’t copy the data and then let people keep using the old system, because anything they add in the meantime (one new invoice, one changed number) gets lost. The only correct model is freeze the source, copy, verify, cut over. And with invoices in scope, my tolerance for loss was exactly zero. Losing even a single invoice or a single digit is unacceptable — 100% of it has to arrive intact.
To make the eventual freeze as short as possible, I ran a full dress rehearsal about a week ahead. I asked Bartek Bobrek to open a clean production environment on the new OCI server while everyone kept working normally on the old one. Then came the safety design that makes such a rehearsal possible without risk.
I told Bartek the new production server had to be completely sealed off from the outside world. Two controls in particular:
Why that second control isn’t optional: PMT’s packages contain scheduled jobs, hourly synchronizations that pull logs, push data outward, and talk to the very same external systems the live production talks to. If the new production could reach the outside while old production was still the system of record, both environments would fire the same syncs at the same destinations. You’d get duplicate writes, corrupted external state, and a genuinely bad day. So I disabled all the database jobs (I have a script that cuts everything off with a single command) and Bartek enforced the network isolation.
Inside that safe sandbox, I did the real preparation:
The whole point of this rehearsal was to move the discovery of errors earlier. Every bug I found here (a missing grant, a broken script, a misconfiguration, etc.) was a bug that would not eat into the real cutover window while users waited. I wanted the live event to genuinely take about 5 hours, not turn up 20 fresh bugs and balloon to 15.
Meanwhile, on development, the rest of the team (Jacek Limanówka and Damian Rybicki) kept shipping the features and fixes people had been asking for. It was a deliberate split: I owned the migration and the production data (the others don’t have authorization to touch production data, so that part was mine), while development momentum didn’t stall. I handed off the new features and fixes to them so I could stay focused on the move.
The original idea (Bartek Zacharuk’s )was to cut over on a Thursday afternoon, starting around 14:00. I pushed back, and the reasoning is a small lesson in risk management: if Thursday went sideways, we’d be up all night, exhausted, and Friday, a full business day, would open with no working database. Too much risk. I argued for Friday instead, so any overrun spilled into the weekend, when the cost to the business was lowest. We moved it.
The date itself was chosen carefully too: around the 22nd of the month, roughly a week before invoicing. That deliberately avoids the period when people actually issue invoices, so the outage would inconvenience almost nobody (maybe a stray individual, but not the larger crowd).
That turned out to be exactly right, but more on that in a second.
Before anything went off, we warned users through multiple channels: email, a Slack announcement, and an in-app banner so it couldn’t be missed. Around 13:00 on Friday, we shut everything down.
With users cut off and old production fully quiesced (every process stopped) I worked through my checklist again, live:
Then a subtle but important bit of access control during the cutover itself. Before opening the doors, I wanted to log in and click through the live applications myself, without curious people checking “is it back yet?” wandering in and creating state before I’d confirmed everything, in case I’d forgotten something. So I asked Bartek to restrict the application URLs, at the VPN level, to just the two of us.
Worth emphasizing for anyone planning similar work: the application URLs did not change. All the familiar addresses stayed the same. That put a real burden on the infrastructure side, Bartek had to reconfigure the reverse proxy (Apache) so those existing addresses now routed to the new environment in the cloud, and certificates and authorizations had to be re-wired accordingly. Keeping URLs stable is a gift to users (nothing to relearn, no broken bookmarks) paid for by ops.
Once the environment was reachable by the two of us, I ran my integration checklist, and yes, tested directly against production, because as I said, these integrations can’t be tested anywhere else. I know “Testing on production” may not sound great, but that was the reality. The technique was clean: perform a real action, verify the downstream effect, then remove the test artifact. For example:
Where something failed, the pattern was instructive and recurring: the database would be correctly configured to talk to, say, Gmail (the database-level ACL allowed it) but the OCI firewall blocked the traffic. That split (database says yes, network says no) generated a clean queue of tasks for Bartek to open the right paths. A good reminder that in cloud migrations, “permission” lives in at least two independent layers, and both have to agree.
This is where the Friday-over-Thursday call paid off. We:
The system became available to users on Saturday around 17:00, later than the Friday evening I’d hoped for, but comfortably inside the low-impact weekend window. Had we gone with the original Thursday plan, Friday would almost certainly have been a full lost business day. Choosing the window was as much a good business move as a technical one.
One more thing we folded into the weekend: rather than only swapping infrastructure, we also ran a standard CI/CD release. All the new functionality and fixes the team had built on development during the rehearsal period got deployed straight onto the new production. So users got the new server, all nine APEX apps in their new versions, and new features, all at once. A full infrastructure swap that also happened to deliver improvements.
Come Monday, Bartek Zacharuk brought a handful of business users online to click through their real billing and HR workflows. We set up a dedicated Slack channel and a Jira ticket where people attached the bugs they found. To move faster, we deliberately shortened the path where it made sense: colleagues less comfortable with Jira could just paste an issue into Slack, and one of us would turn it into a ticket and fix it, patching both development and production directly. Most things worked; the leftovers were small.
A detail that’s easy to overlook but genuinely good practice: as part of all this, I also refreshed the development environment with current production data, pushed through our anonymization process. On export from production (which I run), sensitive fields get replaced with randomized values, invoice amounts become random figures within plausible ranges, rates likewise. The result is a dump that carries a realistic shape but no real sensitive data, so anyone can take that file and load it onto any environment freely.
The payoff: development now mirrors what production looked like about a week and a half ago, so developers reproduce bugs against realistic data instead of stale fixtures. It’s a small process investment that pays back on every future bug hunt.
Two honest caveats.
No test environment yet. PMT still lacks a true test tier, and I feel the pain: hand something to a tester on development and they may report a “bug” that fixes itself ten minutes later because someone else was mid-change. The plan is pragmatic and cost-aware: stand up a test environment on demand, spun up two or three days before a release for final testing and torn down afterward, paying for a few days of runtime rather than a full month of an environment that mostly sits idle. On a daily basis we simply won’t need it, and this is exactly the kind of elasticity moving to OCI makes practical.
The AI features are staged. APEX 26 brings a lot, such as AI-driven natural-language reports where a user just describes what they want and it filters for them, AI Agents, and more. We haven’t switched these on yet. The reasoning is deliberate: don’t do everything at once. From a business point of view, apart from a few hours of downtime and the fact that it now runs on a new APEX, users honestly haven’t noticed much change. We know it’s a new APEX and that a lot is now possible, but this was the first step: getting onto the newest infrastructure. That was the critical thing. Now we can start doing much more, and the AI layer can come in on its own schedule rather than bolted onto an already high-risk cutover.
From the business side, the migration is almost invisible, and that’s the point. Apart from a few hours of unavailability and the new APEX underneath, most people haven’t noticed anything changed. That invisibility is the deliverable. The measure of a successful migration of a live system is how little anyone remembers it.
A few principles this project drove home for me:
I’ll end on something that cuts against the current hype, and I say this as someone who adopted APEX 26 specifically for its AI features. On the migration scripts themselves, AI was essentially no help. These were bespoke, deeply context-dependent tasks I had to work through by hand. I looked things up in the documentation where I hit something new and stitched the scripts together as needed, but the AI didn’t do that part for me. And frankly, I wouldn’t let an AI into production anyway.
My honest view: no AI could have walked into this database and produced the migration checklist. It couldn’t gather the information, because there’s too much of it, spread across too many places that connect in non-obvious ways. What made this work was holistic, structural knowledge of the system, knowing how this idiosyncratic, legacy-laden thing is actually wired together, the way Bartek Bobrek knows the infrastructure inside out and I know the application side. Someone who didn’t know the system couldn’t have done it, no matter the tooling.
And that’s the real point for anyone deciding who should run their next critical migration. The tools mattered, SQLcl Data Pump, OCI, APEX 26 all pulled their weight. But the thing that made it succeed was people who own the system and understand it end to end. That’s not something you generate on demand. It’s what an experienced team brings when the requirement is that 100% of the data arrives and the business barely notices you were there.
Rafał Grzegorczyk is a developer at Pretius, an Oracle partner with 20+ years of enterprise software delivery experience across custom development, low-code, and cloud, including Oracle Cloud Infrastructure and Oracle APEX. This article draws on public Oracle documentation on APEX 26.1, SQLcl Data Pump, and Oracle Database Express Edition for the technical background.