We migrated our organization from Confluence Data Center to Confluence Cloud. After the migration, users often ended up on the wrong instance, either following an old DC link to content that had moved to Cloud, or needing to cross-check a Cloud page against its DC version.
To fix this, we built a Forge app that automatically redirects users in both directions. Sharing it here in case it helps others doing similar migrations. Open to questions in the comments.
Happy to discuss trade-offs in the comments.
Two redirect directions, handled by two different frontend modules but the same backend:
The core of the app is a lookup table mapping DC page IDs to Cloud page IDs, plus a handful of fallback strategies for when a direct mapping isn't available.
The whole thing runs on Atlassian Forge. Nothing is self-hosted; there are no external servers. The relevant Forge pieces we used:
One thing worth calling out: because everything lives inside Forge, there's no infrastructure to manage, no separate auth layer to build, and no secrets to store. This makes it easy to divest the DC infrastructure eventually
┌─────────────────────────────────────────────────────────────────────────┐
│ Confluence Cloud (Forge) │
│ │
│ ┌──────────────────────────┐ ┌──────────────────────────────────┐ │
│ │ Frontend Modules │ │ Backend Resolvers │ │
│ │ │ │ │ │
│ │ • Cloud→DC Module │────▶│ • Mapping Lookups │ │
│ │ • DC→Cloud Module │────▶│ • Tracking Writer │ │
│ │ • Admin Settings UI │────▶│ • Statistics & Export │ │
│ └──────────────────────────┘ │ • Schema Migrations │ │
│ └──────────────┬───────────────────┘ │
│ │ │
│ ┌──────────────▼───────────────────┐ │
│ │ Forge SQL │ │
│ │ │ │
│ │ • page_mappings │ │
│ │ • redirect_tracking │ │
│ │ • migration_log │ │
│ └──────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
│ │
▼ ▼
Confluence Cloud API Confluence DC instance
(page metadata, CQL search) (destination for Cloud→DC)A few design notes:
The mapping data doesn't generate itself. Before (or during) migration, a separate process exports a CSV of DC → Cloud page ID pairs with all the URL variants and metadata. Then:
INSERT ... ON DUPLICATE KEY UPDATE), so re-runs are idempotent — safe to re-run to patch or update rowsFor the batch import itself, we chunk records (around 1,000 per call), dispatch several chunks concurrently, and cap a single run at around 10,000 records. Those numbers were tuned to stay well inside Forge's per-invocation limits; your mileage will vary depending on payload size and Forge SQL latency in your region.
Forge global settings page, admin-only (enforced by Forge). Five sections:
| Tab | Purpose |
|---|---|
| Dashboard | DB health, total mapping count, breakdown by space |
| Test Lookup | Query the mappings table by DC page ID or (space + title) |
| Add Mapping | Manually insert or update a single row |
| Tracking | Daily redirect volume, success/failure rates, paginated log, CSV export |
| Danger Zone | Bulk delete |
The tracking tab turned out to be more valuable than we expected. It's how admins find the "long tail" of broken inbound links that nobody filed a ticket about.
Happy to answer questions on any of this, especially around Forge SQL, the migration system, or the tracking design.
We have added a small js script in the Banner on DC, which takes the dcPage id from the current page, and redirects the user to the cloud URL. Once the cloud page opens, the Forge app takes over the redirection proces as mentioned above.
@Meghdut Mandal wow, I would love to learn more. We have the only cloud redirection app on the Atlassian Marketplace - Redirection for Confluence and we know that migrations are a use case.
Oh nice! This sounds really neat...
Thanks for sharing! 🙌
Definitely going to bookmark the article to potentially get inspired next time we do a migration :))
Great write-up, and a use case I ran into myself recently.
We had a similar but slightly different scenario: Confluence had been migrated to Cloud, but Jira was still sitting on Data Center, holding 300k+ issue links pointing at the old Confluence DC URLs. No redirection layer was going to fix that at scale.
I ended up porting one of my Forge apps into a Data Center plugin to tackle this directly inside Jira. It lets you:
- Upload the CMA (Confluence Migration Assistant) export to map old URLs to new ones
- Connect to the Cloud tenant for live lookups when the CMA does not cover a URL
- Scan projects via JQL to scope what gets processed
- Run a search and replace across issue content, replacing DC links with the correct Cloud URLs
Handles the 300k+ links without timeouts using a background job. If anyone is dealing with the Jira side of a phased migration, happy to share more details.
Wow. Pretty cool.
I do wonder why you thought "No redirection layer was going to fix that at scale."
Are you thinking that a redirection service would not hold up under the load?
I would posit that even though there are 300k+ issue links to Confluence, it seems unlikely that all 300k links are being clicked on at the same time.
Because that's the only time the a redirect service would actually have to actually do anything.
My mapping file contains over 300k links. I don't have the usage stats handy, but we have a 6000 user license, and we've never had issues with the redirection service (I'm using Apache) going down.
Fair point on scaling, you're right.
Throughput wasn't the blocker, "at scale" was sloppy framing on my part.
The actual reason redirect wasn't an option: the source DC Confluence host had been decommissioned by the time we got involved.
No DNS, no Apache, no server.
The links in the Jira issues had become pure dead URLs. With nothing alive at the old hostname, there's nothing to put a redirector on.
That made the choice less about "is redirect or rewrite better" and more about "redirect requires the source host to still exist, and in our case it didn't."
On the Forge SQL angle from Meghdut's setup: storage grows linearly with the mapping list.
Meghdut himself flagged "Forge SQL limits, batch import sizing needs careful tuning" in the original post.
At 300k+ rows per installation it becomes a real operational consideration, especially when the same app gets installed across many customers each with their own large CMA export.
Even when the source host is alive, one more thing nudges toward rewrite: the URL in the Jira issue body is what users see in search results, copy-link, board cards, CSV exports. Even with a working redirector, the displayed URL still reads as the old DC hostname.
Looks broken to end users until they actually click.
I'll also admit a stylistic bias here: I come from the "rather fix what's broken than slap a patch on it" camp.
Rewriting the source feels cleaner to me than carrying a permanent translation layer forward.
So my real point isn't "redirect doesn't scale", it's "redirect requires the source URL to resolve somewhere, and in our scenario it couldn't." Apache RewriteMap is genuinely the right answer when the source host is still up. We just couldn't run it.
PS: And thanks for bringing this up, honestly. I really enjoy these kinds of topics. With everything moving to Cloud nowadays, there's less and less ground for low-level migration conversations like this one.