RFC-138: Move fields out of invocation context to address header size limits for Forge Remote and Forge Containers

Hi everyone,

Project Summary

Publish: 19 June 2026

Discuss: 17 July 2026

Resolve: 30 July 2026

We’re proposing a reduction of the fields included in the signed invocation context sent to Forge Remotes and Forge Containers from frontend invocations. This is to address the issue where large context causes requests to fail before reaching the remote or container.

Problem

Today, when a Forge frontend calls invokeRemote or requestRemote, Forge sends a Forge Invocation Token (FIT) to the remote backend in the Authorization header, which includes a signed context claim. For some modules, this context can become large enough to exceed individual or combined header limit sizes in common infrastructure, causing requests to fail before they reach the app’s remote backend.

Forge Containers use the same underlying invocation model, therefore a large header carrying the context will stop requests reaching the container. A Forge frontend calls a containerised service with invokeService and the context is exposed through the /invocation/context Containers API.

No changes will occur for triggers, lifecycle events or backends connected to endpoints.

Proposed Solution

To address this, we’re proposing an opt-in manifest setting (context: trusted-only) that limits the fields in the signed invocation context sent to remotes and containers. The original context shape will continue to exist such that existing apps do not need to change.

When enabled, Forge Remote and Forge Containers would receive only trusted context in the signed invocation context.

Additional context would remain available to the app frontend through existing APIs, such as view.getContext() in Custom UI and useProductContext() in UI Kit. If your remote backend or container service needs any of that additional context, your frontend can send it explicitly in the request body.

Trusted and untrusted context

This proposal introduces a clearer distinction between two kinds of context:

  • Trusted context: product-specific context that Atlassian has permission-checked and signed into the invocation context.

  • Untrusted frontend context: additional context available to the app frontend, which can be sent to a remote backend or container service if needed, but must be treated as app-provided input and not relied on for security or authorisation decisions.

To clarify, only trusted context will now be available in the FIT context claim for Forge Remotes and through the /invocation/context API for Forge Containers. All remaining app context will be available in the frontend context.

Unfortunately, we cannot currently specify which fields are affected. Each module’s owning team must analyse and determine which fields have adequate permission checks and should legitimately remain in the signed, trusted context. These details will be published in October 2026.

What changes for app developers?

If your app does not opt in, there is no behaviour change.

If your app opts in with:

app: 
  id: ari:cloud:ecosystem::app/example-app-id
  endpoint:
    context: trusted-only // new opt-in property

some fields that were previously available in the context may no longer be present, but they will be available on the frontend context.

If your remote backend or container service needs those fields, your frontend should:

  1. Read the required values from frontend context.

  2. Send these values in the request body of the invokeRemote, requestRemote, or invokeService call.

  3. Treat them as untrusted on the backend.

  4. Validate them before use.

Example: sending frontent context

import { invokeRemote, view } from '@forge/bridge';
// import { invokeService, view } from '@forge/bridge';

export async function callRemote() {
  const context = await view.getContext();

  // same approach for invokeService()
  return await invokeRemote({
    path: '/my-remote-endpoint',
    method: 'POST',
    body: {
      frontendContext: {
        localId: context.localId,
        extension: {
          // Include only the fields your remote actually needs.
          macroParameters: context.extension?.macroParameters,
          gadgetConfiguration: context.extension?.gadgetConfiguration,
        },
      },
    }
  });
}

Example: reading trusted and untrusted context in a Forge Remote

app.post('/my-remote-endpoint', async (req, res) => {
  const fitPayload = await verifyForgeInvocationToken(
    req.header('authorization')
  );

  // Trusted: signed by Atlassian in the FIT.
  const trustedContext = fitPayload.context;

  // Untrusted: supplied by the app frontend.
  const frontendContext = req.body.frontendContext as {
    localId?: string;
    extension?: Record<string, unknown>;
  };

  if (
    frontendContext.localId !== undefined &&
    typeof frontendContext.localId !== 'string'
  ) {
    return res.status(400).json({ error: 'Invalid localId' });
  }

  // Do not use frontendContext as proof of authorization.
  // Validate or independently verify frontend-provided values before use.

  res.json({
    ok: true,
    cloudId: trustedContext?.cloudId,
    localId: frontendContext.localId,
  });
});

Example: reading trusted and untrusted context in a Forge Container

async function getInvocationContext() {
  // Use the local Containers runtime API base URL documented for your container environment.
  const response = await fetch(
    `${CONTAINERS_RUNTIME_API_BASE_URL}/invocation/context`
  );

  if (!response.ok) {
    throw new Error('Failed to read Forge Containers invocation context');
  }

  return response.json();
}

app.post('/my-container-endpoint', async (req, res) => {
  const invocationContext = await getInvocationContext();

  // Trusted: exposed by the Forge Containers runtime API after platform validation.
  const trustedContext = invocationContext.context;

  // Untrusted: supplied by the app frontend.
  const frontendContext = req.body.frontendContext as {
    localId?: string;
    extension?: Record<string, unknown>;
  };

  if (
    frontendContext.localId !== undefined &&
    typeof frontendContext.localId !== 'string'
  ) {
    return res.status(400).json({ error: 'Invalid localId' });
  }

  // Do not use frontendContext as proof of authorization.
  // Validate or independently verify frontend-provided values before use.

  res.json({
    ok: true,
    cloudId: trustedContext?.cloudId,
    localId: frontendContext.localId,
  });
});

Proposed rollout

Date Milestone
19 June 2026 Publish this RFC for community feedback.
17 July 2026 Close RFC feedback period.
31 July 2026 Resolve RFC with a final decision.
1 October 2026 Publish developer documentation about the opt-in process and which module fields will be included in the trusted context vs untrusted frontend context.
1 April 2027 forge deploy will warn if you haven’t defined the opt-in property in the manifest, but deployments will not be blocked. There are no plans to remove the original context payload but it will be considered deprecated in favour of the new context.

What we’d like feedback on

We’d especially like feedback from developers using Forge Remote or Forge Containers.

  1. Does the proposed opt-in manifest property work for your app?

  2. Which fields do your remote endpoints currently read from the FIT context claim, or your container services read from the Containers /invocation/context API and to which modules do they relate? This will help inform module owners on which fields should remain in the trusted context.

  3. Would sending non-sensitive frontend context explicitly in the request body of invokeRemote, requestRemote, or invokeService calls work for your app?

  4. Are the proposed rollout dates workable?

To reiterate, this proposal is intended to reduce failed remote and container invocations caused by oversized headers while making the trust boundary around invocation context clearer.

Please share feedback by 17 July 2026.

(First: Feel free to assign a “RFC-XXX” number, so as to align with all the other RFCs. Next sensible free number is “RFC-139”; last used number “RFC-137” is used twice, I guess one of those will be re-numbered to “RFC-138”)

On-topic:

| 1 April 2027 | Deprecate the original context payload. |

Note that there are currently no plans to remove the original context payload

So the deprecation will be indefinitely? Or how will this work?

  1. Which fields do your remote endpoints currently read from the FIT context claim?

We read:

  • context.cloudId: for analytics tracking
  • context.siteUrl: for redirecting from an external site back to the customer’s Cloud site

Will these fields continue to be in context in the reduced version? I’m unclear about which fields are included.

Would this also apply to the the Forge lifecycle events (app:installed et al)? Because the context there is kind of important to us.

Thanks @LauraHowarthKirke for taking the time to tackle this issue blocking many partners.

Taking the opt-in approach works great as we can check endpoint by endpoint to opt into the new context.

I do have a couple of question though:

  1. You mention deprecating the original context payload

What does this mean? Will the opt-in become enforced in the in 2027?

  1. What fields are considered trusted in the new context?

Currently it is not clear what product-specific context is included.

  1. How are other calls to a remote or service impacted by this RFC?

This RFC only mentions calls from the frontend to a remote/service. But there are also other invocations that will benefit from this change, like triggers and scheduledTriggers or any other module that uses endpoint to connect with a remote or service.

  1. As noted by Andreas, the RFC is inconsistent about the deprecation: it says both that no deprecation is planned for the original context payload, but also that it will be deprecated next April. Regardless, please be careful about planning any future deprecation. Customers who are using older versions of Forge app code will not necessarily upgrade due to permissions issues, so unilaterally deprecating a core platform feature can easily break old versions of apps that are unable to be updated easily.

  2. Atlassian has not defined exactly what it considers to be “trusted context”. Can the proposed fields please be spelled out for app vendors? The RFC says that this would be provided in October, but the ideal time to do this is while the RFC is open for discussion. Even if it is just a proposal of Atlassian’s current thinking.

  3. If Atlassian can make the field list clear, vendors would then need to assess how it impacts them. For example, proposals to remove values that vendors are currently relying on may result in additional round-trips required to the server (especially if this reduced context is used for triggers and so on) which increases app latency.

  4. I have heard in other places echoing the concern that data in the FIT has to be limited due to size concerns. Still, other data that would be otherwise be extremely helpful in the FIT (like data related to rolling releases) cannot be added due to the FIT size already being constrained.

Instead of adding additional limitations and band-aids to work around the problem, what about shifting the architecture and allow vendors to opt in an endpoint to sending the full FIT as part of the requestRemote() payload, rather than trying to stuff it into headers?

This permanently solves the problem of header size, it allows additional critical data to be added to the FIT if this is ever necessary, and it remains secure: the payload can be { "fit": { /* ... */ }, "unverifiedUserPayload": { /* ... */ } }.

The only downside is that it would require all requestRemote() calls to use POST (but it’s not like previous GET requests could be cached anyway, given that the FIT is always different).

Hi all,

Thanks for the quick feedback.

Below, I address some common concerns and will update the RFC description shortly.

Clarity on Deprecation

By “deprecating” the original context payload, we mean it remains usable but is not recommended because a safer alternative with clearer trust boundaries exists. Starting April 2027, forge deploy will warn if you haven’t defined the opt-in property in the manifest, but deployments will not be blocked.

We have no plans to publish a removal date for two reasons:

  1. We want to avoid breaking active but abandoned apps by forcing everyone to use the new context payload.

  2. We don’t want to block deployment of apps under development that cannot easily update (as @scott.dudley mentioned).

Affected Fields

Unfortunately, we cannot currently specify which fields are affected. Each module’s owning team must analyse and determine which fields have adequate permission checks and should legitimately remain in the signed, trusted context.

I understand this complicates evaluating the RFC, but we want to know if you approve the general approach.

Reviewing the RFC early lets you indicate which fields from the context you use, and from what module. This will guide owning teams on which fields should stay in the trusted context.

Affected Invocation Types

This change applies to context from frontend requestRemote, invokeRemote and invokeService requests only.

No changes will occur for triggers, lifecycle events or backends connected to endpoints.

@scott.dudley :

what about shifting the architecture and allow vendors to opt in an endpoint to sending the full FIT as part of the requestRemote() payload

This alternative approach is something we considered but concluded the API change for partners would be too large. We aimed to solve the issue within the existing API constraints rather than introducing a very different method.

This RFC proposes an opt-in property applying to all endpoints, so you cannot migrate endpoint by endpoint. Would you require or prefer that?

We considered defining the opt-in property per endpoint to allow incremental migration, but it would bloat the manifest. We also considered a global opt-in plus endpoint override, but theorised that most partners would likely migrate the same remote/container at once.

Big bang can work definitely, it will just make the migration process itself a bit longer but that is fine.

Will it be at app level or at remote level? By this is mean, does the opt-in work for all remotes/services that an app has, or will we be able to opt-in per remote/service?

Also, can you comment on how this change would effect non-UI modules and remote/service interactions like with trigger and scheduledTrigger modules?

The proposal is to opt-in to a new pattern, though. Why can’t the opt-in be to the approach proposed by @scott.dudley ?

I think this RFC tries to address two problems that are not directly related to each other:

  1. How to solve the header size limit
  2. How to improve the context contract, including the distinction between trusted/untrusted contexts

Generally, I share the concern highlighted above that the proposed solution is a band-aid solution.

Header size limit problem

Moving the context to the request body seems to be a reasonable, long-term solution. Removing untrusted context from the FIT seems to just defer the problem. I do not quite understand why this would solve it. How would Atlassian ensure that no single module fills it with excessively large trusted context data?

The biggest impact of this change is that the remote may no longer receive some of the context it previously did. Whether the Remote is reading the body or FIT context doesn’t make much of a difference.

I think it is totally reasonable for Atlassian to reserve some of the remote request body for its own use (with a clear contract! see below) to pass platform context to a Remote/Container app, rather than relying solely on headers.

Trusted/untrusted context distinction

This is a welcome change, but “trusted vs untrusted context” is part of a larger problem: a generally vague contract between Remotes/Functions and the Forge platform.

If Atlassian does not clearly know today which fields in the context are trusted, how are we supposed to build a secure Remote/Function?

From a developer’s perspective, context is a black box. For Atlassian teams, it appears to be a container to provide anything that may be required/useful for an invocation. There is no clear contract around it other than “an object of data”. Forge module context documentation is often wrong and/or incomplete.

Context is fragile and needs rethinking ([1], [2], [3]). I do not know what the platform implementation looks like, but here are just some ideas on how this may be improved:

  • Treat the boundary as an interface that needs to be specified the same way as Atlassian uses OpenAPI spec to define the contract for REST APIs.
  • Atlassian teams define the context shape for a module as a JSON schema and publish that with the module to a centralized location
  • Typescript types for each module’s context can be derived from the JSON schema.
  • Module context documentation can be generated from the published JSON schemas
  • Teams can mark trusted vs untrusted context directly in the JSON schema, maybe even with additional details.

I am aware that these suggestions go far beyond this RFC, but I think it’s a problem that is being ignored and needs to be addressed.

Focused RFC questions/feedback:

We currently use these fields in Remotes:

FIT context JSON source Forge module type(s) in our code path
context.extension.issue.id jira:issueContext
context.extension.project.id jira:issueContext, jira:projectSettingsPage
context.extension.request.key jiraServiceManagement:portalRequestDetailPanel
context.siteUrl core:trigger, jira:workflowPostFunction

I strongly believe the fix for this should be the same across all modules and invocations. Atlassian should work to unify the contract rather than fragment it further.

Will it be at app level or at remote level? By this is mean, does the opt-in work for all remotes/services that an app has, or will we be able to opt-in per remote/service?

App level: applies to all remotes (invokeRemote and requestRemote) and services (invokeService).

Also, can you comment on how this change would effect non-UI modules and remote/service interactions like with trigger and scheduledTrigger modules?

It applies only to UI modules because backend invocations carry little or no context, not causing the header size issue.

Thanks for the details of your usage.

I think this RFC tries to address two problems that are not directly related to each other:

  1. How to solve the header size limit

  2. How to improve the context contract, including the distinction between trusted/untrusted contexts

You’re right - this RFC mainly addresses the header size limit and, as a side effect, aims to clarify the contract.

Moving the context to the request body seems to be a reasonable, long-term solution.

As mentioned above, we considered this approach. Beyond deviating from the current interface, the main issue is that it works only for invokeRemote and invokeService, which use JSON requests and responses. The FIT can’t be added automatically to the body of requestRemote because the developer sets its Content-Type.

How would Atlassian ensure that no single module fills it with excessively large trusted context data?

While implementing this RFC, we will limit the size of the trusted context transmitted through the platform instead of leaving it unbounded as it is today.

From a developer’s perspective, context is a black box. For Atlassian teams, it appears to be a container to provide anything that may be required/useful for an invocation.

Indeed - this is largely a product of organic platform growth and we acknowledge it’s difficult to work with. I agree with your suggested direction; the challenge is that delivering it involves coordinating dozens of teams, so it’s not something we can change quickly.

Thank you all for the detailed feedback. We hear your concerns about:

  1. A need for a well-defined contract for context.

  2. A worry that this is a band-aid solution and not a root cause fix.

  3. Though header size isn’t an issue for backend invocations (including triggers and lifecycle events), you want a standardised way to receive context for all invocation types, not only frontend APIs.

The suggested alternative approach

We reconsidered moving the FIT from the header to the body, but it isn’t feasible for all APIs. Because invokeRemote and invokeService use JSON, the platform can add the FIT to the body automatically. requestRemote doesn’t allow this, since the developer controls the Content-Type.

What’s next?

The RFC is still open for comments until 17th July 2026.

Any feedback around which fields from the context you use, from what modules and which API you’re using would be much appreciated.

This isn’t a final decision. We’ll take all your feedback on board and consider how best to proceed, and will let you know by 31st July.

HI @LauraHowarthKirke

Is there no way Atlassian could tweak requestRemote to make this work, allowing for harmony across all three methods?

In the scenario that the vendor has opted in to the new changes (meaning nothing changes for existing users):

What about providing a helper function that generates a FIT and allowing the developer to integrate it into the requestRemote payload as they deem appropriate?

If Atlassian requires this to be done synchronously for whatever reason, what about allowing the vendor to specify a callback in the requestRemote call, with the callback receiving the FIT and request body, and allowing it to return a new request body as needed?

Or even just require the vendor to use a specific Content-Type if this feature is opted in? If using FormData, you can hardcode one MIME part for the FIT with a Content-Type of Atlassian’s choice.

I am trying to imagine any possible security issues here, but I do not see them: an app can already design a back-end remote that simply returns the full FIT to the invoker, so I am not sure of how having direct FIT access from the front end would change anything.

What about providing a helper function that generates a FIT and allowing the developer to integrate it into the requestRemote payload as they deem appropriate?

If Atlassian requires this to be done synchronously for whatever reason, what about allowing the vendor to specify a callback in the requestRemote call, with the callback receiving the FIT and request body, and allowing it to return a new request body as needed?

Thank you for your ideas. Something like this could work, but we need to assess whether it is the correct direction for the platform.

Or even just require the vendor to use a specific Content-Type if this feature is opted in? If using FormData, you can hardcode one MIME part for the FIT with a Content-Type of Atlassian’s choice.

I’m confident though that we wouldn’t take this approach as it would fragment the APIs too much.

Rest assured we’ll take all your feedback into account and update you in the coming weeks.

Hope you have a lovely weekend :slight_smile: