What is forge
What is Atlassian Forge?
Atlassian Forge is Atlassian's cloud app development platform that allows developers to build apps hosted on infrastructure that is provisioned, managed, monitored, and scaled automatically by Atlassian. In simple terms you write the code, Atlassian handles everything else.
Before platforms like Forge existed, building an app for Jira or Confluence meant setting up your own servers, managing hosting, handling authentication, and worrying about scaling. Forge removes all of that. It gives developers a complete toolkit for extending Atlassian products with hosting, multiple development environments, and API authentication all built in. The goal is to let developers focus more on building and less on infrastructure.
Forge currently supports six Atlassian products: Jira, Jira Service Management, Confluence, Bitbucket, Compass, and Rovo.
Forge apps are written in JavaScript, using Node.js LTS versions 22.x or 24.x.
Apps run inside a secure environment on Atlassian's own infrastructure. A second security layer enforces tenancy isolation and data egress restriction by design meaning one customer's data cannot reach another's. Built-in storage options include a Key-Value Store, Entity Store, and SQL database. For the UI, developers can choose between UI Kit pre-built components that match Atlassian's design system or Custom UI which runs inside an iframe for more flexibility.
Adoption Statistics
The platform has seen strong adoption over 5,600 apps built, 73,000+ CLI downloads, and 13,000+ active developers in the community.
A Brief History of How We Got to Forge
To understand why Forge exists, you need to understand what came before it. Atlassian's extensibility model went through four distinct eras, each solving the problems the previous one created.
The P1 Era: The Original Plugin System
Before P2, Atlassian products were extended using Plugin Framework 1, commonly known as P1. Plugins were installed simply by dropping a JAR file into the host application's WEB-INF/lib classpath directory.
P1 had two plugin types:
Plugin Type | How It Worked |
|---|---|
Static | Deployed to |
Dynamic | Could be installed via the web UI, available only in Confluence |
The fundamental problem with P1 was inconsistency. The capabilities and features available to a P1 plugin varied significantly across different Atlassian products. A plugin behaving one way in Jira could behave completely differently in Confluence. There was no standardised plugin layer that worked uniformly across the Atlassian ecosystem.
The P2 Era: On-Premise Plugins Done Right
To solve P1's inconsistency problem, Atlassian introduced Plugin Framework 2 (P2). P2 was built on OSGi and Spring Dynamic Modules, meaning every plugin now ran inside a standardised OSGi container regardless of which Atlassian product it was installed on.
P2 plugins were JAR files with special OSGi manifest entries. They ran directly inside the host application's JVM, giving them deep access to the application's internals. This made extensive customisation possible.
Plugin Type | Behaviour |
|---|---|
Static | Still required a full application restart, deployed to |
Dynamic | Loaded and activated without restarting the application |
Each plugin was made up of one or more plugin modules. A single plugin could do many things, while each module represented one specific function.
The fundamental limitation of P2 was simple: it only worked for local, on-premise deployments. When Atlassian began moving to the cloud, P2 could not extend cloud-based Atlassian products at all. That is why Connect was introduced in 2014.
The Connect Era: Cloud Extensibility via External Hosting
In 2014, Atlassian introduced the Connect framework. At that time, Atlassian did not even have "apps." They had "add-ons" that installed into "JIRA OnDemand," the precursor to Jira Cloud.
Connect was a significant step forward. It allowed developers to build apps hosted externally on their own infrastructure, typically on services like Heroku, AWS, Azure, or Google Cloud Platform, and connect to Atlassian products via APIs. Authentication was handled through JWT tokens and UI ran inside iframes embedded in the Atlassian product.
Connect worked well for years and powered the Atlassian Marketplace through its early growth. But it came with a fundamental problem: Atlassian had limited control over how apps were built, secured, and maintained. Every Connect app lived on a different vendor's server, with different security practices, different data handling, and different compliance postures. For enterprise customers demanding stronger guarantees, this was unacceptable.
Why Atlassian Moved to Forge
As Atlassian grew, Connect began to show its limitations. Atlassian needed a platform where security, governance, and infrastructure were consistent across every app, not fragmented across thousands of vendor servers.
Forge was the answer. As Atlassian officially stated:
"Forge is Atlassian's modern cloud app development platform that replaces Connect. It lets you build secure, scalable apps for Jira and Confluence, running on Atlassian's infrastructure for easier deployment and management."
As Atlassian put it: "When Connect reaches end of support, it will be considered 'use at your own risk.'"
The Forge Era: Atlassian Owns the Infrastructure
Forge fundamentally changed how apps are built for Atlassian products. Rather than vendors managing their own servers, Forge introduced a serverless, security-first environment where Atlassian owns and manages everything: hosting, runtime, scaling, and security.
At its core, Forge runs on AWS Lambda, a serverless FaaS platform provisioned, managed, monitored, and scaled automatically by Atlassian. Apps run inside sandboxed environments within dedicated AWS accounts, enforcing data egress restriction by design. Every external request an app makes must be declared in the app manifest. Nothing leaves Atlassian's infrastructure without explicit permission.
For developers, Forge introduced a complete toolkit:
Tool | Purpose |
|---|---|
Forge CLI | Creating, deploying, and managing apps across environments |
UI Kit | Declarative framework for building interfaces with just a few lines of code |
Custom UI | Building interfaces from scratch inside an isolated iframe |
Bridge API | JavaScript API for secure frontend-to-backend communication |
Built-in Storage | KVS, Entity Store, SQL, and Object Store, all scoped per installation |
Debugging Support | Via IntelliJ and VS Code with tunneling support |
Three principles define what Forge stands for:
Standardisation: every app follows consistent security practices, deployment methods, and permission models
Platform ownership: Atlassian manages infrastructure so vendors can focus on building features
Governance at scale: consistent performance and compliance across the entire ecosystem
How Forge Works Internally
This is the part most tutorials skip. Everyone tells you what Forge is but almost nobody explains how it actually works under the hood. All the technical details referenced below can be found in the Atlassian Forge developer documentation.
1. The Compute Layer: AWS Lambda Under the Hood
When your Forge function runs, it does not run on some generic Atlassian server. It runs on AWS Lambda, but in a carefully isolated setup described in Atlassian's security.
Atlassian does not run your code inside its own core AWS account alongside its databases and services. Instead, it provisions dedicated, low-privilege AWS accounts that are completely separated from Atlassian's operational infrastructure. Your code runs in one of these isolated accounts.
Why does this matter? If something goes wrong, such as a malicious app, a memory overflow, or a container escape, the damage is contained. The rogue process only has access to that isolated account, not to Atlassian's core systems or neighboring tenants' workloads.
Every function invocation happens inside one of these sandboxed Lambda containers, with outbound internet access blocked by default. Nothing leaves unless you explicitly declared it.
2. Runtime Evolution: From V8 Sandbox to Native Node.js
Forge did not always run on standard Node.js. Understanding the history here explains many quirks developers encounter today.
The Legacy V8 Sandbox (deprecated)
Originally, Forge isolated your code at the application interpreter level using a custom-engineered V8 JavaScript isolate sandbox. This worked like a custom runtime that mimicked Node.js 14. Every single invocation started fresh: the sandbox bootstrapped from a completely clean state, loaded your code from scratch, ran it, and discarded everything.
This sounds clean but it had serious problems:
Cold-start latency on every invocation with no warmup
128 MB memory limit, not enough for real-world apps
Most standard npm packages did not work inside the custom isolate
Async timers were killed immediately when the function returned
Atlassian fully deprecated this runtime. Apps that had not migrated by October 29, 2024 stopped working. The legacy runtime was completely disabled on February 28, 2025. Full migration steps are available in the legacy runtime migration guide. If you still have snapshots: true in your manifest.yml, remove it and set app.runtime.name: nodejs24.x.
The Native Node.js Runtime (current)
Today, Forge runs your code inside real VM-level sandboxes on standard Node.js LTS, either Node 22 or Node 24. Isolation moved from the application interpreter to the virtual machine layer.
Parameter | Legacy V8 | Native Node.js |
|---|---|---|
Node.js environment | Emulated Node.js 14 | Real Node.js 22/24 |
Memory per invocation | 128 MB | 512 MB |
Sandboxing layer | Custom V8 isolate | VM-level sandbox |
Standard npm packages | Mostly blocked | Fully supported |
Cold starts | Every invocation | Warm containers reused |
Async timers | Killed on function return | Run in background post-return |
Default Content-Type | Not set | application/json |
Performance improved by 35 to 40 percent compared to the legacy runtime, a direct result of shifting isolation from interpreter to VM level and enabling warm container reuse.
3. The Tenant Isolation Problem and How Forge Solves It
This is the most important thing to understand about Forge's runtime, and it is also where developers make the most dangerous mistakes. Atlassian's full guidance is in the tenant data isolation documentation.
When the native Node.js runtime reuses a warm Lambda container for a new invocation, the Node.js module cache is not cleared. Any variable declared at module scope, outside your handler function, retains its value from the previous call.
Consider this scenario:
Tenant A's request runs → stores data in global cache → Lambda stays warm
Tenant B's request arrives → reuses the same warm container → reads Tenant A's cached data
This is a cross-tenant data leak. It is silent. No error, no warning, just wrong data served to the wrong customer.
The tricky part: Jira issue keys like "ABC-123" are not globally unique. The same key can exist in two different tenants' Jira instances. So if you cache by issue key at module scope, you have no safe way to detect a tenant mismatch.
Atlassian guarantees that Tenant A cannot deliberately invoke Tenant B's app instance. But Atlassian does not guarantee that a warm container previously serving Tenant A will not be reused for Tenant B. That responsibility belongs to the developer.
Safe patterns verified from Atlassian docs:
Option 1: Compute everything fresh inside the handler Do not cache anything at module scope. Fetch and compute all tenant data inside the invocation handler function. Slower, but always safe.
Option 2: Partition in-memory caches by cloudId or installationId If you need module-scope caching for performance, key every cache entry by the tenant's cloudId or installationId, not by Jira issue keys or page IDs, which are not globally unique.
// Unsafe
const cache = {};
cache['ABC-123'] = issueData;
// Safe
const cache = {};
const tenantKey = `${cloudId}:ABC-123`;
cache[tenantKey] = issueData;
Option 3: Use Forge Storage (recommended) The KVS, Entity Store, and SQL modules are automatically scoped per app installation at the platform level. You cannot accidentally cross tenant boundaries through Forge Storage.
Option 4: bindInvocationContext When asynchronous operations lose their execution metadata and throw a "Forge runtime metadata not found" error, this API re-binds the async callback to the correct request context, ensuring tenant isolation is preserved during background async work.
4. Frontend Architecture: UI Kit 2 vs Custom UI
Forge gives you two ways to build UI, with fundamentally different architectures.
UI Kit 2: Declarative, Browser-Native, No ReactDOM
UI Kit 2 renders directly inside the Atlassian product's browser session. It uses @forge/react components and a custom ForgeReconciler instead of standard ReactDOM.
ForgeReconciler.render(<App />);
// NOT: ReactDOM.render() or ReactDOM.createRoot()
The reconciler bridges React's component tree directly into Atlassian's product UI with no iframe boundary and no DOM isolation. UI renders faster because clicking a button does not trigger a serverless Lambda call.
What is not supported due to the absence of a standard DOM:
Custom HTML elements
React portals
Ref forwarding to DOM nodes
External scripts
UI Kit 2 provides a rich component library including Stack, Inline, Box, Text, Button, native Comments, Interactive Charts, and platform-specific components for Jira and Confluence. It also handles internationalization across 26 languages automatically.
Custom UI: Full Flexibility Inside a Secure Iframe
Custom UI renders your compiled static assets inside an iframe hosted on an Atlassian-managed domain, completely isolated from the host product's DOM. You get full React freedom: hooks, portals, custom HTML, third-party component libraries. But there are strict rules:
All assets must be bundled locally within the app's resource path, no CDN loading
The iframe cannot load external scripts or make direct HTTP requests to third-party servers
Inline CSS styles are blocked by default unless you explicitly declare
'unsafe-inline'inpermissions.content.stylesinmanifest.ymlEvery external API call must go through a backend Forge resolver via
@forge/bridge
Hybrid Views: Embedding Custom UI Inside UI Kit
You can embed a Custom UI frame inside a UI Kit view using the <Frame> component. Since these two environments run in completely separate window contexts, they cannot share JavaScript references. Communication goes through the @forge/bridge Events API, which passes serialized payloads across the iframe boundary via the browser's secure PostMessage mechanism.
// From inside Custom UI iframe
import { events } from '@forge/bridge';
events.emit('USER_SELECTED', { userId: 'abc123' });
// From UI Kit parent
import { events } from '@forge/bridge';
events.on('USER_SELECTED', (payload) => {
console.log(payload.userId);
});
events.on returns a Subscription object so you can unsubscribe and prevent memory leaks.
5. Calling Backend Functions: The Bridge
To call backend logic, write to storage, or hit external APIs, frontend code calls backend FaaS resolvers using invoke() from @forge/bridge.
Here is the full request path when your frontend calls invoke('fetchLabels', { projectKey: 'PROJ' }):
Frontend (iframe or UI Kit browser context)
@forge/bridge bundles function key and payload
Serializes across iframe PostMessage boundary
Atlassian parent window receives it
Routes to Forge Gateway
Forge Gateway invokes the corresponding Lambda resolver
Response bubbles back through the same chain
For REST API calls, @forge/bridge 2.0 provides requestJira and requestConfluence, allowing Custom UI and UI Kit apps to call Jira and Confluence REST APIs directly from the browser. These calls execute under the current logged-in user's OAuth 2.0 context.
Important: if a user does not have permission to access a Jira endpoint, requestJira returns 403 Forbidden even if your app's manifest has the correct scopes. The user's own permission level is the binding constraint, not your app's.
For any admin-level operation, background job, or action the user should not have to authorize, you must delegate to a backend resolver using api.asApp() from @forge/api.
6. Hosted Storage: Four Engines, One Namespace
Forge provides four built-in storage options, all automatically partitioned per installation. The storage namespace is constructed by combining your app's unique ID, the deployment environment, the customer installation ID, and the host product.
Code running in a Jira context cannot access the same app's data stored in a Confluence context, even on the same tenant. Development data is physically isolated from production tables. Two different apps can never read each other's data.
Key-Value Store (@forge/kvs)
For simple key-value persistence: user preferences, app configuration, counters, feature flags.
The consistency model is split depending on how you read:
kvs.get(key): strictly consistent, always queries the primary write master, you always get the latest valuekvs.query(...): eventually consistent, reads from read replicas, data may be slightly behind
If you write a value and immediately query for it using kvs.query, you might not see the new value. Use kvs.get when freshness matters. The legacy storage module from @forge/api was deprecated as of March 17, 2025. All apps must migrate to @forge/kvs.
Custom Entity Store
For structured data with relationships, filtering, and custom querying. More powerful than raw KVS, less overhead than SQL.
Forge SQL (@forge/sql)
For complex relational data models requiring transactions, joins, and structured queries. Forge SQL runs on a self-hosted TiDB cluster configured in MySQL dialect compatibility mode. Each app installation gets its own dedicated database instance.
Resource | Limit |
|---|---|
Storage | 1 GiB |
Tables | 200 |
DML requests/second | 150 |
Per-query memory | 16 MiB |
SELECT timeout | 5 seconds |
INSERT / UPDATE / DELETE timeout | 10 seconds |
Critical constraints developers frequently hit:
No foreign key enforcement:
ON DELETE CASCADEwill not work, cascading deletes must be handled in application codeSingle query per SQL call: you cannot send multiple statements in one call
No
AUTO_INCREMENT: useAUTO_RANDOM(S,R)to distribute writes evenly across TiDB shardsStore UUIDs as
BINARY(16), not as strings, to optimize storage footprint and index performance
Object Store (Early Access)
For binary data, files, and media. Designed for large unstructured content.
7. Security: Zero-Trust Networking by Default
Forge operates on a zero-trust network model. Your app cannot make any outbound network request by default. Everything is blocked at the firewall.
To allow external communication, you must explicitly declare target domains in permissions.external in manifest.yml. At runtime, all outbound requests go through a mandatory egress proxy that inspects the destination hostname, headers, and protocol before allowing the connection.
Plain HTTP is restricted. The proxy enforces HTTPS termination. Only a predefined set of ports are allowed for outbound traffic: 80, 8080, 443, 8443, 8444, 7990, 8089, 8090, 8085, 8060.
For Custom UI, the iframe is governed by strict Content Security Policies. External scripts cannot be loaded from CDNs. Third-party tools like Google Analytics or Sentry cannot be injected. All assets must be bundled locally.
For enterprise customers with dynamic egress requirements, Forge supports Customer-Managed Egress. When permissions.external.configurable.enabled: true is set, administrators can approve specific egress routes through an approval modal without the developer needing to redeploy.
8. Forge Remote: When You Need an External Backend
For apps that need to integrate with external databases, proprietary ML models, or enterprise systems that cannot move into Forge's serverless model, Forge Remote provides a secure bridge.
There are two distinct ways to call a remote backend:
Feature | invokeRemote | requestRemote |
|---|---|---|
Proxied through Forge Gateway | Yes | No |
OAuth tokens injected | Yes | No |
FIT token included | Yes | Yes |
Metrics tracked | Yes | No |
File uploads supported | No | Yes |
Latency | Higher | Lower |
When using requestRemote, your backend must verify the Forge Invocation Token (FIT), a cryptographically signed JWT included in the Authorization header. For standard commercial environments you verify it against Atlassian's public JWKS endpoint. For Atlassian Isolated Clouds, you must dynamically construct the JWKS URL by extracting the icLabel from the unverified token and injecting it into a pre-configured URI template.
9. Event-Driven Execution: Async Triggers and Lifecycle Hooks
Forge supports three trigger types declared in the manifest:
trigger: fires on product events such as issue created or page updatedscheduled-trigger: runs on a cron schedulewebtrigger: exposes an HTTP endpoint that external systems can call
All async triggers execute under the identity of the App System User, not any logged-in human. If a Jira project or Confluence space is restricted to specific user groups, your event handler will fail unless the App System User has been explicitly granted access. All REST API calls inside event handlers must use api.asApp().
The Cascading Delete Problem
Forge does not emit events for child entities when a parent is deleted. If an admin deletes a Jira project, Forge emits exactly one event: avi:jira:deleted:project. It does not emit individual events for every issue, comment, or attachment inside that project.
If your app stores data keyed by issue ID, you will accumulate orphaned records forever unless you handle this yourself:
Maintain a local map of project to issue IDs in your persistent storage
Listen for the top-level
avi:jira:deleted:projecteventQuery your local map for all issue IDs in that project
Programmatically delete your local records for each one
10. Data Residency: Where Your Data Physically Lives
Forge Storage automatically inherits Atlassian's data residency capabilities. When an enterprise administrator pins their Jira or Confluence instance to a specific region such as EU, US, or AU, the Forge platform automatically pins your app's storage to that same region. Eleven regions are currently supported. If the customer later migrates to a different region, Forge migrates your app's stored data automatically.
For Forge Remote, you maintain compliance by declaring region-specific base URLs in your manifest:
remotes:
- key: remote-backend
baseUrl: https://global.backend.com
regionBaseUrls:
us: https://us.backend.com
eu: https://eu.backend.com
au: https://au.backend.com
The Forge routing layer reads this at installation time and routes all outbound calls to the correct regional endpoint based on the tenant's data residency setting. This keeps your app eligible for the PINNED compliance badge in the Atlassian administration interface.
Forge vs Connect : What Actually Changed
Feature | Connect | Forge |
|---|---|---|
Hosting | Vendor managed | Atlassian managed |
Authentication | JWT (developer managed) | OAuth 2.0 (Atlassian managed) |
Data location | Vendor servers | Atlassian cloud |
Security | Vendor responsibility | Atlassian enforced |
UI flexibility | Higher | Limited (UI Kit) |
Infrastructure control | More | Less |
Updates | Manual, slower | Automatic, minutes |
Where Forge Doesn't Measure Up
Forge is a development platform, but using it on a daily basis helps uncover where it falls short. The constraints that have the greatest impact on real-world development are discussed below. All numerical limits referenced here are documented on the Forge platform limits page.
Your Functions Have a Clock Tick-Tocking
Each Forge function has a hard timeout:
Standard functions: 25 seconds
Web triggers: 55 seconds
Async events and scheduled triggers: 15 minutes (900 seconds)
Forge Remote events: 5 seconds
Single outgoing request timeout (async events): 180 seconds
This seems quite generous at first, until you are trying to loop through and process tens of thousands of Jira issues or make several API calls per record. Your timer continues ticking away regardless of how quickly your API calls complete, and when the clock stops, so does your function. This pushes you to design with checkpoints from the beginning.
API Calls Add Up Much Quicker Than You Might Think
External egress requests are limited to 100 per runtime minute per invocation, rounded up to the nearest minute. When you are building applications that touch numerous Jira issues, such as a label transfer or a bulk update, each issue may require multiple API calls. Hit this limit and your app will begin throttling.
Calls to Jira and Confluence using requestJira and requestConfluence are counted as internal calls and are excluded from this budget, but calls to any external service count against this limit immediately.
Rate Limits Hit Hard at Scale
Rate limits apply at three levels:
Scope | Limit |
|---|---|
Per user on a single installation | 1,200 invocations per minute |
Per installation across all users | 5,000 invocations per minute |
Per app across all installations in an environment | 30,000 invocations per minute |
While this works fine for a small team, applications serving large organizations where many users are running operations simultaneously will find these limits become a bottleneck that needs to be addressed by design, with mechanisms like batching and queuing.
Storage Has Limits Too
The KVS documentation outlines the following per-installation limits:
Resource | Limit |
|---|---|
Value size per key | 240 KiB |
Key length | 500 characters |
Read operations per minute | 4,000 (at 10 KB request sizing) |
Write operations per minute | 4,000 (at 10 KB request sizing) |