We’re implementing an auth layer for our Forge Remote integration and we got caught out today by an unexpected change in the shape of license information on Forge Invocation Tokens. Previously we were adhering to the shape of the license listed on the FIT section here:
However, as of this morning UK time, it seems that an additional active property was added, similar to Connect licenses. We had written in some backwards compatibility logic for the isActive property documented above into our deserialisation of the Forge license attribute, which broke when an additional active property appeared as well. This caused a production incident.
Clearly we need to make our deserialisation logic more defensive. However, this has highlighted that there is no clear contract for the shape of the license attribute, particularly on the distinction between active and isActive. Currently I see 3 definitions:
The doc I linked above, which indicates that only isActive should be present
Another doc here which disagrees with the first doc in stating that only active should be present
The actual behaviour we’re now seeing, where both are present.
Furthermore, the change from today is not documented on the changelog, which again would’ve helped us to avoid a production incident. Please can a strong contract with regard to Forge licenses be published so that we can avoid these issues in the future?
Hi @GeorgeScott - thanks for bringing this to the community’s attention. I am chasing down more details to see if I can share what has happened here and how we can improve going forward.
This change was unintentionally missed from our changelog. A retroactive announcement will be added today. Because this change was additive, it did not get flagged as requiring a deprecation period.
I’m sorry for the pain this caused you! We’ll strive to do better.
I have tried to get clarity on that contract before as well, but we unfortunately never received a response.
I mentioned a couple of times that reading and parsing the context in a Forge or Forge Remote invocation is one of the most fragile parts of Forge apps, because SDK types are very generic and documentation often does not match the actual delivered data.
Just for clarity, does this change also apply to the context.license object available in resolver functions?
import { makeResolver } from "@forge/resolver";
interface ExampleRequest {
payload: ExamplePayload;
context: Context
}
interface ResolverDefinitions {
example: (args: ExamplePayload) => void;
}
function example({ payload, context }) { // <<<<< this context value here
const { license } = context;
// Currently in our apps we're checking license.isActive
if (license.active) {
// licensed
} else {
// unlicensed
}
}
export const handler = makeResolver<ResolverDefinitions>({ example });
The above changelog entry includes three links:
One to the documentation for useProductContext for UI kit
One to the documentation for Forge Remote Essentials for the FIT
One to the documentation for getAppContext from @forge/api (which we’re not calling)
It makes no mention of the context argument passed to resolver functions though.
Is this the same as the app context object returned from getAppContext?
In the documentation for forge resolver methods, there is Context type documented that makes no mention of the license property at all, yet we know from our experience that context.license.isActive does exist as we’re using for license checks in our backend resolvers.
At a technical level, this change didn’t apply to this context, since active was already present there. At a conceptual level, it does apply in the sense that we now have a universal LicenseDetails shape across all contexts that can be depended upon as stable, with changes subject to standard deprecation periods.
The function resolver context is constructed separately from the appContext and the productContext (because… reasons).
Here we go again. This has been one of the biggest issues I have faced with Forge, and in many ways, I still face it today.
Beyond the licensing payload changing as often as the weather, it is still unclear what the payload is actually meant to return. One piece of documentation says one thing, another says something else. There needs to be a single source of truth that clearly defines the exact response structure and behaviour of the licensing API.
Since I’m on the topic, there is another major issue with the licensing API. When an app moves from trial to paid, it is possible for a customer to activate or reactivate the app in a way that automatically extends their evaluation licence.
Because of that, I had to build my own middleware to verify whether my apps were genuinely running under an evaluation licence. If that scenario occurs, the middleware prevents further usage.
The problem is that Atlassian does not appear to have resolved this behaviour, which makes the licensing API itself difficult to rely on with confidence.
Could you clarify what happens to old versions of Forge apps? Does it only apply to latest version or are you supposed to go back and backport this to each and every major release that was done in the past?
This change also applies to old versions of Forge apps and it will be necessary to backport this change to actively supported app versions before the deprecation date on Oct 17, 2026.
Beyond the licensing payload changing as often as the weather, it is still unclear what the payload is actually meant to return. One piece of documentation says one thing, another says something else. There needs to be a single source of truth that clearly defines the exact response structure and behaviour of the licensing API.
Our intention behind this change is to ensure a consistent licence payload in all available contexts. I empathise with the lack of consistency - this is meant to be a step in the right direction.
Since I’m on the topic, there is another major issue with the licensing API. When an app moves from trial to paid, it is possible for a customer to activate or reactivate the app in a way that automatically extends their evaluation licence.
I wasn’t aware of this issue - do you have any further details (eg. an existing bug report of community post with steps to reproduce) that I can share with the Marketplace team to investigate further?
Good catch! I have updated this code snippet and also done a sanity check for other references to isActive in both the Forge and Marketplace doc repositories (all clear).
I don’t have clear steps to reproduce this issue. The only reason I became aware of it was after raising a ticket with Atlassian support partner team (CA-4005344), asking why a customer was still on an evaluation license even though their license had already expired.
I wasn’t aware that an installed app could move from an expired license back into an evaluation state after the initial trial had ended.
To answer your question directly, no, I don’t know the exact steps to reproduce it. What I do know is that a customer can reactivate an app after expiration and end up back on an evaluation license.
I’m not an expert on this, but I think there are circumstances where customers are able to abuse trial licenses in some conditions. As far as I know, there is no default behaviour where new evaluations are generated automatically for expired licenses.
There are currently some on-going investigations into reducing instances of this license abuse. In the meantime, the best course of action is to flag inappropriate use of evaluations with the licensing team, as you did in this case.
I have passed this feedback on to the Marketplace licensing team.