Skip to content

Architecture Overview

This document describes the system architecture of Lens (codebase: Foundation), a Project Portfolio Management app for Jira Cloud built on Atlassian Forge.

+=========================================================================+
| JIRA CLOUD INSTANCE |
| |
| +-------------------------------------------------------------------+ |
| | FORGE SANDBOX (per-installation isolation) | |
| | | |
| | +-------------------------------------------------------------+ | |
| | | Custom UI Layer (cross-origin iframe) | | |
| | | | | |
| | | +------------------+ +---------+ +-------------------+ | | |
| | | | AG Grid Engine | | dnd-kit | | Inline Editors | | | |
| | | | (tree data, | | (DnD | | (24 field types) | | | |
| | | | inline editing) | | layer) | +-------------------+ | | |
| | | +------------------+ +---------+ | | |
| | | | | |
| | | +------------------+ +-------------------+ | | |
| | | | Gantt/Timeline | | Pipeline Studio | | | |
| | | | (bars, deps, | | (sort, group, | | | |
| | | | critical path) | | filter) | | | |
| | | +------------------+ +-------------------+ | | |
| | | | | |
| | | @forge/bridge invoke() ──> all data operations | | |
| | | Forge Realtime subscribe() ──> live UI updates | | |
| | +------------------------------+------------------------------+ | |
| | | | |
| | +------------------------------v------------------------------+ | |
| | | Resolver Layer (Node.js 22.x, 25s timeout) | | |
| | | | | |
| | | +-------------+ +------------------+ +---------------+ | | |
| | | | Lens CRUD | | Hierarchy Engine | | Inline Edit | | | |
| | | +-------------+ +------------------+ +---------------+ | | |
| | | +-------------+ +------------------+ +---------------+ | | |
| | | | Views | | Permissions | | Jira Sync | | | |
| | | +-------------+ +------------------+ +---------------+ | | |
| | | +-------------+ +------------------+ +---------------+ | | |
| | | | Sync Agents | | Dependencies | | Resources | | | |
| | | +-------------+ +------------------+ +---------------+ | | |
| | +------------------------------+------------------------------+ | |
| | | | |
| | +------------------------------v------------------------------+ | |
| | | Rovo Agent Layer | | |
| | | | | |
| | | +------------------+ +----------------------------------+ | | |
| | | | Rovo Agent | | 35 Action Functions | | | |
| | | | (rovo:agent) | | (standalone + 5 dispatchers) | | | |
| | | | Chat sidebar | | GET / CREATE / UPDATE / DELETE | | | |
| | | +------------------+ +----------------------------------+ | | |
| | +--------------------------------------------------------------+ | |
| | | | |
| | +------------------------------v------------------------------+ | |
| | | Data Layer | | |
| | | | | |
| | | +---------------------+ +------------------------------+ | | |
| | | | Forge SQL (MySQL) | | Forge KVS | | | |
| | | | ─────────────────── | | ─────────────────────────── | | | |
| | | | lenses | | user:{id}:prefs | | | |
| | | | hierarchy_nodes | | rate_limit:hourly_points | | | |
| | | | generators | | foundation:schema_version | | | |
| | | | issue_cache | | announcements_dismissed:{id} | | | |
| | | | views | | feature_flags | | | |
| | | | lens_permissions | | | | | |
| | | | resources | | | | | |
| | | | teams / skills | | | | | |
| | | | baselines | | | | | |
| | | +---------------------+ +------------------------------+ | | |
| | +--------------------------------------------------------------+ | |
| | | | |
| | +------------------------------v------------------------------+ | |
| | | Event Processing Layer | | |
| | | | | |
| | | Jira Product Events ──> Issue Cache Updater | | |
| | | (avi:jira:created/updated/deleted:issue) (free, no points) | | |
| | | | | |
| | | Async Consumers (15-min timeout): | | |
| | | generator-queue ──> Sync Agent Executor | | |
| | | leveling-queue ──> Resource Leveling | | |
| | | import-queue ──> Data Import | | |
| | | wbs-queue ──> WBS Builder | | |
| | | | | |
| | | Scheduled Triggers: | | |
| | | hourly ──> Cache Integrity Check | | |
| | | daily ──> Health Check | | |
| | +--------------------------------------------------------------+ | |
| +-------------------------------------------------------------------+ |
| |
| +-------------------------------------------------------------------+ |
| | Jira REST API v3 (rate-limited: 65K-500K pts/hr) | |
| | Issue CRUD, JQL search, transitions, user search, permissions | |
| +-------------------------------------------------------------------+ |
+=========================================================================+

Design Philosophy: Server-Centric Architecture

Section titled “Design Philosophy: Server-Centric Architecture”

The most important architectural decision in Lens is its server-centric design. The backend assembles complete views; the frontend is a thin rendering layer.

How it works:

  1. When a user opens a lens, the frontend calls a single resolver: getLensView(lensId)
  2. The backend performs all data assembly: permission check, tree loading, issue cache enrichment, Jira BROWSE filtering
  3. The backend returns a pre-assembled flat row array ready for AG Grid to render
  4. The frontend never calls Jira APIs directly — all data flows through the resolver layer

Why this matters:

  • Jira API rate limits (65,000 points/hour in Tier 1) are a hard constraint. Every API call costs points. The event-driven issue cache means opening a lens costs zero Jira API points.
  • The frontend runs in a Forge sandboxed iframe with limited capabilities. Complex data assembly in the frontend would be fragile and slow.
  • Backend assembly enables permission filtering at the data layer, ensuring users never see issues they cannot access in Jira.

See Design Decisions for the full rationale.

Lens runs entirely on Atlassian Forge, Atlassian’s cloud app platform. Forge provides:

CapabilityHow Lens Uses It
Sandboxed RuntimeNode.js 22.x functions execute in Forge’s isolated environment. No self-hosted infrastructure.
Forge SQLMySQL-compatible database (1 GiB per installation). Stores lenses, hierarchy, views, permissions, issue cache. Per-installation isolation enforced by the platform.
Forge KVSKey-value store for lightweight data: user preferences, rate limit counters, schema version tracking, feature flags, announcement dismissals.
Product EventsFree event triggers (avi:jira:created/updated/deleted:issue) keep the issue cache fresh without consuming API points.
Async EventsQueue-based consumers with 15-minute timeout for long-running operations: sync agent execution, resource leveling, data imports, WBS generation.
Scheduled TriggersHourly cache integrity checks and daily health monitoring.
Custom UIReact apps served in cross-origin iframes with Forge bridge for resolver communication.
Forge RealtimePub/sub channels for live UI updates (e.g., when a sync agent completes).
Rovo AgentAI agent accessible via Rovo Chat sidebar with 35 action functions for conversational CRUD.
LicensingBuilt-in license enforcement via app.licensing.enabled: true in the manifest.

The app registers three Forge UI modules, each serving a different Jira integration point:

jira:globalPage — Main Application (key: fm)

Section titled “jira:globalPage — Main Application (key: fm)”

The primary module. Uses layout: blank for full-page rendering. Routes:

RouteComponentPurpose
/LensListOrRedirectAuto-redirects to last-viewed lens, or shows lens menu
/lens/:idLensViewMain grid + Gantt + toolbar + inspector
/importImportHubData import wizard (CSV, competitor imports)
/adminAdminPageAdmin settings (cache, stats, CSS overrides)
/structure/:idStructureRedirectLegacy URL redirect to /lens/:id

jira:issueContext — Issue Sidebar Panel (key: fic)

Section titled “jira:issueContext — Issue Sidebar Panel (key: fic)”

Shows which lenses contain a given Jira issue. Appears in the issue detail sidebar. Lightweight React app in static/issue-context/.

jira:adminPage — Admin Settings (key: fad)

Section titled “jira:adminPage — Admin Settings (key: fad)”

Jira site admin configuration page. Accessed via Jira administration. React app in static/admin/.

The frontend is a React 18 + TypeScript application. It has no state management library — all state is managed with useState, useCallback, and useMemo.

App.tsx
ErrorBoundary
IntlProvider (react-intl, 11 locales)
FeatureFlagProvider
RovoProvider
LicenseBanner
AnnouncementPopup
Router (React Router v6, Forge bridge history)
LensListOrRedirect / LensMenu
LensView <-- owns all data state
Toolbar
ColumnPicker
ViewSwitcher
SortGroupPresets
ZoomControls
HierarchyGrid (AG Grid) <-- controlled component
Inline Editors (8 types)
Column Header Menus
Gantt/Timeline
Dependency Arrows
Rollup Brackets
InspectorPanel
AccordionSections
PipelineStudio (sort/group/filter)
GeneratorPanel
BulkActions
CommandPalette (Cmd+K)
  1. LensView owns all data state. Child components receive props and callbacks. HierarchyGrid never fetches its own data.
  2. No new CSS files. All styles go in foundation.css or inline for dynamic values. Use --ds-* Atlassian Design System tokens with hex fallbacks.
  3. Density-aware. Five density levels (comfortable, standard, compact, supercompact, micro) via parent CSS class selectors.
  4. Feature-organized directories. Components live under components/Grid/, components/Toolbar/, components/Gantt/, etc. Tests in __tests__/ subdirectories.
  5. AG Grid specifics: suppressRowHoverHighlight={true} (CSS :hover instead of JS hover), stopEditingWhenCellsLoseFocus={false} (manual click-outside handling due to Forge iframe).

AG Grid is the core rendering engine. Lens uses its Enterprise features:

  • Tree Data: Hierarchical rows with expand/collapse, arbitrary depth
  • Inline Editing: 24 cell editor types — 8 core (text, number, date, status, priority, assignee, sprint, labels) + 16 custom field editors (see Inline Editing Guide)
  • Column Management: Dynamic column definitions built by buildColumnDefs() in columns.tsx
  • Row Grouping: Group rows by status, priority, assignee, etc.
  • Custom Cell Renderers: Issue links, user cards, rich text (ADF), progress bars

All frontend-to-backend communication goes through the Forge resolver. The frontend calls invoke('resolverName', payload) via @forge/bridge, and the backend dispatches to the matching handler.

foundation/src/index.ts -- Resolver registry (100+ handlers)
defineResolver('name', handler) -- Registers each resolver with optional metrics wrapping

Resolvers are organized by domain:

DomainKey ResolversSource
Lens CRUDlistLenses, createLens, deleteLens, updateLensresolvers/lenses.ts
HierarchygetLensView, addIssueById, moveNode, deleteNode, addFlexItemresolvers/hierarchy.ts
Inline EditupdateField, getTransitions, getPrioritiesresolvers/inline-edit.ts
ViewslistViews, createView, updateView, deleteView, setDefaultViewresolvers/views.ts
Sync AgentscreateSyncAgent, executeSyncAgents, listSyncAgentsresolvers/sync-agents.ts
PermissionsgetPermissions, grantPermission, revokePermissionresolvers/permissions.ts
DependenciescreateIssueLink, deleteIssueLink, getDependencyLagsresolvers/dependencies.ts
AdmingetAdminStats, refreshCache, resetAllDataresolvers/admin.ts
ResourcesgetResourceWorkload, startLeveling, teams, skills, baselinesMultiple files
ImportpreviewCsvImport, executeStructureImport, competitor importsMultiple files

Resolvers use composable guard middleware:

// Compose guards: schema check + admin check
defineResolver('getAdminStats', compose(withSchema, withAdmin)(getAdminStatsHandler));
// Guards available:
// withSchema -- ensures database schema is at current version
// withAuth -- verifies user is authenticated
// withAdmin -- verifies user is Jira site admin
// withLicense -- checks app license is active
// withLensPermission('view'|'edit'|'control') -- checks lens-level ACL

Business logic lives in src/services/, separate from resolver handlers:

ServicePurpose
hierarchy-engine.tsTree manipulation with gap=100 position strategy
cache-manager.tsIssue cache CRUD (upsert with ON DUPLICATE KEY)
permission-service.tscheckPermission() + group/role resolution
view-service.tsView persistence + default view creation
generator-service.tsSync agent execution queue + job tracking
jira-sync.tsJira API fetch + custom field parsing
realtime.tsForge Realtime channel publish
error-reporter.tsSentry error reporting
resolver-metrics.tsPer-resolver timing and performance tracking

Event-driven processing keeps the app responsive and the cache fresh:

HandlerTriggerPurpose
issue-handler.tsavi:jira:created/updated/deleted:issue, issuelink eventsSync issue cache, trigger auto-sync for affected lenses
sprint-handler.tsavi:jira:started/closed/updated:sprintUpdate sprint fields on cached issues
cache-integrity.tsHourly scheduled triggerDetect stale entries, clean orphans
sync-agent-executor.tsgenerator-queue consumerExecute sync agents (JQL fetch, 15-min timeout)
leveling-executor.tsleveling-queue consumerResource leveling calculations
import-executor.tsimport-queue consumerBackground data import processing
wbs-executor.tswbs-queue consumerAI-driven WBS generation
daily-health-check.tsDaily scheduled triggerInstallation health monitoring
uninstall-handler.tsavi:forge:uninstalled:appCleanup on app uninstall

Forge SQL provides a MySQL-compatible database, isolated per installation (platform-enforced). Current schema version is 32, managed by a sequential migration system.

Core Tables:

TableKey ColumnsPurpose
lensesid, name, owner_account_id, archivedTop-level lens definitions
hierarchy_nodeslens_id, parent_id, position, node_type, jira_issue_idTree structure (adjacency list, gap=100 positioning)
generatorslens_id, generator_type, config (JSON), parent_node_idSync agent definitions
issue_cachejira_issue_id, jira_issue_key, 30+ cached fields, custom_fields (JSON)Local copy of Jira issue data
viewslens_id, columns (JSON), settings_json, is_defaultView configurations
lens_permissionslens_id, grantee_type, grantee_id, permission_levelAccess control lists
resourcesjira_account_id, hours_per_day, efficiency_multiplierResource management
teamsname, membersTeam organization
baselineslens_id, created_at, entriesBaseline snapshots

Schema Migration System:

  • CURRENT_SCHEMA_VERSION in src/db/migrations.ts (currently 32)
  • ensureSchema() runs on every Forge cold-start, reads version from KVS
  • Fresh installs: CREATE TABLE statements in src/db/schema.ts
  • Existing installs: sequential migrations (v2 through v32)
  • safeDDL() wrapper for idempotent ALTER TABLE operations
  • KVS-based distributed lease prevents concurrent migration runs

The Rovo Agent provides conversational AI access via the Rovo Chat sidebar in Jira. It has 35 action functions organized as:

  • 29 standalone actions (direct function handlers)
  • 6 dispatcher actions that route via _action parameter:
    • lensCrud — lens create/rename/delete (3 sub-actions)
    • hierMeta — hierarchy metadata operations (10 sub-actions)
    • viewPerm — view and permission CRUD (5 sub-actions)
    • jiraUtil — JQL validation, project analysis (3 sub-actions)
    • r2g — read-only intelligence/analytics (19 sub-actions, GET verb, no confirmation)
    • r2r5 — destructive operations (14 sub-actions, TRIGGER verb, requires confirmation)

The dispatcher pattern exists to work around the CaaS manifest byte size limit (~257KB). See Design Decisions.

Lens uses a three-tier permission system (the edit_generators level was collapsed into edit in schema v30):

LevelCapabilities
ViewSee the lens and its data (read-only)
EditView + rearrange, add, remove items, flex items, create/modify/delete sync agents
ControlEdit + change settings, permissions, archive, delete

Resolution order:

  1. Lens owner automatically has Control
  2. Jira site admins automatically have Control
  3. Explicit grants checked: user > group > role > everyone (highest level wins)

The issue cache stores data fetched as the app. When a user opens a lens, the backend filters the row set based on the user’s Jira permissions:

  1. Batch JQL permission check: api.asUser() verifies BROWSE permission
  2. Results cached in KVS with 30-minute TTL
  3. Issues the user cannot access in Jira are excluded from the response
  4. Fail-open strategy: If Jira API is unreachable, assume all issues visible (prevents outage from blocking the entire app)
  5. Hidden parent cascades to children
  • Forge sandbox isolation: Each installation has a separate database. Code runs in Forge’s isolated runtime.
  • CSP enforcement: Custom UI runs in a sandboxed iframe with strict Content Security Policy. No external scripts.
  • Parameterized SQL: All queries use parameterized statements, never string concatenation.
  • No PII in logs: No user emails, display names, or account IDs in log output.
  • License enforcement: app.licensing.enabled: true in manifest. Frontend LicenseBanner component. Backend resolvers detect and surface license_inactive errors.

These hard limits shape the architecture. They are enforced by Forge and Jira, not by application code.

ConstraintLimitImpact
Standard function timeout25 secondsAll resolver calls must complete within 25s. Complex operations use async consumers.
Async event timeout900 seconds (15 min)Sync agent execution, resource leveling, imports run as async consumers.
Frontend response payload5 MBA lens with 1,000 issues fits within this limit. Larger lenses may need pagination.
Forge SQL storage1 GiB per installationAt ~10 MB for a moderate deployment, this leaves 99% headroom.
Forge SQL DML rate150 operations/secondBulk operations must batch and throttle writes.
Jira API rate limit (Tier 1)65,000 points/hour (global)Shared across ALL installations. The event-driven cache is the primary mitigation.
Jira API rate limit (Tier 2)100K-500K points/hour (per-tenant)Granted to Lens. Per-tenant isolation prevents noisy-neighbor problems.
Invocation limits1,200/user/min, 5,000/install/minSets an upper bound on how frequently the frontend can call resolvers.
CaaS manifest byte limit~257KB (converted format)Limits the number of Rovo action entries. Currently at 35 actions, ~22,850 bytes.
Forge Realtime60 messages/channel/sec, 32 KB payloadSufficient for live update notifications. Preview status.