How TitanApps Migrated Smart Checklist to Forge (and Got It to Run on Atlassian)
This article is a guest post by TitanApps, an Atlassian Marketplace Partner.
Smart Checklist is trusted by thousands of teams to manage complex processes, including Definition of Done, approvals, risk assessments, procurement, HR workflows, and manufacturing steps. At TitanApps, we consider Smart Checklist to be our flagship app, which is why it was essential to ensure Smart Checklist runs on the most modern technology stack available: Forge.
To achieve this, we needed to migrate the app from Connect, but our goal wasn’t just to rewrite the app. We wanted to meet the strictest security expectations, qualify for the new Runs on Atlassian badge, and do it all without interrupting service for thousands of teams.
Spoiler: it wasn’t easy. But it worked.
Runs on Atlassian is Atlassian’s newest badge for cloud apps. All apps in the program are built on Forge and must use Forge compute and storage, which under the hood is the same secure and scalable AWS cloud infrastructure powering Atlassian Cloud. Runs on Atlassian apps provide data residency in the same region as the host product, and allow customers to block data egress at any time. These criteria are automatically verified and continuously monitored by Atlassian.
This post is a behind-the-scenes look at how we planned, rewrote, and shipped a fully Forge-native version of Smart Checklist, including:
- Why we decided to start with our flagship product
- How we rethought feature delivery to make deliverables tangible
- The storage and indexing challenges we solved with entity properties
- How we moved all customer data without downtime or data egress
- What we’d do differently (and what we think every app team should know)
If you’re planning a Forge migration, especially for an app with complex storage, we hope this helps.
How We Made the Decision
The decision to migrate to Forge wasn’t if, but when. Between Atlassian’s plans to end support for Connect and the increasing number of enterprise customers migrating from Data Center to Cloud with strict compliance requirements, it was clear: Forge was the future.
So instead of waiting for the perfect moment, we asked ourselves a simple question – why postpone the inevitable?
Then we made what might look like a bold move: we started with our flagship product, Smart Checklist.
It wasn’t the easiest option. Smart Checklist had the largest user base and the most data to handle, which made it the riskiest app to migrate. We could have chosen a safer path: start with a less popular app like Smart Templates, or even create a separate, parallel version of Smart Checklist on Forge. But none of those routes would create real value for customers or test the limits of what Forge could handle.
We figured: if we could move Smart Checklist Pro to Forge, we could move anything.
This approach would give us immediate benefits for security-conscious customers. We didn’t want a test case we could use from. We wanted real impact.
How to Eat an Elephant: One Bite at a Time
Once we committed to migrating Smart Checklist Pro, we knew we were in for a full rewrite. This wasn’t a lift-and-shift project. We started a rebuild on an entirely new platform. And when you’re rebuilding an app this large, it’s easy to get overwhelmed.
We reminded ourselves of the classic saying: “There’s only one way to eat an elephant – one bite at a time.”
First, we attempted to break things down by technical components:
- UI modules
- Read endpoints
- Write endpoints
- Backend logic
On paper, this looked like a good plan. In practice, it quickly fell apart. The problem? Deliverables weren’t testable. We’d finish a module or an endpoint, but we couldn’t validate whether the code was good enough to support a product feature. Our “definition of done” became fuzzy. It was too abstract.
So we flipped the model.
Instead of organizing the work by code structure, we built a feature tree and decomposed the project by actual product functionality: what users would interact with and what we could test.
This meant slicing the scope into Epics like:
- Checklist Rendering and Editing
- Manage Templates
- Settings Management
- JQL Functions & Automations
- JCMA Support
Now every increment was meaningful, testable, and something we could put in front of the QA team. It also gave the team a clear vision of what we were doing step by step.
This shift, from thinking in code to thinking in features, was one of the most important changes we made in the entire migration.
Solving Storage and Indexing with Entity Properties
One of the trickiest parts of the migration was rethinking how to store and query data without relying on our old database setup. In the Connect version of Smart Checklist, we used SQL to store checklist data and support custom JQL functions. But Forge didn’t give us a similar database when we started migration, and Forge Storage (Key Value Storage) wasn’t enough for our needs.
We also ruled out Forge’s new SQL option because it was still in EAP at the time of migration. And even if we used Forge Storage, it wouldn’t solve our main challenge: making checklists searchable in Jira using JQL.
That’s where entity properties came in.
Instead of storing search-indexable data in Forge Storage, we used Atlassian issue entity properties in Jira to create lightweight metadata that we could query via JQL. We stored hashed versions of checklist items inside the property, like this:
json
CopyEdit
issue.property["com.<vendor>.<app>.ItemStatusSearchMeta"].c = "67b53b769ee1bcea6ba112125de2eaef"
Then we declared a custom JQL function in our Forge app that could generate these hashes from the user’s search input. So when someone searches for:
jql
CopyEdit
checklistItemStatus("Item Content", "Done")
…our function turns “Item Content” into a hash and finds matching issues through the entity property. Users don’t see any of the hashing or storage; they get fast, accurate search results inside Jira.
We used hashes to keep predictable search limits, covering up to the first ~900 checklist items. For the raw checklist data, we split it across multiple entity properties to stay within Jira’s 30KB limit per property.
This approach gave us a way to:
- Store checklist search data without hitting storage limits in most cases
- Keep checklist content private (only metadata is indexed)
- Maintain fast JQL search in a Forge-native app
It wasn’t trivial, as we had to crawl all existing checklist data and generate entity properties for each issue. But it worked.
And most importantly, it gave our users the same search power they had before without any compromises.
How We Handled Data Migration Without Downtime
Rewriting an app is hard. Rewriting an app and migrating live customer data without interrupting service is harder. That’s what made data migration the most technically complex part of our Forge journey.
From the start, our goal was clear: no data egress. To qualify for the Runs on Atlassian badge, all data had to stay within the Atlassian Cloud, which meant no middleman servers and no external storage.
We briefly considered a phased migration:
- First, move to Connect-on-Forge. At this stage, the app has been converted to use the Forge manifest file, and it contains both Connect and Forge modules
- Then, gradually sync data to Forge storage while migrating the code module by module
- Finally, deprecate all Connect parts of the app and go full Forge
It looked simple on paper. But in practice, it introduced too many risks and would take months to migrate all the data.
If even one customer missed an update or stayed on an older version, we’d be forced to support that outdated data model indefinitely. On the other hand, cutting off the Connect version too early posed an even bigger risk: customers who skipped the intermediate upgrade step could lose all their data when moving to Forge. At the time of our migration, Atlassian hadn’t yet removed the requirement for manual admin approval, so we had to account for every possible edge case.
At the same time, Connect-on-Forge still meant data egress—and that wouldn’t pass Runs on Atlassian eligibility requirements.
Next, we thought about launching a brand-new, no-egress app in Forge, letting existing customers stay on Connect until we figured out a way to migrate. But that plan had drawbacks too. We didn’t want to launch the Runs on Atlassian version without any installs, which wouldn’t reflect the true popularity and traction of our app to potential customers.
So we landed on a new idea: perform a one-step switch, where we packaged all existing customer data, passed it through the Jira instance itself, and unpacked it into Forge storage the moment the Forge-native app was installed.
The glue that made it all work? Entity properties.
We used four types of Jira entity properties to bridge the gap between old and new:
- Application user properties – for global settings
- Addon properties – for app-level metadata
- Project properties – for things like checklist templates
- Issue properties – for everything else: checklists, history, etc.
Below, we can see the user properties example:
- Global settings
- Permission settings
- Statuses
- Global templates (content and their metadata like name, settings etc.)
See the project properties example on the next screenshot:
– project level templates (content and their metadata like name, settings etc.)
Next screenshot shows issue properties example:
- checklist content
- checklist metadata (mentions, dates, overall checklist stats)
- search metadata (used for previously described custom JQL search)
- checklist update history events
We exported all data from our Connect database, encoded it into these properties, and waited. As soon as the user installed the Forge version, our app unpacked the data from the entity properties into Forge storage.
There was only one hitch: entity properties are capped at 30KB per key. For big checklists, we had to split data across multiple properties, keeping track of everything using index references and status markers in Forge storage.
This unpacking process could take hours on large Jira instances, so we ran it in background jobs. But Forge background jobs have a 15-minute execution limit, so we built a checkpointing system. If a job hit the limit, it saved its state in storage and restarted from where it left off.
Sometimes Forge rate limits would block our job from continuing. In those cases, we paused, restarted, and bypassed the block. But if jobs kept restarting too often, Forge’s cyclic invocation limit would kick in and block us for good.
To handle that, we built a scheduled restarter service. Every hour, it scanned for “stuck” jobs and relaunched them, effectively resetting the invocation limit.
It wasn’t the cleanest part of the migration, but it worked. This approach allowed us to:
- Keep data fully on-platform
- Migrate large datasets transparently
- Ensure every user saw their old checklists exactly as they left them
It meant no extra work for users, no skipped items and no “please reinstall” notices.
There are still a few rare edge cases where data couldn’t be backed up by the Connect app. When possible, we handle these proactively.
For example:
- Archived issues don’t allow updates to entity properties
- In earlier stages of the migration, some checklists were skipped due to misconfigured instances or unexpected Jira errors
What We’d Do Differently (and What App Developers Should Know Before You Start)
We’re happy with how the migration turned out, but there were definitely lessons we learned the hard way. If you’re planning a similar migration from Connect to Forge, here are a few things we’d recommend (and a few we’d avoid next time around):
Do create abstractions for long-running jobs.
If your migration or background processing takes longer than Forge’s async function limits (15 minutes), you’ll need a fallback system. We built snapshot logic to persist the job state in Key Value Storage and resume processing on the next invocation. This was essential for migrating data across large Jira instances without timeouts.
Do prepare helpers for test environments.
When you’re working with Key Value Storage storage or entity properties, testing becomes challenging. We wrote mock helpers that emulate Forge storage APIs and Jira REST endpoints. This drastically improved developer speed and confidence, especially when working on isolated features or simulating edge cases.
If you would like to gain access to those – please write “I want test” to contact@titanapps.io
Do plan for gradual rollout if you’re migrating at scale.
In our case, we used a controlled release strategy to migrate a specific Jira instance first (our own, in fact) before scaling it to others. This gave us a safe playground to validate the full install–migrate–unpack cycle and surface hidden issues.
Do log everything and do it properly.
Forge’s limitations make debugging a bit of a guessing game unless you have detailed logs. We learned to log state transitions, background job activity, async function progress, and edge-case errors. It’s not glamorous, but it’s the only way to understand what happened when something breaks.
Do reach out to Atlassian support.
We had a few cases where Atlassian support was incredibly responsive. When we hit API limitations or edge-case behavior in Forge, their engineering team scheduled calls, shared internal context, and even rolled out fixes within hours. That kind of support made a big difference.
To achieve quicker answers make sure that the issue is well scoped and framed in a way Atlassian support workflows can handle. And sometimes, persistence helps too.
On the flip side:
Don’t chain async events.
Avoid triggering Forge async events from within other async functions. It might work a few times, but repetitive invocations will quickly hit Forge’s cyclic invocation limit. Once that happens, your job pipeline can break silently, or worse, hang in an unresolvable state. We hit this limit more than once and had to build a manual restarter service to recover from it.
These are the kind of “small things” that can become big blockers late in the game. Thinking about them early and building around them will save you hours of debugging and retries later.
Our Migration Timeline
From start to finish, the full migration of Smart Checklist Pro to Forge took just under three months. Our engineering team of four handled everything from rewriting the app to managing the data migration, feature validation, and background processing jobs.
Looking back, we’re confident we could have shaved at least a month off that timeline. Most of the delays came not from Forge itself, but from trial-and-error around storage limitations, job orchestration, and rollout strategy. Had we known on day one what we know now, we could have delivered even faster.
That’s part of why we’re sharing this story. If you are planning to migrate your app to Forge and face similar challenges, we’re open to helping you with the transition.
Why Forge Was Worth It
For us, migrating to Forge was a strategic move for the future of our products and our customers.
Qualifying for the Runs on Atlassian badge, Smart Checklist Pro now meets the strictest expectations around:
- Data residency (that mirrors Atlassian products)
- Security and compliance (data is contained within Atlassian infrastructure)
- Zero external infrastructure (compute and storage is fully Atlassian-hosted)
- Aligned with the security needs of migrating Data Center customers (as such customers tend to have stricter security requirements)
This matters most to our enterprise and security-sensitive customers, especially in the government sector, banking, financials, healthcare and other industries with strong data security requirements. Forge helps reduce procurement friction, streamline security approvals, and give teams confidence that their data stays exactly where it should.
Forge and the Runs on Atlassian badge are a clear way to show customers you care about security.
Now we have a proven path to get there, and if you’re navigating a tough Forge migration or building from scratch, ring us up. We might be able to help and guide you. If you are interested please reach out at contact@titanapps.io.
Check out Smart Checklist for Jira on the Atlassian Marketplace
