{"id":50325738,"url":"https://github.com/forge-sql-orm/atlassian-runtime-bridge","last_synced_at":"2026-05-29T06:02:48.870Z","repository":{"id":357907590,"uuid":"1235465009","full_name":"forge-sql-orm/atlassian-runtime-bridge","owner":"forge-sql-orm","description":"Spring Boot runtime bridge for Atlassian Forge Remote and Forge Containers with legacy Connect compatibility.","archived":false,"fork":false,"pushed_at":"2026-05-22T12:20:54.000Z","size":313,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-22T14:59:51.437Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/forge-sql-orm.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-11T10:56:28.000Z","updated_at":"2026-05-22T12:20:30.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/forge-sql-orm/atlassian-runtime-bridge","commit_stats":null,"previous_names":["forge-sql-orm/atlassian-runtime-bridge"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/forge-sql-orm/atlassian-runtime-bridge","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/forge-sql-orm%2Fatlassian-runtime-bridge","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/forge-sql-orm%2Fatlassian-runtime-bridge/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/forge-sql-orm%2Fatlassian-runtime-bridge/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/forge-sql-orm%2Fatlassian-runtime-bridge/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/forge-sql-orm","download_url":"https://codeload.github.com/forge-sql-orm/atlassian-runtime-bridge/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/forge-sql-orm%2Fatlassian-runtime-bridge/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33639056,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-29T02:00:06.066Z","response_time":107,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-05-29T06:02:47.375Z","updated_at":"2026-05-29T06:02:48.836Z","avatar_url":"https://github.com/forge-sql-orm.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# atlassian-runtime-bridge\n\n![Maven Central Version](https://img.shields.io/maven-central/v/com.github.vzakharchenko/atlassian-runtime-bridge)\n![GitHub License](https://img.shields.io/github/license/forge-sql-orm/atlassian-runtime-bridge)\n![GitHub Release](https://img.shields.io/github/v/release/forge-sql-orm/atlassian-runtime-bridge)\n\n\n[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=forge-sql-orm_atlassian-runtime-bridge\u0026metric=alert_status)](https://sonarcloud.io/summary/new_code?id=forge-sql-orm_atlassian-runtime-bridge)\n[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=forge-sql-orm_atlassian-runtime-bridge\u0026metric=bugs)](https://sonarcloud.io/summary/new_code?id=forge-sql-orm_atlassian-runtime-bridge)\n[![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=forge-sql-orm_atlassian-runtime-bridge\u0026metric=code_smells)](https://sonarcloud.io/summary/new_code?id=forge-sql-orm_atlassian-runtime-bridge)\n[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=forge-sql-orm_atlassian-runtime-bridge\u0026metric=coverage)](https://sonarcloud.io/summary/new_code?id=forge-sql-orm_atlassian-runtime-bridge)\n\n[![Maintainability](https://qlty.sh/gh/forge-sql-orm/projects/atlassian-runtime-bridge/maintainability.svg)](https://qlty.sh/gh/forge-sql-orm/projects/atlassian-runtime-bridge)\n[![Code Coverage](https://qlty.sh/gh/forge-sql-orm/projects/atlassian-runtime-bridge/coverage.svg)](https://qlty.sh/gh/forge-sql-orm/projects/atlassian-runtime-bridge)\n\n[![Codacy Badge](https://app.codacy.com/project/badge/Grade/ee719b11f0c5447fbe547f93168a34ba)](https://app.codacy.com/gh/forge-sql-orm/atlassian-runtime-bridge/dashboard?utm_source=gh\u0026utm_medium=referral\u0026utm_content=\u0026utm_campaign=Badge_grade)\n\n[![LoC (full)](https://raw.githubusercontent.com/forge-sql-orm/atlassian-runtime-bridge/badges/loc-full.svg)](https://github.com/forge-sql-orm/atlassian-runtime-bridge/tree/badges)\n[![LoC (src)](https://raw.githubusercontent.com/forge-sql-orm/atlassian-runtime-bridge/badges/loc-src.svg)](https://github.com/forge-sql-orm/atlassian-runtime-bridge/tree/badges)\n![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/forge-sql-orm/atlassian-runtime-bridge)\n\n**One Spring codebase** for Connect, Forge Remote, and Forge Containers — product APIs go through shared adapters (`JiraProductAdapter`, …); the bridge picks Connect JWT, Forge tokens, or the container egress sidecar from `SecurityContext`.\n\n| You are building… | Maven artifact | Sample module |\n|-------------------|----------------|---------------|\n| Connect iframe **and/or** Forge Remote (hybrid or migrated) | [`bridge-forge-connect`](#maven-coordinates) | [`forge-connect/`](examples/atlassian-connect-forge-spring-boot-sample/forge-connect/) → then [`forge-remote/`](examples/atlassian-connect-forge-spring-boot-sample/forge-remote/) |\n| Forge Containers (isolated cloud) | [`bridge-connect-container`](#forge-containers-bridge-connect-container) | [`forge-container/`](examples/atlassian-connect-forge-spring-boot-sample/forge-container/) |\n\n**Working end-to-end example** (Connect + Forge Remote + Containers in one repo): **[`examples/atlassian-connect-forge-spring-boot-sample`](examples/atlassian-connect-forge-spring-boot-sample/)** — see its [README](examples/atlassian-connect-forge-spring-boot-sample/README.md) for install/run.\n\nDo **not** put `bridge-forge-connect` and `bridge-connect-container` on the same classpath.\n\nBuilt against **Atlassian Connect Spring Boot 6.x** and **Spring Boot 3.5.x** (root `pom.xml`).\n\n---\n\n## Migration: Connect → Forge Remote\n\nTarget manifest shape: [`forge-remote/manifest.yml`](examples/atlassian-connect-forge-spring-boot-sample/forge-remote/manifest.yml) (Forge modules only, no `connectModules`). Hybrid stepping stone: [`forge-connect/manifest.yml`](examples/atlassian-connect-forge-spring-boot-sample/forge-connect/manifest.yml).\n\n### 1. Tenant keys: `cloudId` instead of `clientKey`\n\nMove **your** persistence off `AtlassianHost.clientKey`:\n\n- Internal entities and repositories should use **`cloudId`** as the tenant key.\n- There must be **no FK or JPA relation** from your tables to Connect’s `AtlassianHost` entity — that table is Connect lifecycle metadata, not your domain model.\n\n**Transitional pattern** (while some sites still exist only on native Connect):\n\n| Column / lookup | Purpose |\n|-----------------|--------|\n| `cloudId` | Primary tenant id going forward |\n| `clientKey` | Legacy id during migration only |\n\nLookup order: **`cloudId` first**, then **`clientKey`** if missing (for the rare native-Connect-only installs). A small mapping table `(cloud_id, client_key, installation_id)` is enough; drop `client_key` from app queries once all tenants are on Forge.\n\nThe bridge can still **read** Connect’s host row via `installationId` for hybrid enrichment ([`ConnectOnForgeContext`](#built-in-connect-row-by-installationid)) — that is separate from your entity model.\n\n### 2. Add `bridge-forge-connect`\n\n```xml\n\u003cdependency\u003e\n    \u003cgroupId\u003ecom.github.vzakharchenko\u003c/groupId\u003e\n    \u003cartifactId\u003ebridge-forge-connect\u003c/artifactId\u003e\n    \u003cversion\u003e1.0.2\u003c/version\u003e\n\u003c/dependency\u003e\n```\n\nKeep Connect starters on the classpath during hybrid; use `@SpringBootApplication` only — the bridge loads from `AutoConfiguration.imports` ([details](#enabling-the-bridge-in-your-spring-boot-app-hybrid--forge-remote)). Set **`app.id`** to the Forge manifest `app.id`.\n\nRefactor services to call **`JiraProductAdapter`** / **`ManualAuthorizationService`** instead of branching on Connect vs Forge.\n\n### 3. Two frontend builds (Connect iframe vs Forge Custom UI)\n\nShip **two bundles** (or one repo with a **platform alias** — same idea as [this community post](https://community.developer.atlassian.com/t/disable-forge-tunnel/96807/10?u=vzakharchenko)):\n\n- **Connect** — iframe / `AP.request` transport.\n- **Forge** — Custom UI + `@forge/bridge` transport.\n\nThe sample uses **esbuild aliases** (`app-transport` → `connect.ts` | `forge.ts` | `container.ts`):\n\n```json\n\"build:connect\": \"esbuild ... --alias:app-transport=./.../connect.ts\",\n\"build:forge\": \"esbuild ... --alias:app-transport=./.../forge.ts\"\n```\n\nSee [`frontend/package.json`](examples/atlassian-connect-forge-spring-boot-sample/frontend/package.json). Output: Connect `bundle.js` on the classpath; Forge `customUI/` for `forge deploy` (see [`forge-remote/manifest.yml`](examples/atlassian-connect-forge-spring-boot-sample/forge-remote/manifest.yml) `resources`).\n\n### 4. Forge manifest: add Forge modules, remove Connect modules\n\nIn the manifest you deploy:\n\n- **Add** Forge `modules` (`jira:globalPage`, `endpoint`, `scheduledTrigger`, `trigger`, `remotes`, …) pointing at your Spring **`baseUrl`**.\n- **Remove** `connectModules` (lifecycle, `generalPages`, …) when you no longer serve Connect UI from the descriptor.\n\nCompare hybrid vs Forge-only in the sample:\n\n| | Hybrid | Forge Remote |\n|---|--------|--------------|\n| Manifest | [`forge-connect/manifest.yml`](examples/atlassian-connect-forge-spring-boot-sample/forge-connect/manifest.yml) | [`forge-remote/manifest.yml`](examples/atlassian-connect-forge-spring-boot-sample/forge-remote/manifest.yml) |\n| `connectModules` | Present | **Absent** |\n| `app.connect.remote` | `connect` | `forge-remote` |\n\n### 5. Deploy and cut over\n\n1. `mvn clean install` (builds frontend + Spring).\n2. Run Spring from **`forge-connect/`** (same JVM serves both transports until Connect UI is retired).\n3. `forge deploy` from **`forge-remote/`** when ready; `forge install --upgrade`.\n4. Retire Connect iframe traffic; you are on **Forge Remote**.\n\n---\n\n## Migration: Forge Remote → Forge Containers\n\nHarder path — only after **Forge Remote** works. Target: [`forge-container/manifest.yml`](examples/atlassian-connect-forge-spring-boot-sample/forge-container/manifest.yml) (`services`, container image, egress sidecar). Runbook: [`forge-container/README.md`](examples/atlassian-connect-forge-spring-boot-sample/forge-container/README.md).\n\n### 1. Complete Connect → Forge Remote first\n\nContainers assume Forge invocation context and platform routing, not Connect iframe JWT + public `remotes.baseUrl` as the primary runtime.\n\n### 2. Swap the bridge module\n\n| Remove | Add |\n|--------|-----|\n| `bridge-forge-connect` | `bridge-connect-container` |\n\nSeparate Spring module or profile: **`forge-container/`** in the sample. Entry point scans container auto-config only ([`AddonApplication`](examples/atlassian-connect-forge-spring-boot-sample/forge-container/src/main/java/sample/connect/spring/atlaskit/AddonApplication.java)).\n\n### 3. Replace the data layer\n\nForge Containers run in **isolated cloud** — no Connect JPA host table on the classpath (auto-excluded). Migrate persistence off **Hibernate/JDBC** to platform storage, for example:\n\n- [Forge SQL](https://developer.atlassian.com/platform/forge/storage-reference/sql/) (HTTP SQL API), or\n- Forge **KVS** / other Forge storage modules.\n\nTenant identity remains **`cloudId`** / **`installationId`** from invocation context or webtrigger query params — not `clientKey`.\n\n### 4. Container image, manifest, deploy\n\n1. `forge containers create -k \u003ccontainer-key\u003e` (sample: `java-service`).\n2. Build Custom UI: `npm run build:container` → `forge-container/customUI/`.\n3. Docker image from repo root → push to Forge ECR → `forge variables set TAG` → `forge deploy`.\n4. Local loop: Spring on host + proxy sidecar + `forge tunnel` ([`dev-loop.sh`](examples/atlassian-connect-forge-spring-boot-sample/forge-container/dev-loop.sh)).\n\nYou now run on **Forge Containers** and **isolated cloud**.\n\n---\n\n## Modules\n\n| Artifact | Role |\n|----------|------|\n| **`bridge-common`** | Shared API: `JiraProductAdapter`, `ConfluenceProductAdapter`, `OtherProductAdapter`, `ManualAuthorizationService`, `AtlassianHostContextEnricher`. |\n| **`bridge-forge-connect`** | Hybrid / **Forge Remote**: security bridge, Forge filter, select adapters, optional Connect host lookup by `installationId`. |\n| **`bridge-connect-container`** | **Forge Containers**: ingress headers, egress sidecar, container Jira adapter; excludes Connect/JPA auto-config. |\n\n## Maven coordinates\n\n**Hybrid / Forge Remote:**\n\n```xml\n\u003cdependency\u003e\n    \u003cgroupId\u003ecom.github.vzakharchenko\u003c/groupId\u003e\n    \u003cartifactId\u003ebridge-forge-connect\u003c/artifactId\u003e\n    \u003cversion\u003e1.0.2\u003c/version\u003e\n\u003c/dependency\u003e\n```\n\n**Forge Containers:**\n\n```xml\n\u003cdependency\u003e\n    \u003cgroupId\u003ecom.github.vzakharchenko\u003c/groupId\u003e\n    \u003cartifactId\u003ebridge-connect-container\u003c/artifactId\u003e\n    \u003cversion\u003e1.0.2\u003c/version\u003e\n\u003c/dependency\u003e\n```\n\nYou still need Atlassian Connect Spring Boot artifacts on the classpath where noted below (see the [sample](examples/atlassian-connect-forge-spring-boot-sample/) `pom.xml`). For **Connect-persisted host rows** in hybrid apps, add **`atlassian-connect-spring-boot-jpa-starter`** and a datasource — the forge bridge registers an extra repository only when Connect’s JPA auto-configuration is present (see [Connect host persistence](#connect-host-persistence-jpa)). **Container apps do not use JPA** — the container module excludes datasource/JPA auto-configurations automatically.\n\n---\n\n## Library reference\n\n### Enabling the bridge in your Spring Boot app (hybrid / Forge Remote)\n\n1. Add the dependency above (plus Connect starter, JPA if you persist hosts/tokens, web, etc.).\n2. Use a normal **`@SpringBootApplication`** in your app package. **`bridge-forge-connect`** registers via **`META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`** — no mandatory `@ComponentScan` on the bridge.\n\n```java\n@SpringBootApplication\npublic class MyApplication { /* ... */ }\n```\n\nOptional: anchor component scan on **`AtlassianConnectForgeAutoConfiguration`** if you want your application class in a different package than your controllers but still scan them together (do not scan the bridge class *in addition* to the auto-config import — that duplicates beans).\n\n3. Set **`app.id`** to your Forge manifest `app.id` (used when reconstructing `ForgeApp` metadata in `AtlassianForgeSecurityBridgeServiceImpl`).\n\n**`AtlassianConnectForgeAutoConfiguration`** (`@AutoConfigureAfter` Connect) keeps the stock Connect auto-configuration and adds:\n\n- **`AtlassianForgeFilter`** — runs after Forge auth and replaces `ForgeAuthentication` with one backed by a resolved `AtlassianHostUser` when possible.\n- **`graphqlClient`** — shared `RestClient` for `https://api.atlassian.com/graphql` (offline user token / impersonation).\n\n**`AtlassianConnectForgeJpaAutoConfiguration`** is a separate auto-configuration entry, active when Connect’s JPA starter is on the classpath (see below).\n\n### Configuration classes\n\n| Class | When active | Role |\n|-------|-------------|------|\n| **`AtlassianConnectForgeAutoConfiguration`** | Always (via `AutoConfiguration.imports`) | Filter, GraphQL `RestClient`, bridge component scan. |\n| **`AtlassianConnectForgeJpaAutoConfiguration`** | Only if `com.atlassian.connect.spring.internal.jpa.AtlassianJpaAutoConfiguration` is on the classpath (Connect JPA starter) | Adds **`AtlassianHostByInstallationIdRepository`** via `@EnableJpaRepositories` **after** Connect’s own JPA config — no `@Primary`, no changes to `AtlassianHostRepository`. |\n\n### Calling Jira, Confluence, or generic product REST\n\nDefine dependencies on the **adapter interfaces** from `common` (`JiraProductAdapter`, …). The **forge** module registers **select** beans (`JiraProductSelectAdapter`, …) that:\n\n- If the current `SecurityContext` holds **`ForgeAuthentication`**, delegate to Forge `AtlassianForgeRestClients`-backed implementations.\n- Otherwise delegate to classic Connect `AtlassianHostRestClients`-backed code paths.\n\nSo the same service layer can run in Connect-only, Forge-only, or hybrid deployments without branching on product type everywhere.\n\n### Security bridge and manual authorization\n\n**`AtlassianForgeSecurityBridgeService`** maps Forge invocation / persisted system tokens into the same `AtlassianHost`, `AtlassianHostUser`, and Spring `Authentication` types Connect Spring already uses, and can build `ForgeAuthentication` for programmatic use.\n\n**`ManualAuthorizationService`** sets that authentication on **`SecurityContextHolder`**. Use it whenever product adapters must run **outside** a normal Forge/Jira request thread (schedulers, queues, outbound webhooks, etc.), because the select adapters key off the security context.\n\nTypical pattern: run work on a thread with a clean or known context, call `authorize(...)`, invoke the adapters, then **clear the security context** when the task ends (especially for thread pools).\n\n`ManualAuthorizationService` overloads:\n\n- `authorize(AtlassianHostUser)` / `authorize(AtlassianHost)` when you already have full host objects.\n- `authorize(String cloudId, String installationId, Optional\u003cString\u003e accountId)` for a **minimal** host built from ids (non-null `cloudId` / `installationId`; optional non-blank account id for user-scoped REST). Populate extra host fields yourself if a given API needs them.\n\n### Host enrichment from Forge context\n\nWhen resolving an `AtlassianHost` from a live **`ForgeApiContext`**, **`AtlassianForgeSecurityBridgeServiceImpl`** builds a **minimal** host (`installationId`, `cloudId`, `clientKey`, `addonInstalled`) from the Forge invocation token, then runs every registered **`AtlassianHostContextEnricher\u003cForgeApiContext\u003e`** in ascending **`order()`** (lower runs first).\n\n### Built-in: Connect row by `installationId`\n\nIf you use Connect’s host table (JPA), the forge module registers **`ConnectOnForgeContext`** automatically when Connect’s **`AtlassianJpaAutoConfiguration`** is present. It:\n\n1. Looks up the row with **`AtlassianHostByInstallationIdRepository.findByInstallationId(...)`** (Spring Data derived query; **not** on stock `AtlassianHostRepository`, which is keyed by `clientKey`).\n2. **Merges** the persisted row with the minimal Forge host via **`AtlassianHostMerge`**: Connect columns (`sharedSecret`, `baseUrl`, entitlements, audit fields, …) come from the database; non-null scalar fields from the invocation token overlay the copy (e.g. fresh **`cloudId`**). The JPA entity returned from the repository is **not** mutated.\n\n**`ConnectOnForgeContext.CONNECT_LOOKUP_ORDER`** is `1`, so this enricher runs before custom enrichers that keep the default **`order()`** (`Integer.MAX_VALUE`).\n\nWithout JPA / without a matching row, the minimal host is unchanged and the bridge still works for Forge-only flows.\n\n### Custom enrichers\n\nImplement **`AtlassianHostContextEnricher\u003cForgeApiContext\u003e`** in `common` and register it as a Spring bean if you need more than the Connect table (custom flags, external tenant registry, per-tenant config, etc.). The enricher receives the host produced by the previous step in the chain and returns the next one — so a custom bean can **fully replace** the `AtlassianHost`, copy fields from any source, or attach a richer subclass (see below). Override **`order()`** when your logic must run before or after **`ConnectOnForgeContext`** (which uses `order() = 1`).\n\n```java\n@Component\npublic class TenantConfigEnricher implements AtlassianHostContextEnricher\u003cForgeApiContext\u003e {\n\n  private final TenantConfigRepository tenants;\n\n  public TenantConfigEnricher(TenantConfigRepository tenants) {\n    this.tenants = tenants;\n  }\n\n  @Override\n  public int order() {\n    return 100; // after ConnectOnForgeContext (1), before defaults (Integer.MAX_VALUE)\n  }\n\n  @Override\n  public Optional\u003cAtlassianHost\u003e update(\n      Optional\u003cAtlassianHost\u003e host, Optional\u003cForgeApiContext\u003e ctx) {\n    return host.map(h -\u003e {\n      h.setBaseUrl(tenants.resolveBaseUrl(h.getInstallationId())); // or build a fresh instance\n      return h;\n    });\n  }\n}\n```\n\n### Extending `AtlassianHost` with your own fields\n\n`com.atlassian.connect.spring.AtlassianHost` is **not `final`** — you can subclass it and carry extra state (feature flags, tenant tier, cached entitlement details, etc.) through the same `AtlassianHostUser` / security context the bridge already propagates. Return your subclass from an enricher and downstream code (product adapters, custom `@Service` beans) can cast or call typed helpers on it:\n\n```java\npublic class TenantAwareHost extends AtlassianHost {\n  private String tier;          // e.g. \"free\" / \"standard\" / \"premium\"\n  private Set\u003cString\u003e features; // tenant-level feature toggles\n\n  public String getTier()              { return tier; }\n  public void setTier(String tier)     { this.tier = tier; }\n  public Set\u003cString\u003e getFeatures()     { return features; }\n  public void setFeatures(Set\u003cString\u003e f) { this.features = f; }\n}\n\n@Component\npublic class TenantAwareHostEnricher implements AtlassianHostContextEnricher\u003cForgeApiContext\u003e {\n\n  @Override\n  public Optional\u003cAtlassianHost\u003e update(\n      Optional\u003cAtlassianHost\u003e host, Optional\u003cForgeApiContext\u003e ctx) {\n    return host.map(base -\u003e {\n      TenantAwareHost enriched = new TenantAwareHost();\n      // copy whatever Connect / ConnectOnForgeContext already filled in:\n      enriched.setClientKey(base.getClientKey());\n      enriched.setInstallationId(base.getInstallationId());\n      enriched.setCloudId(base.getCloudId());\n      enriched.setBaseUrl(base.getBaseUrl());\n      enriched.setSharedSecret(base.getSharedSecret());\n      // add your own:\n      enriched.setTier(lookupTier(base.getInstallationId()));\n      enriched.setFeatures(lookupFeatures(base.getInstallationId()));\n      return enriched;\n    });\n  }\n}\n```\n\nNotes when subclassing:\n\n- **Don't mutate JPA-managed instances.** `ConnectOnForgeContext` already guards against this via `AtlassianHostMerge`; if your enricher receives a host that came from `AtlassianHostRepository`, build a new object instead of calling setters on the input.\n- **Persisting your subclass is your responsibility.** Connect Spring's `AtlassianHostRepository` stores the base `AtlassianHost` columns only — extra fields on your subclass live in memory unless you persist them yourself (separate table, your own JPA entity, cache, etc.).\n- **Downstream code reaches your fields via `(TenantAwareHost) hostUser.getHost()`** (or an `instanceof` pattern). The bridge's select adapters do not care about the concrete type — they only read the standard Connect fields.\n\n### Connect host persistence (JPA)\n\nConnect’s **`AtlassianJpaAutoConfiguration`** already enables **`AtlassianHostRepository`**, **`AtlassianHostMappingRepository`**, and **`ForgeSystemAccessTokenRepository`**.\n\nThe bridge adds a **separate** repository interface:\n\n- **`AtlassianHostByInstallationIdRepository`** — `Optional\u003cAtlassianHost\u003e findByInstallationId(String installationId)`\n\nIt is enabled by **`AtlassianConnectForgeJpaAutoConfiguration`**, which is conditional on the Connect JPA auto-configuration class name (string-based `@ConditionalOnClass` / `@AutoConfigureAfter`, so the bridge JAR does not require that class at compile time for consumers that omit JPA).\n\n**`ConnectOnForgeContext`** injects `Optional\u003cAtlassianHostByInstallationIdRepository\u003e`: if the bean is absent, enrichment from the host table is skipped.\n\n### Forge Containers (`bridge-connect-container`)\n\nUse this module when your Spring Boot app runs **inside a Forge Container** (platform routes traffic to your image; Jira/Confluence REST goes through the **egress proxy sidecar**, not Connect host REST clients or a public `remotes.baseUrl`).\n\nOfficial reference: [Forge Containers](https://developer.atlassian.com/platform/forge/containers-reference/).\n\n### How it differs from `bridge-forge-connect`\n\n| | **`bridge-forge-connect`** | **`bridge-connect-container`** |\n|---|---------------------------|--------------------------------|\n| Traffic | Browser / Forge Remote → your HTTPS URL | Forge → your container; sidecar on `FORGE_EGRESS_PROXY_URL` |\n| Jira REST | Connect `AtlassianHostRestClients` or Forge `AtlassianForgeRestClients` (select adapters) | Always via egress proxy (`/jira/...`) |\n| Ingress auth | Connect JWT + `AtlassianForgeFilter` | `x-forge-invocation-id` → sidecar `/invocation/context` |\n| Connect JPA / host table | Optional enrichment | **Excluded** (no datasource) |\n| Spring registration | Boot **`AutoConfiguration.imports`** | Boot **`AutoConfiguration.imports`** + environment post-processor |\n\n### Enabling in your app\n\n1. Add **`bridge-connect-container`** and **`spring-boot-starter-web`**.\n2. Add **`atlassian-connect-spring-boot-core`** only (types for `ForgeAuthentication`, `AtlassianHost`, …) — **not** `atlassian-connect-spring-boot-starter`, **not** `atlassian-connect-spring-boot-jpa-starter`, **not** `bridge-forge-connect`.\n3. Anchor component scan on the container auto-configuration (same pattern as hybrid, different anchor class):\n\n```java\n@SpringBootApplication\n@ComponentScan(basePackageClasses = {\n        com.github.vzakharchenko.runtime.bridge.containers\n                .AtlassianConnectForgeContainerAutoConfiguration.class,\n        MyApplication.class\n})\npublic class MyApplication { /* ... */ }\n```\n\n**`AtlassianConnectForgeContainerAutoConfiguration`** is also listed in `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`, so beans load even without scan; scanning the class keeps your app and bridge packages aligned (see the [sample `AddonApplication`](examples/atlassian-connect-forge-spring-boot-sample/forge-container/src/main/java/sample/connect/spring/atlaskit/AddonApplication.java)).\n\nOn startup, **`ContainerAutoConfigurationEnvironmentPostProcessor`** merges **`spring.autoconfigure.exclude`** so Connect web/JPA, JDBC, Liquibase, Quartz, and Redis auto-configurations do not start in a typical container deployment (see **`ContainerExcludedAutoConfigurations`**).\n\n### Configuration\n\n| Property / env | Default | Purpose |\n|----------------|---------|---------|\n| **`egress.proxy.url`** | `http://localhost:7072` | Base URL of the Forge **egress sidecar** (local docker-compose or platform-injected `FORGE_EGRESS_PROXY_URL` in cloud) |\n| **`FORGE_EGRESS_PROXY_URL`** | — | Often set in `application.yaml` as `${FORGE_EGRESS_PROXY_URL:http://localhost:7072}` (see [sample `application.yaml`](examples/atlassian-connect-forge-spring-boot-sample/forge-container/src/main/resources/application.yaml)) |\n| **`app.id`** | — | Forge manifest `app.id` when building `ForgeApp` metadata in manual authorization paths |\n| **`bridge.container.security.public-paths`** | `[/health]` | Ant patterns exempt from HTTP security auth (health, etc.); other paths need `ContainerAuthorizationFilter` or manual auth |\n\nExample `application.yaml`:\n\n```yaml\negress:\n  proxy:\n    url: ${FORGE_EGRESS_PROXY_URL:http://localhost:7072}\n\nbridge:\n  container:\n    security:\n      public-paths:\n        - /health\n        - /actuator/health/**\n        - /api/public/**\n```\n\n**Local dev:** run Spring on the host (`mvn spring-boot:run`, port **8080**), start the platform **proxy sidecar** (ports **7071** / **7072**), then `forge tunnel`. Full steps: **[`forge-container/README.md`](examples/atlassian-connect-forge-spring-boot-sample/forge-container/README.md)**.\n\n**Cloud deploy:** build the image from the **repository root** (`forge-container/Dockerfile`), push to Forge ECR (`forge containers create` + `docker push`), set Forge variable **`TAG`**, `forge deploy`. Use [`build-and-deploy.sh.example`](examples/atlassian-connect-forge-spring-boot-sample/forge-container/build-and-deploy.sh.example) locally (the real `build-and-deploy.sh` is gitignored).\n\nExpose a platform health check (sample: **`GET /health`** → `OK`).\n\n### Runtime flow\n\n```text\nJira / Forge UI\n      │\n      ▼\nForge platform ──ingress──► your container :8080\n      │                      (x-forge-invocation-id)\n      │                      ContainerAuthorizationFilter\n      │                      → ForgeAuthentication in SecurityContext\n      │\n      └──egress sidecar :7072──► Jira REST (/jira/rest/api/…)\n             ▲\n             └── EgressClientService / JiraProductAdapterImpl\n                 (forge-proxy-authorization: Forge id=… / installationId=…)\n```\n\n### Beans you use in application code\n\n| Bean | Role |\n|------|------|\n| **`JiraProductAdapter`** (`JiraProductAdapterImpl`) | Same interface as hybrid; implementation calls **`EgressClientService.jiraTemplateRequest`** |\n| **`EgressClientService`** | Sidecar JSON APIs (`/invocation/context`) and Jira path factory |\n| **`ForgeContextService`** | Resolves invocation context from egress for ingress filter |\n| **`ManualAuthorizationService`** (`ManualAuthorizationServiceImpl`) | Seed **`SecurityContextHolder`** for background work, webtriggers, or tests (rejects cross-tenant `cloudId` when a context already exists) |\n| **`ContainerAuthorizationFilter`** | Ingress: when `x-forge-invocation-id` is present, populates `SecurityContextHolder` with `ForgeAuthentication`. Inserted **inside** the Spring Security chain via `addFilterAfter(SecurityContextHolderFilter.class)` — running before `SecurityContextHolderFilter` (or entirely outside the chain) would let it reload an empty deferred context from the stateless repository and overwrite the installed authentication, after which `AuthorizationFilter` rejects non-public paths with 403 |\n| **`ContainerWebSecurityConfiguration`** | Stateless HTTP security (no form login). Public paths come from `bridge.container.security.public-paths` (default `[/health]`); everything else requires an authenticated principal set by `ContainerAuthorizationFilter` |\n\n**`ManualAuthorizationService`** overloads match the hybrid module (`authorize(AtlassianHostUser)`, `authorize(AtlassianHost)`, `authorize(cloudId, installationId, accountId)`). Clear the security context after background tasks.\n\n### Reaching endpoints via webtrigger\n\nThe container is not internet-routable — only Forge platform reaches it. To expose a backend route as a public URL (browser, cron, external system), declare a **`webtrigger`** module in `manifest.yml` pointing at an `endpoint` that maps to your Spring path:\n\n```yaml\nmodules:\n  webtrigger:\n    - key: my-trigger\n      endpoint: my-trigger-ep\n  endpoint:\n    - key: my-trigger-ep\n      service: java-service\n      route:\n        path: /api/my-trigger      # your @GetMapping path\n```\n\nThen `forge webtrigger create -e \u003cenv\u003e` returns a URL of the form `https://\u003capp-id\u003e.hello.atlassian-dev.net/x1/\u003cwebtrigger-token\u003e` that Forge routes to that endpoint inside your container. The token authorizes the **URL**, not the caller — treat it as a capability URL and add caller-side controls when needed.\n\nWebtrigger requests do not carry a Connect iframe JWT. The recommended controller pattern is:\n\n```java\n@GetMapping(\"/api/my-trigger\")\n@IgnoreJwt\n@ResponseBody\npublic Map\u003cString, String\u003e myTrigger(\n    @RequestParam String accountId, @AuthenticationPrincipal AtlassianHostUser user) {\n\n  manualAuthorizationService.authorize(user.getHost().getCloudId(), user.getHost().getInstallationId(), Optional.of(accountId));\n  // select adapters now see a ForgeAuthentication with that AtlassianHostUser\n  return ...;\n}\n```\n\n`ManualAuthorizationService.authorize(...)` seeds `SecurityContextHolder` with a `ForgeAuthentication` so the **select adapters** (`JiraProductAdapter`, …) call the egress sidecar as that user without any further wiring. The same call also enforces **tenant isolation** — if a `ForgeAuthentication` is already in the context (e.g. you compose the trigger with another authenticated flow), the new `cloudId` must match the existing one, otherwise an `IllegalStateException(\"Cross tenant authorization is not allowed: …\")` is thrown. This prevents a webtrigger URL minted for tenant A from being used to drive Jira calls against tenant B.\n\nEnd-to-end example with real URL shape, query parameters, and CLI commands: **[`forge-container/README.md` → Calling `/api/impersonation` via webtrigger](examples/atlassian-connect-forge-spring-boot-sample/forge-container/README.md#calling-apiimpersonation-via-webtrigger)**.\n\n### Manifest and container image (sample)\n\nIn **[`examples/.../forge-container/manifest.yml`](examples/atlassian-connect-forge-spring-boot-sample/forge-container/manifest.yml)**:\n\n- **`containers[].key`**: `java-service` (must match `forge containers create -k …` and ECR repo name)\n- **`services`**: routes Forge modules to the container\n- **`app.connect.remote`**: Connect key for shared descriptors (no Connect iframe in this sample)\n- Deployed image tag: Forge variable **`${TAG}`**\n\nSee **[`forge-container/README.md`](examples/atlassian-connect-forge-spring-boot-sample/forge-container/README.md)** for register, Custom UI build, `dev-loop.sh`, and deploy.\n\n## Build\n\nFrom the repository root (requires a JDK matching **`java.version`** in the root `pom.xml`, currently **21**):\n\n```bash\nmvn clean install\n```\n\nThe [sample app](examples/atlassian-connect-forge-spring-boot-sample/) may use a different Java level in its own `pom.xml`.\n\n### Code style\n\n- **Format (Google Java Format + import cleanup):** `mvn spotless:apply`\n- **Check formatting without writing files:** `mvn spotless:check`\n- **`mvn verify`** runs **Spotless** (`spotless:check`), **Checkstyle**, **SpotBugs** (`spotbugs:check`), and **PMD** (`pmd:check`) on each library module.\n\nTo skip Spotless temporarily (for example during a large merge): `mvn verify -Dspotless.skip=true`. To skip Checkstyle: `mvn verify -Dcheckstyle.skip=true`. To skip SpotBugs: `mvn verify -Dspotbugs.skip=true`. To skip PMD: `mvn verify -Dpmd.skip=true`.\n\nSpotBugs uses **`config/spotbugs/exclude-filter.xml`** for known false positives (extend with `\u003cMatch\u003e` as needed). PMD rules are the built-in **`category/java/errorprone.xml`** and **`category/java/bestpractices.xml`** (see root `pom.xml`); reports: **`target/pmd.xml`** per module.\n\n### Error Prone\n\n[Error Prone](https://errorprone.info/) runs as a **javac plugin** during **`compile`** (so **`mvn clean install`** and **`mvn verify`** both compile sources with checks enabled).\n\n- **Skip temporarily:** `mvn … -Derrorprone.skip=true` (profile **`errorprone-off`**).\n- **JDK module flags** for the build JVM live in **`.mvn/jvm.config`**; forked `javac` also gets the `-J--add-exports` / `-J--add-opens` flags from **`maven-compiler-plugin`** (see [Error Prone Maven install](https://errorprone.info/docs/installation#maven)).\n- Version: **`error-prone.version`** in the root `pom.xml` (currently aligned with `error_prone_core` on Maven Central).\n\n### Git pre-commit hook\n\nHooks live in **`.githooks/`** (tracked in git). **`pre-commit`** runs **`mvn spotless:apply`**, then **`mvn clean install`** at the repository root (full reactor including **`bridge-connect-container`**, **tests on**, **`verify`** including **JaCoCo** check and report), then **`mvn jacoco:report`** for **`bridge-common`** and **`bridge-forge-connect`** (refreshes HTML under each module’s **`target/site/jacoco/`**), prints **`file://…/target/site/jacoco/index.html`** paths, then **`mvn clean install`** for **`examples/atlassian-connect-forge-spring-boot-sample`** (example build and tests).\n\n**Automatic registration:** building from the **repository root** runs **`scripts/install-git-hooks.sh`** on the **`initialize`** phase (via **`exec-maven-plugin`**, `inherited=false`), so a normal **`mvn clean install`** (or any goal that runs `initialize`) sets **`git config core.hooksPath .githooks`** for this clone.\n\n- **Manual install** (same effect): `./scripts/install-git-hooks.sh`\n- **Skip from Maven:** `-DinstallGitHooks.skip=true`\n- **Skip in scripts:** `CI` set (e.g. GitHub Actions), **`GIT_HOOKS_INSTALL=0`**, or not a Git checkout — the script exits without changing Git config.\n- **Bypass hook on commit:** `SKIP_HOOKS=1 git commit …`\n- **Note:** `spotless:apply` can reformat files outside the staged set; review `git status` and re-stage if needed before committing again.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fforge-sql-orm%2Fatlassian-runtime-bridge","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fforge-sql-orm%2Fatlassian-runtime-bridge","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fforge-sql-orm%2Fatlassian-runtime-bridge/lists"}