Skip to content

Backend Architecture

The Foundation/Lens backend runs entirely on Atlassian Forge — a serverless platform for Jira Cloud apps. There is no standalone server; all backend code executes as Forge resolver functions, event handlers, and async consumers within the Forge runtime.

Forge Bridge (invoke)
┌──────────────────┐ ──────────────────────> ┌──────────────────────┐
│ Custom UI │ │ Resolver Registry │
│ (React SPA) │ <────────────────────── │ (src/index.ts) │
└──────────────────┘ JSON response └─────────┬────────────┘
┌───────────────────────────┐
│ Resolver Handlers │
│ (src/resolvers/*.ts) │
└───────────┬───────────────┘
┌────────────────────────┼──────────────────────┐
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌───────────────────┐
│ Services Layer │ │ Forge SQL │ │ Forge KVS │
│ (src/services/) │ │ (MySQL 5.7) │ │ (Key-Value) │
└────────┬─────────┘ └──────────────────┘ └───────────────────┘
┌──────────────────┐
│ Jira REST API │
│ (asUser/asApp) │
└──────────────────┘
  • Server-Centric: The backend assembles complete views. The frontend is a thin rendering layer — it never fetches data from Jira directly.
  • Issue Cache in Forge SQL: A local copy of Jira issue data, kept fresh by free product event triggers (avi:jira:updated:issue). Users read from the cache, not from the Jira API.
  • Event-Driven Sync: Jira fires product events on issue/sprint/link changes. The backend catches these, fetches the updated issue (2 API points), and upserts the cache.
  • Rate-Limit Aware: Every Jira API call goes through a KVS-backed point counter (utils/rate-limiter.ts). Budget is checked before calls and refunded on failure.
foundation/src/
├── index.ts # Resolver registry -- ALL endpoints defined here
├── db/
│ ├── schema.ts # Fresh-install DDL + ensureSchema() orchestrator
│ └── migrations.ts # Sequential migration runner (v2 -> v32)
├── resolvers/ # API endpoint handlers (invoked via Forge bridge)
│ ├── lenses.ts # Lens CRUD
│ ├── hierarchy.ts # Tree operations (getLensView, add/move/delete nodes)
│ ├── views.ts # View CRUD
│ ├── inline-edit.ts # Field updates, transitions, priorities
│ ├── sync-agents.ts # Sync agent (generator) CRUD + execution
│ ├── permissions.ts # ACL management, user/group search
│ ├── dependencies.ts # Issue link CRUD, dependency lag overrides
│ ├── admin.ts # Admin stats, cache refresh, CSS overrides, reset
│ ├── announcements.ts # Announcement popup dismissal (KVS)
│ ├── ai.ts # Portfolio intelligence analysis
│ ├── team.ts # Team CRUD + membership
│ ├── holiday.ts # Holiday calendar management
│ ├── absence.ts # Resource absence tracking
│ ├── baseline.ts # Baseline snapshot CRUD
│ ├── leveling.ts # Resource leveling jobs
│ ├── skills.ts # Skill taxonomy + resource skill assignments
│ ├── placeholder.ts # Placeholder resource management
│ ├── resource-workload.ts # Resource workload analysis
│ ├── cross-lens-workload.ts # Cross-lens workload aggregation
│ ├── resource-matcher.ts # Best-match resource finder
│ ├── gdpr.ts # GDPR data deletion + reporting
│ ├── field-metadata.ts # Jira field discovery (standard + custom)
│ ├── field-options.ts # Custom field option caching
│ ├── job-status.ts # Async job polling
│ ├── realtime.ts # Forge Realtime auth token
│ ├── budget.ts # Rate limit budget status
│ ├── search.ts # Issue search for link picker
│ ├── sprint.ts # Sprint data fetching
│ ├── preferences.ts # User preferences (KVS)
│ ├── support-chat.ts # In-app support chat (KVS)
│ ├── feedback.ts # In-app feedback widget
│ ├── monitoring.ts # OPS monitoring dashboard
│ ├── feature-flags.ts # Feature flag management
│ ├── last-lens.ts # Last-visited lens tracking
│ ├── init-context.ts # Lens context initialization
│ ├── diff.ts # Incremental diff after sync
│ ├── sync-freshness.ts # Sync agent freshness check
│ ├── jql-validation.ts # JQL query validation
│ ├── issue-context.ts # Issue context panel (lenses for an issue)
│ ├── projects.ts # Project search + importable project listing
│ ├── jpd.ts # Jira Product Discovery integration
│ ├── import-projects.ts # Project-based hierarchy import
│ ├── remove-project-import.ts # Remove imported project
│ ├── bulk-add.ts # Bulk add issues by key
│ ├── csv-import.ts # CSV/Excel import
│ ├── structure-import.ts # Third-party hierarchy import
│ ├── bigpicture-import.ts # Third-party PPM import
│ ├── export-file-import.ts # DC-to-Cloud export file import
│ ├── delete.ts # Delete issues from Jira
│ ├── sync.ts # Manual sync trigger
│ └── work-issue-hierarchy.ts # Work issue hierarchy resolution
├── services/ # Business logic layer
│ ├── hierarchy-engine.ts # Tree manipulation (gap=100 positioning)
│ ├── cache-manager.ts # Issue cache CRUD (upsert with ON DUPLICATE KEY)
│ ├── permission-service.ts # checkPermission(), group/role resolution
│ ├── view-service.ts # View persistence + default view creation
│ ├── sync-agent-service.ts # Sync agent persistence + execution queue
│ ├── jira-sync.ts # Jira API fetch + custom field parsing
│ ├── realtime.ts # Forge Realtime channel publish
│ ├── lens-service.ts # Lens persistence
│ ├── license-service.ts # Forge licensing checks
│ ├── edition-service.ts # App edition detection
│ ├── team-service.ts # Team persistence
│ ├── holiday-service.ts # Holiday calendar persistence
│ ├── absence-service.ts # Absence persistence
│ ├── baseline-service.ts # Baseline snapshot persistence
│ ├── skills-service.ts # Skill taxonomy persistence
│ ├── resource-workload.ts # Workload calculation engine
│ ├── cross-lens-workload.ts # Cross-lens workload aggregation
│ ├── resource-leveling.ts # Resource leveling algorithm
│ ├── resource-matcher.ts # Best-match scoring
│ ├── capacity-calculator.ts # Capacity calculation with holidays/absences
│ ├── timeline-scheduler.ts # Auto-scheduling engine
│ ├── ai-service.ts # Portfolio intelligence analysis
│ ├── auto-sync.ts # Auto-sync staleness detection
│ ├── deploy-version.ts # Deploy-triggered cache refresh
│ ├── error-reporter.ts # Sentry error forwarding
│ ├── resolver-metrics.ts # Resolver performance tracking
│ ├── monitoring-service.ts # Monitoring data aggregation
│ ├── feedback-service.ts # Feedback persistence
│ ├── feature-flags.ts # Feature flag persistence
│ ├── csv-transform.ts # CSV data transformation
│ ├── structure-client.ts # Structure API client
│ ├── structure-transform.ts # Structure data transformation
│ ├── bigpicture-client.ts # BigPicture API client
│ └── bigpicture-transform.ts # BigPicture data transformation
├── actions/ # Rovo agent action handlers (invoked via manifest actions)
│ ├── add-issues.ts # Add issues by JQL
│ ├── create-flex-item.ts # Create flex item
│ ├── move-node.ts # Move node in hierarchy
│ ├── update-field.ts # Bulk field updates
│ ├── create-and-add-issue.ts # Create Jira issue + add to lens
│ ├── execute-sync-agents.ts # Trigger sync agent execution
│ ├── remove-nodes.ts # Remove nodes from hierarchy
│ ├── orchestrate-lens.ts # Multi-step lens orchestration
│ ├── build-wbs.ts # AI-driven WBS generation
│ ├── analyze-section-gaps.ts # Gap analysis
│ ├── get-summary.ts # Lens summary for Rovo
│ ├── get-lens-context.ts # Lens context for Rovo
│ ├── find-nodes.ts # Search nodes in hierarchy
│ ├── bulk-move-nodes.ts # Bulk move nodes
│ ├── lens-crud.ts # Lens CRUD for Rovo
│ ├── sync-agent-crud.ts # Sync agent CRUD for Rovo
│ ├── view-crud.ts # View CRUD for Rovo
│ ├── view-perm-dispatch.ts # View/permission dispatcher
│ ├── r1-dispatch.ts # hierMeta dispatcher
│ ├── r2r5-dispatch.ts # R2R5 destructive operations dispatcher
│ ├── r2r5-read-dispatch.ts # R2G read-only operations dispatcher
│ └── ... # ~40+ additional action handlers
├── events/ # Jira product event handlers
│ ├── issue-handler.ts # Issue created/updated/deleted/linked -> cache sync
│ ├── sprint-handler.ts # Sprint started/closed/updated -> cache sync
│ ├── cache-integrity.ts # Hourly: stale cache re-fetch
│ ├── daily-health-check.ts # Daily: health snapshots + alert evaluation
│ ├── sync-agent-executor.ts # Async: sync agent JQL execution (15-min timeout)
│ ├── leveling-executor.ts # Async: resource leveling
│ ├── cache-refresh-executor.ts # Async: per-project cache refresh
│ ├── import-executor.ts # Async: hierarchy import
│ ├── wbs-executor.ts # Async: AI WBS generation
│ ├── uninstall-handler.ts # App uninstall cleanup
│ └── alert-engine.ts # Alert threshold evaluation
└── utils/
├── rate-limiter.ts # KVS-backed rate limit point counter
├── resolver-guards.ts # Composable auth/schema/permission middleware
├── kvs-lease.ts # Distributed locking via KVS
├── retry.ts # Retry with exponential backoff
├── retry-helpers.ts # Jira API retry wrapper
├── uuid.ts # UUID v4 generation
├── visible-issues.ts # Jira BROWSE permission checking
├── cost-model.ts # API point cost estimation
├── diff-accumulator.ts # Incremental diff builder
├── reset-recovery.ts # Reset-all-data crash recovery
└── import-*.ts # Import utilities

All resolvers are registered in src/index.ts using the defineResolver() helper:

function defineResolver(
name: string,
handler: (args: any) => Promise<any>,
options?: { skipMetrics?: boolean }
)

Each resolver receives { payload, context } where:

  • payload — parameters sent by the frontend via invoke(name, payload)
  • context.accountId — the current user’s Jira account ID
  • context.cloudId — the Jira Cloud site ID

The frontend calls resolvers through the @forge/bridge invoke() function:

// Frontend (api/client.ts)
const result = await invoke('getLensView', { lensId: '...' });
// Backend (src/resolvers/hierarchy.ts)
export async function getLensViewHandler({ payload, context }) {
const { lensId } = payload;
// ... returns JSON-serializable response
}

Composable middleware wrappers are defined in src/utils/resolver-guards.ts:

GuardPurpose
withSchemaEnsures Forge SQL schema is initialized before handler runs
withAuthRequires context.accountId to exist
withAdminRequires the user to be a Jira site admin
withLensPermission(level)Requires the user to have at least level permission on the lens
withLicenseChecks that the app license is active

Guards are composed with compose():

defineResolver('getAdminStats', compose(withSchema, withAdmin)(getAdminStatsHandler));
defineResolver('listViews', compose(withSchema, withLensPermission('view'))(listViewsHandler));

Every resolver is automatically wrapped with withMetrics() (from services/resolver-metrics.ts) which records execution time to the monitoring_metrics table. Lightweight utility resolvers opt out with { skipMetrics: true }.

Critical Forge bug (ECO-277): Never return bare undefined from resolvers — the frontend receives {} (truthy). Always return explicit null or { error: string }.

Standard error responses:

// Validation errors
return { error: 'lensId is required' };
return { error: 'Name is required' };
// Permission denied (from permissionService.denyAccess())
return { error: 'Permission denied' };
// Rate limit exhausted
return { error: 'budget_exhausted', remaining: status.remaining, resetsAt: status.resetsAt };
// License inactive
return { error: 'license_inactive', message: '...' };
// Success responses
return { success: true };
return { lenses: [...] };
return { lens: { id, name, ... } };

The backend uses two storage systems:

  • MySQL 5.7 compatible, 1 GiB per installation
  • 150 DML operations/second
  • All hierarchical, relational, and cached data
  • See Database Schema for full documentation
  • Simple key-value pairs, JSON-serializable values
  • Used for: user preferences, rate limit counters, job status, dismissed announcements, session data, distributed locks (leases), feature flags, auto-sync timestamps
  • Key naming convention: namespace:identifier (e.g., user_prefs:accountId, job:jobId, active-gen-job:lensId)