{"id":1473,"date":"2025-09-28T12:10:28","date_gmt":"2025-09-28T09:10:28","guid":{"rendered":"https:\/\/www.drinkits.lv\/?p=1473"},"modified":"2025-09-29T06:50:43","modified_gmt":"2025-09-29T03:50:43","slug":"data-center-to-cloud-the-story-of-developing-a-niche-jira-app-on-forge","status":"publish","type":"post","link":"https:\/\/www.drinkits.lv\/en\/2025\/09\/28\/data-center-to-cloud-the-story-of-developing-a-niche-jira-app-on-forge\/","title":{"rendered":"Data Center to Cloud: The Story of Developing a Niche Jira App on Forge"},"content":{"rendered":"<p style=\"text-align: left;\"><img loading=\"lazy\" decoding=\"async\" class=\"alignright wp-image-1482 size-full\" src=\"https:\/\/www.drinkits.lv\/wp-content\/uploads\/2025\/09\/logo.png\" alt=\"\" width=\"144\" height=\"144\" srcset=\"https:\/\/www.drinkits.lv\/wp-content\/uploads\/2025\/09\/logo.png 144w, https:\/\/www.drinkits.lv\/wp-content\/uploads\/2025\/09\/logo-12x12.png 12w, https:\/\/www.drinkits.lv\/wp-content\/uploads\/2025\/09\/logo-120x120.png 120w\" sizes=\"auto, (max-width: 144px) 100vw, 144px\" \/>Every developer has the project. The one that began with an easy concept, but becomes a game of frustrations and ups and downs of technical issues, experiments with design, and learning to live with the peculiarities of the platform that you were to be working with. In my case, such undertaking was the migration of my Data Center app, ASiC Viewer, to the Atlassian Cloud.<\/p>\n<p>Here is the narrative of that trip &#8211; a tale of struggling with UI Kit 2, authentication circles, and searching in vain after that native Jira feel.<\/p>\n<h3>The Issue: The lack of a Jira capability to recognize\/read signatures from ASiC containers<\/h3>\n<p>Digital signatures are not only convenient in Europe but a legal obligation. Contracts, official submissions, and legal documents today are often handled in formats such as BDOC, EDOC, ASICS, and others. To check a signature, our users were forced to:<\/p>\n<ol>\n<li>Download the attachment.<\/li>\n<li>Open desktop application or use online validation platform.<\/li>\n<li>Upload the file there.<\/li>\n<li>Analyze the results.<\/li>\n<\/ol>\n<p>It was cumbersome, bulky and interrupted the process. My <a href=\"https:\/\/marketplace.atlassian.com\/apps\/1752327390\/asic-viewer?hosting=datacenter&amp;tab=overview\">Data Center application<\/a> fixed this by allowing users to see the signatures in Jira. It worked beautifully. However, as Atlassian drove a definite &#8220;Cloud-first&#8221; strategy, I realized I had to &#8220;move&#8221; our plugin to Jira Cloud.<\/p>\n<h3>The Cloud Imperative: Not Just a Port<strong><br \/>\n<\/strong><\/h3>\n<p>Initially, I believed that I would simply be able to port our Java application to the Cloud. I was wrong. Forge is a totally different animal. You are no longer dropping code into Jira, you are creating a sandboxed web app that communicates with Jira through APIs. It was a complete rewrite &#8211; Java to Node.js and React. This wasn\u2019t just a port, I had to start from the very beginning.<\/p>\n<h3>UI\/UX Odyssey: Pursuing the UIKit 2: The Jira Feel<\/h3>\n<p><strong>Phase 1: The &#8220;Broken&#8221; Table<\/strong><br \/>\nMy initial effort of creating good attachment list was a mess. Columns didn\u2019t line up. The Stack and Inline components of UI Kit 2 are not designed to work in hard tables &#8211; any attempt to make them do so caused everything to break.<\/p>\n<p><a href=\"https:\/\/www.drinkits.lv\/wp-content\/uploads\/2025\/09\/table.png\" rel=\"attachment wp-att-1486\" data-lbwps-width=\"1325\" data-lbwps-height=\"1000\" data-lbwps-srcsmall=\"https:\/\/www.drinkits.lv\/wp-content\/uploads\/2025\/09\/table.png\"><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-1486 alignleft\" src=\"https:\/\/www.drinkits.lv\/wp-content\/uploads\/2025\/09\/table-300x226.png\" alt=\"\" width=\"320\" height=\"244\" \/><\/a><a href=\"https:\/\/www.drinkits.lv\/wp-content\/uploads\/2025\/09\/tabel2.png\" data-lbwps-width=\"1297\" data-lbwps-height=\"1156\" data-lbwps-srcsmall=\"https:\/\/www.drinkits.lv\/wp-content\/uploads\/2025\/09\/tabel2.png\"><img loading=\"lazy\" decoding=\"async\" class=\"alignleft wp-image-1490\" src=\"https:\/\/www.drinkits.lv\/wp-content\/uploads\/2025\/09\/tabel2-300x267.png\" alt=\"\" width=\"334\" height=\"299\" srcset=\"https:\/\/www.drinkits.lv\/wp-content\/uploads\/2025\/09\/tabel2-768x685.png 768w, https:\/\/www.drinkits.lv\/wp-content\/uploads\/2025\/09\/tabel2-13x12.png 13w, https:\/\/www.drinkits.lv\/wp-content\/uploads\/2025\/09\/tabel2-700x624.png 700w, https:\/\/www.drinkits.lv\/wp-content\/uploads\/2025\/09\/tabel2.png 1297w\" sizes=\"auto, (max-width: 334px) 100vw, 334px\" \/><\/a><\/p>\n<p><strong>Phase 2: The Over-engineered Accordion.<br \/>\n<\/strong>Then I experimented with an interactive accordion: a row is clicked, and one can expand it inline to read the details. Modern, but disruptive. The layout of the page was moved over-the-top and in order to just check the signature, it felt clunky.<\/p>\n<p><strong><a href=\"https:\/\/www.drinkits.lv\/wp-content\/uploads\/2025\/09\/signer_card.jpg\" data-lbwps-width=\"507\" data-lbwps-height=\"375\" data-lbwps-srcsmall=\"https:\/\/www.drinkits.lv\/wp-content\/uploads\/2025\/09\/signer_card.jpg\"><img loading=\"lazy\" decoding=\"async\" class=\"alignright size-medium wp-image-1496\" src=\"https:\/\/www.drinkits.lv\/wp-content\/uploads\/2025\/09\/signer_card-300x222.jpg\" alt=\"\" width=\"300\" height=\"222\" srcset=\"https:\/\/www.drinkits.lv\/wp-content\/uploads\/2025\/09\/signer_card-300x222.jpg 300w, https:\/\/www.drinkits.lv\/wp-content\/uploads\/2025\/09\/signer_card-16x12.jpg 16w, https:\/\/www.drinkits.lv\/wp-content\/uploads\/2025\/09\/signer_card.jpg 507w\" sizes=\"auto, (max-width: 300px) 100vw, 300px\" \/><\/a>Phase 3: Mimic, Don\u2019t Reinvent<br \/>\n<\/strong>Finally, I asked myself: &#8220;How lists look originally in Jira Cloud? &#8221; I abandoned my experiments and reproduced the well-known pattern. The component that came to the rescue of us was the <em>Popup<\/em> component which was lightweight, contextual and fast. On a single click of the View Signatures button, a nice pop-up was brought up attached to the button.<\/p>\n<h3>Agonizing with Forge: The notorious Authentication Loop<\/h3>\n<p>Every Forge developer will eventually bump into <em>NEEDS_AUTHENTICATION_ERR<\/em>. I was no exception. My application was properly using the secure <em>asUser()<\/em> API calls, but I kept getting the user consent prompt over and over again.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"shell\">INFO\u00a0 \u00a0 13:06:47.679\u00a0 49458f07-ce56-4473-9d22-afa224615b33\u00a0 [asic-cloud] handler route { action: 'getIssueAttachments', payloadKeys: [ 'issueId' ] }\r\nINFO\u00a0 \u00a0 13:06:47.679\u00a0 49458f07-ce56-4473-9d22-afa224615b33\u00a0 [asic-cloud] doGetIssueAttachments { issueId: '10000' }\r\nINFO\u00a0 \u00a0 13:06:47.680\u00a0 49458f07-ce56-4473-9d22-afa224615b33\u00a0 [asic-cloud] fetchIssueAttachmentsAsUser start { issueId: '10000' }\r\nERROR\u00a0 \u00a013:06:47.998\u00a0 49458f07-ce56-4473-9d22-afa224615b33\u00a0 handler failed [NEEDS_AUTHENTICATION_ERR: Authentication Required] {\r\n\u00a0 status: 401,\r\n\u00a0 serviceKey: 'atlassian-token-service-key',\r\n\u00a0 options: undefined\r\n}\r\n\r\nERROR\u00a0 \u00a013:06:47.998\u00a0 49458f07-ce56-4473-9d22-afa224615b33\u00a0 [NEEDS_AUTHENTICATION_ERR: Authentication Required] {\r\n\u00a0 status: 401,\r\n\u00a0 serviceKey: 'atlassian-token-service-key',\r\n\u00a0 options: undefined\r\n}<\/pre>\n<p>I chased every rabbit hole. I tried complex workarounds, manually catching and throwing 401 errors, believing it was a logic bug. Religiously repeated the clean uninstall and reinstall process, thinking there was a conflict between my local<em> forge tunnel<\/em> and the deployed development environment. Nothing worked.<\/p>\n<p>The actual answer was simple and buried in the Forge permission scheme.<\/p>\n<p>The problem wasn\u2019t my code\u2014it was <em>manifest.yml<\/em>. In my attempt to follow the &#8220;Principle of Least Privilege,&#8221; I had been too aggressive. I was only using the most granular scopes, like <em>read:attachment:jira<\/em>. It turns out that for the user consent flow to authorize and remember decisions across the app, a slightly broader scope was required.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"yaml\">permissions:\r\n  scopes:\r\n    - 'read:jira-work' # The key to solving the loop\r\n    - 'read:attachment:jira'\r\n    - 'read:jira-user'<\/pre>\n<h3>Lessons Learned in the Forge<\/h3>\n<p>This trip was not only a code writing exercise, but also an exercise in learning a new way of developing Atlassian apps. Here are my key takeaways:<\/p>\n<ol>\n<li><strong>Accept the Limitations<\/strong>: The limitations that UI Kit 2 has are not bugs; they are features that provide security, consistency and accessibility. Rather than struggling against the framework, application of its components (such as <em>Popup<\/em> and <em>Grid<\/em>) to their intended purpose will give the best results.<\/li>\n<li><strong>The Compass is the User Feeling<\/strong>: It was the feeling that the design was a bit broken, or the feeling that the design was not quite right, which led me. The process of endless repetition of spacing, alignment, and micro-interactions is what makes a functional tool a professional product.<\/li>\n<li><strong>Replicate the Native UI<\/strong>: Users are already experts in using Jira.<\/li>\n<\/ol>\n<p><span aria-haspopup=\"dialog\" aria-expanded=\"false\" aria-controls=\"radix-_r_7b_\" data-state=\"closed\" data-slot=\"popover-trigger\">The outcome is the <strong>ASiC Viewer Cloud<\/strong>, which is an application that I am proud of.<\/span> <span aria-haspopup=\"dialog\" aria-expanded=\"false\" aria-controls=\"radix-_r_7c_\" data-state=\"closed\" data-slot=\"popover-trigger\">It is quick, safe and, above all, it seems to be part of Jira.<\/span><\/p>\n<p><a href=\"https:\/\/www.drinkits.lv\/wp-content\/uploads\/2025\/09\/viewer.jpg\" data-lbwps-width=\"1239\" data-lbwps-height=\"274\" data-lbwps-srcsmall=\"https:\/\/www.drinkits.lv\/wp-content\/uploads\/2025\/09\/viewer-18x4.jpg\"><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-1475\" src=\"https:\/\/www.drinkits.lv\/wp-content\/uploads\/2025\/09\/viewer.jpg\" alt=\"\" width=\"1239\" height=\"274\" srcset=\"https:\/\/www.drinkits.lv\/wp-content\/uploads\/2025\/09\/viewer.jpg 1239w, https:\/\/www.drinkits.lv\/wp-content\/uploads\/2025\/09\/viewer-300x66.jpg 300w, https:\/\/www.drinkits.lv\/wp-content\/uploads\/2025\/09\/viewer-768x170.jpg 768w, https:\/\/www.drinkits.lv\/wp-content\/uploads\/2025\/09\/viewer-18x4.jpg 18w, https:\/\/www.drinkits.lv\/wp-content\/uploads\/2025\/09\/viewer-700x155.jpg 700w\" sizes=\"auto, (max-width: 1239px) 100vw, 1239px\" \/><\/a><\/p>\n<p><iframe loading=\"lazy\" title=\"YouTube video player\" src=\"https:\/\/www.youtube.com\/embed\/Q_4EuNnw0n0?si=YgZOjyrNchRve9gl\" width=\"700\" height=\"394\" frameborder=\"0\" allowfullscreen=\"allowfullscreen\"><\/iframe><\/p>\n<p>You can check out the final app on the Atlassian Marketplace <a href=\"https:\/\/marketplace.atlassian.com\/apps\/1975788992\">here<\/a>.<\/p>","protected":false},"excerpt":{"rendered":"<a href=\"https:\/\/www.drinkits.lv\/en\/2025\/09\/28\/data-center-to-cloud-the-story-of-developing-a-niche-jira-app-on-forge\/\" rel=\"bookmark\" title=\"Permalink to Data Center to Cloud: The Story of Developing a Niche Jira App on Forge\"><p>Every developer has the project. The one that began with an easy concept, but becomes a game of frustrations and ups and downs of technical issues, experiments with design, and learning to live with the peculiarities of the platform that you were to be working with. In my case, such undertaking was the migration of [&hellip;]<\/p>\n<\/a>","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"footnotes":""},"categories":[245],"tags":[250,253,254,248,252,246,251,247,47,249],"class_list":{"0":"post-1473","1":"post","2":"type-post","3":"status-publish","4":"format-standard","6":"category-jira","7":"tag-asic","8":"tag-asice","9":"tag-asics","10":"tag-atlassian","11":"tag-ddoc","12":"tag-drinkits-dev","13":"tag-edoc","14":"tag-forge","15":"tag-jira","16":"tag-jira-cloud","17":"h-entry","18":"hentry"},"_links":{"self":[{"href":"https:\/\/www.drinkits.lv\/en\/wp-json\/wp\/v2\/posts\/1473","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.drinkits.lv\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.drinkits.lv\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.drinkits.lv\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.drinkits.lv\/en\/wp-json\/wp\/v2\/comments?post=1473"}],"version-history":[{"count":33,"href":"https:\/\/www.drinkits.lv\/en\/wp-json\/wp\/v2\/posts\/1473\/revisions"}],"predecessor-version":[{"id":1558,"href":"https:\/\/www.drinkits.lv\/en\/wp-json\/wp\/v2\/posts\/1473\/revisions\/1558"}],"wp:attachment":[{"href":"https:\/\/www.drinkits.lv\/en\/wp-json\/wp\/v2\/media?parent=1473"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.drinkits.lv\/en\/wp-json\/wp\/v2\/categories?post=1473"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.drinkits.lv\/en\/wp-json\/wp\/v2\/tags?post=1473"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}