<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Tim Osborne</title>
    <description>The latest articles on DEV Community by Tim Osborne (@osbornetim89).</description>
    <link>https://dev.to/osbornetim89</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4233983%2Fba021527-5155-49e8-9649-a8c8d4cf7d55.png</url>
      <title>DEV Community: Tim Osborne</title>
      <link>https://dev.to/osbornetim89</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/osbornetim89"/>
    <language>en</language>
    <item>
      <title>Porting a 48,000-install Jira app off Atlassian Connect: what moved, what didn't, and the OAuth gap nobody warns you about</title>
      <dc:creator>Tim Osborne</dc:creator>
      <pubDate>Sun, 08 Nov 2026 18:28:56 +0000</pubDate>
      <link>https://dev.to/osbornetim89/porting-a-48000-install-jira-app-off-atlassian-connect-what-moved-what-didnt-and-the-oauth-gap-2k0n</link>
      <guid>https://dev.to/osbornetim89/porting-a-48000-install-jira-app-off-atlassian-connect-what-moved-what-didnt-and-the-oauth-gap-2k0n</guid>
      <description>&lt;p&gt;Atlassian Connect reaches end of support on 31 January 2027. Connect apps keep running after that date, but Atlassian "will only address critical security patches", and some changes "will cause apps to stop functioning without advance notice" (&lt;a href="https://www.atlassian.com/blog/development/getting-ready-for-connect-end-of-support" rel="noopener noreferrer"&gt;Atlassian&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;To see what a real migration involves, I took an open-source Connect app and ported it to Forge. I chose &lt;a href="https://github.com/atlassian-labs/figma-for-jira" rel="noopener noreferrer"&gt;atlassian-labs/figma-for-jira&lt;/a&gt; because it is Apache-2.0 licensed and widely used: the "Figma for Jira" Marketplace listing has 48,052 installs and still has no Forge manifest (Marketplace and forge-apps.com APIs, 7 November 2026). It is also a hard case, which makes it a useful one.&lt;/p&gt;

&lt;p&gt;The port runs on a live Jira Cloud site. It is a case study, with no connection to Figma or Atlassian. Code: &lt;a href="https://github.com/tosborne89/figma-forge-port" rel="noopener noreferrer"&gt;github.com/tosborne89/figma-forge-port&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What moved cleanly
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Connect&lt;/th&gt;
&lt;th&gt;Forge&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Admin page served from the app's own server in an iframe&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;jira:adminPage&lt;/code&gt; in UI Kit, rendered natively by Jira&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;installed&lt;/code&gt; webhook storing &lt;code&gt;clientKey&lt;/code&gt; and &lt;code&gt;sharedSecret&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;An install trigger with nothing to store, because Forge owns installation identity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;uninstalled&lt;/code&gt; webhook&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;preUninstall&lt;/code&gt;, which deletes the app's Figma webhooks and then its data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two Express routes for Figma webhooks, with a passcode check&lt;/td&gt;
&lt;td&gt;One web trigger routed by path, with a constant-time passcode check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Background sync via &lt;code&gt;setImmediate&lt;/code&gt; with no retry&lt;/td&gt;
&lt;td&gt;A Forge Async Events queue (up to 900 s) that retries on failure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL tables for teams and webhooks&lt;/td&gt;
&lt;td&gt;Forge key-value storage, with passcodes in secret storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connect JWT middleware and an installations table&lt;/td&gt;
&lt;td&gt;Deleted. Forge authenticates requests itself.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last row is the pattern for the whole port: a lot of code simply disappears, because Forge takes over hosting, installation identity and request auth.&lt;/p&gt;

&lt;h2&gt;
  
  
  What did not move: the Designs panel
&lt;/h2&gt;

&lt;p&gt;The app's core feature is Jira's native Designs panel, provided through the Connect module &lt;code&gt;jiraDesignInfoProvider&lt;/code&gt;. Forge has no equivalent module, and its scope list has no design scope (&lt;a href="https://developer.atlassian.com/cloud/jira/software/scopes-for-oauth-2-3LO-and-forge-apps/" rel="noopener noreferrer"&gt;Forge scopes&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Keeping that one module as Connect inside a Forge manifest doesn't escape the deadline either: such an app "is considered a Connect app until all Connect modules are removed" (&lt;a href="https://community.developer.atlassian.com/t/how-long-forge-connectmodules-will-be-working/98926" rel="noopener noreferrer"&gt;developer community&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;For a client in this position, I'd recommend moving everything else now and raising the gap with Atlassian. In parallel, prototype a pure-Forge fallback, the route &lt;a href="https://marketplace.atlassian.com/apps/1221981" rel="noopener noreferrer"&gt;Figma for Jira Pro&lt;/a&gt; already takes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The OAuth gap
&lt;/h2&gt;

&lt;p&gt;Forge has built-in external auth (&lt;code&gt;withProvider&lt;/code&gt;), and the obvious plan is to delete the app's own Figma OAuth code and use it. That plan fails.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;withProvider&lt;/code&gt; only works in functions a user invokes from the UI. Atlassian staff say providers "only support online asUser which requires user context" (&lt;a href="https://community.developer.atlassian.com/t/can-forge-webtriggers-access-provider-authentication-like-google/96357" rel="noopener noreferrer"&gt;developer community&lt;/a&gt;). This app calls Figma in two places where no user is present:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the queue consumer that reacts to Figma webhooks;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;preUninstall&lt;/code&gt;, which deletes the app's Figma webhooks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the app has to keep its own OAuth code. The port does it like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Start:&lt;/strong&gt; the admin clicks Connect Figma. A resolver signs a short-lived state with HMAC and stores a one-time PKCE S256 verifier. It then returns Figma's authorize URL, which opens in a new tab because Figma blocks its sign-in inside WebViews (&lt;a href="https://developers.figma.com/docs/rest-api/oauth-apps/" rel="noopener noreferrer"&gt;Figma OAuth apps&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Callback:&lt;/strong&gt; a dedicated web trigger checks the state and consumes the verifier, so a replayed callback fails. It then exchanges the code straight away, since Figma's codes expire after 30 seconds. Tokens go into Forge secret storage under the admin's account ID.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use with no user present:&lt;/strong&gt; the consumer looks up who connected the team, loads that account's token, and calls Figma through Forge &lt;code&gt;fetch&lt;/code&gt;, with &lt;code&gt;api.figma.com&lt;/code&gt; as the only declared egress host.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refresh:&lt;/strong&gt; within an hour of expiry. Figma's refresh response contains no new refresh token, so the old one is kept (&lt;a href="https://developers.figma.com/docs/rest-api/oauth-apps/" rel="noopener noreferrer"&gt;Figma OAuth apps&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is stricter than the original, which used a signed state only. The port adds PKCE and one-time state.&lt;/p&gt;

&lt;h2&gt;
  
  
  A finding that matters for Marketplace apps
&lt;/h2&gt;

&lt;p&gt;Forge web trigger URLs are stable per app, site and environment (&lt;a href="https://developer.atlassian.com/platform/forge/runtime-reference/web-trigger-api/" rel="noopener noreferrer"&gt;web trigger API&lt;/a&gt;). On Connect, the app had one base URL, so it could register one OAuth redirect URI with Figma. On Forge, every customer site gets a different callback URL.&lt;/p&gt;

&lt;p&gt;For a private app on one site, that doesn't matter. A Marketplace app needs one of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a small fixed relay that forwards the callback to the per-site URL carried in the signed state;&lt;/li&gt;
&lt;li&gt;or one OAuth app per customer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plan for this before you start, not after.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proof
&lt;/h2&gt;

&lt;p&gt;On a live Jira Cloud site, on 8 November 2026:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sign-in works.&lt;/strong&gt; The admin page shows "Connected as" with the Figma account's email, read from Figma's &lt;code&gt;/v1/me&lt;/code&gt; with the stored token.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe7sqqwcbrbrm6ad37v3y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe7sqqwcbrbrm6ad37v3y.png" alt="The Forge admin page in Jira settings, showing &amp;quot;Connected as&amp;quot; with the Figma account" width="800" height="251"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Figma's paid-plan rule is handled.&lt;/strong&gt; Figma answers webhook creation on free plans with 400 "Access Denied". The port maps that to a clear message, as the original did.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Background calls work with no user present.&lt;/strong&gt; A queued job, running in the consumer with no user context, read a Figma file with the stored token and logged &lt;code&gt;figma-sync read file { name: 'Forge port sync test' }&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft3dya88mral4llgyjkfu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft3dya88mral4llgyjkfu.png" alt="forge logs output showing the queue consumer reading the Figma file with no user present" width="799" height="222"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tests:&lt;/strong&gt; 60 unit tests covering state tampering and expiry, one-time verifiers, redirect mismatches, refresh behaviour, and the callback never echoing input. &lt;code&gt;npm audit&lt;/code&gt; reports 0 vulnerabilities.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Practical traps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Adding an egress host forces a major version, a &lt;code&gt;forge install --upgrade&lt;/code&gt;, and loss of "Runs on Atlassian" eligibility.&lt;/li&gt;
&lt;li&gt;The string form of an egress entry (&lt;code&gt;'https://api.figma.com'&lt;/code&gt;) now gives a lint deprecation warning. Use &lt;code&gt;address: api.figma.com&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;jira:adminPage&lt;/code&gt; with &lt;code&gt;useAsConfig: true&lt;/code&gt; lives at &lt;code&gt;/jira/settings/apps/configure/{appId}/{envId}&lt;/code&gt;. The URL without &lt;code&gt;configure&lt;/code&gt; doesn't work.&lt;/li&gt;
&lt;li&gt;Store client secrets with &lt;code&gt;forge variables set --encrypt&lt;/code&gt;, and type them in your own terminal, never in a shared session.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  If you own a Connect app
&lt;/h2&gt;

&lt;p&gt;If your app still shows LEGACY, I offer a free audit. You get a component map like the one above, with an honest list of what Forge can't do yet. Details are on the &lt;a href="https://tosborne89.github.io/" rel="noopener noreferrer"&gt;offer page&lt;/a&gt;, email &lt;a href="mailto:osborne.tim89@gmail.com?subject=Forge%20migration"&gt;osborne.tim89@gmail.com&lt;/a&gt;, or &lt;a href="https://www.upwork.com/freelancers/~01c351fe04e5d1ae40" rel="noopener noreferrer"&gt;message me on Upwork&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>atlassian</category>
      <category>jira</category>
      <category>forge</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
