Skip to content

Database Schema

Foundation uses Forge SQL (MySQL 5.7 compatible) for all relational data and Forge KVS (key-value store) for session data, counters, and user preferences.

Current schema version: 32 (defined in foundation/src/db/migrations.ts)



lenses (1) ──────── (*) hierarchy_nodes
│ │
│ └── jira_issue_id -> issue_cache.jira_issue_id
├──── (*) generators
├──── (*) views
├──── (*) lens_permissions
├──── (*) ai_insights
├──── (*) baselines ──── (*) baseline_entries
├──── (*) leveling_jobs
├──── (*) dependency_lag_overrides
├──── (*) lens_snapshots
├──── (*) automation_rules
└──── (*) alerts
resources (1) ──── (*) resource_schedules
│ │
│ └── schedule_id -> work_schedules.id
├──── (*) team_memberships ──── team_id -> teams.id
├──── (*) resource_absences
├──── (*) resource_calendar_assignments ──── calendar_id -> holiday_calendars.id
└──── (*) resource_skills ──── skill_id -> skills.id
holiday_calendars (1) ──── (*) holiday_dates

Top-level entity. Each lens is a curated view of Jira issues organized as a hierarchy tree.

ColumnTypeDefaultDescription
idCHAR(36) PKUUID v4
numeric_idINT UNIQUEAuto-assigned sequential ID for friendly URLs
nameVARCHAR(255) NOT NULLDisplay name
descriptionTEXTOptional description
modeVARCHAR(16)'power'Mode: power, deleting
owner_account_idVARCHAR(128) NOT NULLJira account ID of the owner
item_limitINT1000Max issues allowed in this lens
archivedINTEGER00=active, 1=archived
created_atTIMESTAMPCURRENT_TIMESTAMP
updated_atTIMESTAMPCURRENT_TIMESTAMP

Tree structure for a lens. Each node is either a Jira issue, a flex item (grouping node), a milestone, or a sync agent group.

ColumnTypeDefaultDescription
idCHAR(36) PKUUID v4
lens_idCHAR(36) NOT NULLParent lens
parent_idCHAR(36)Parent node (null = root level)
positionINT NOT NULLSort order within parent (gap=100 strategy)
node_typeVARCHAR(32) NOT NULLissue, flex, generator_group, milestone
jira_issue_idVARCHAR(64)Jira issue ID (for issue nodes)
jira_issue_keyVARCHAR(32)Jira issue key (e.g., PROJ-123)
flex_nameVARCHAR(255)Display name (for flex/milestone nodes)
flex_descriptionTEXTDescription (for flex nodes)
flex_iconVARCHAR(128)Icon identifier (for flex nodes)
hiddenBOOLEANFALSEHidden by filter sync agents
generator_idCHAR(36)Links to the sync agent that created this node
milestone_dateDATETarget date (for milestone nodes)
scheduling_modeVARCHAR(16)'auto_bottom_up'Scheduling mode for auto-scheduling
leveling_delay_daysINT0Leveling delay in days
depthINT NOT NULL0Tree depth (0 = root)
created_byVARCHAR(128)Jira account ID of creator
created_atTIMESTAMPCURRENT_TIMESTAMP
updated_atTIMESTAMPCURRENT_TIMESTAMP

Indexes:

  • idx_lens_parent (lens_id, parent_id, position) — tree traversal
  • idx_lens_issue (lens_id, jira_issue_id) UNIQUE — prevents duplicate issues per lens
  • idx_jira_key (jira_issue_key) — issue key lookups
  • idx_hierarchy_generator_id (generator_id) — sync agent lookups

Sync agent configurations. Each generator defines a JQL query or rule that populates a lens.

ColumnTypeDefaultDescription
idCHAR(36) PKUUID v4
lens_idCHAR(36) NOT NULLParent lens
parent_node_idCHAR(36)Target parent node for generated issues
generator_typeVARCHAR(32) NOT NULLType: jql_insert, child_extend, hierarchy_builder, jpd_links, auto_project, etc.
positionINT NOT NULLExecution order
configJSON NOT NULLType-specific config (JQL query, project key, etc.)
created_byVARCHAR(128)Creator’s account ID
last_executed_atTIMESTAMPLast execution timestamp
last_execution_statusVARCHAR(16)success, error, partial
last_execution_messageTEXTHuman-readable status message
last_known_totalINTNULLJQL result count (for freshness checks)
created_atTIMESTAMPCURRENT_TIMESTAMP

Indexes:

  • idx_lens (lens_id)

Local cache of Jira issue data. Kept fresh by product event triggers. This is the primary read source for the frontend.

ColumnTypeDefaultDescription
jira_issue_idVARCHAR(64) PKJira issue ID
jira_issue_keyVARCHAR(32) NOT NULLIssue key (e.g., PROJ-123)
project_idVARCHAR(64) NOT NULLJira project ID
project_keyVARCHAR(16) NOT NULLProject key (e.g., PROJ)
issue_type_idVARCHAR(32)Issue type ID
issue_type_nameVARCHAR(128)Issue type name (Story, Bug, etc.)
summaryVARCHAR(512) NOT NULLIssue summary/title
status_idVARCHAR(32)Status ID
status_nameVARCHAR(128)Status display name
status_categoryVARCHAR(32)new, indeterminate, done
assignee_account_idVARCHAR(128)Assignee’s Jira account ID
assignee_display_nameVARCHAR(255)Assignee name
assignee_avatar_urlVARCHAR(512)Assignee avatar URL
reporter_account_idVARCHAR(128)Reporter’s Jira account ID
reporter_display_nameVARCHAR(255)Reporter name
reporter_avatar_urlVARCHAR(512)Reporter avatar URL
resolution_idVARCHAR(32)Resolution ID
resolution_nameVARCHAR(128)Resolution name (Done, Won’t Do, etc.)
resolution_dateTIMESTAMPWhen the issue was resolved
priority_idVARCHAR(16)Priority ID
priority_nameVARCHAR(64)Priority name (Highest, High, etc.)
sprint_idVARCHAR(32)Current sprint ID
sprint_nameVARCHAR(128)Current sprint name
sprint_stateVARCHAR(32)active, closed, future
parent_issue_idVARCHAR(64)Jira parent issue ID (Epic)
story_pointsDECIMAL(10,2)Story points / estimation
labelsJSONArray of label strings
componentsJSONArray of component objects
fix_versionsJSONArray of fix version objects
affects_versionsJSONArray of affects version objects
due_dateDATEDue date
start_dateDATEStart date (promoted from customfield_10015 in v15)
created_atTIMESTAMPIssue creation date in Jira
updated_atTIMESTAMPLast update date in Jira
cached_atTIMESTAMPCURRENT_TIMESTAMPWhen this cache entry was last refreshed
custom_fieldsJSONAll non-standard custom fields as JSON blob
links_jsonTEXTIssue links as JSON array
description_textTEXTPlain text from ADF description (max 500 chars)
environment_textTEXTPlain text from ADF environment (max 500 chars)
original_estimate_secondsINTOriginal time estimate
remaining_estimate_secondsINTRemaining time estimate
time_spent_secondsINTTime spent
hierarchy_levelINTJira hierarchy level (-1=Subtask, 0=Story, 1=Epic)
last_accessed_atTIMESTAMPCURRENT_TIMESTAMPFor auto-expiry (v32)

Indexes:

  • idx_project (project_key) — project-scoped queries
  • idx_parent (parent_issue_id) — parent issue lookups
  • idx_key (jira_issue_key) — key-based lookups

Saved view configurations for a lens (column order, formatting, Gantt settings).

ColumnTypeDefaultDescription
idCHAR(36) PKUUID v4
lens_idCHAR(36) NOT NULLParent lens
nameVARCHAR(255) NOT NULLView display name
is_defaultBOOLEANFALSEWhether this is the default view
visibilityVARCHAR(16)'public'public or private
owner_account_idVARCHAR(128) NOT NULLCreator’s account ID
columnsJSON NOT NULLColumn configuration array
settings_jsonTEXTAdditional settings (density, Gantt config, formatting, etc.)
created_atTIMESTAMPCURRENT_TIMESTAMP
updated_atTIMESTAMPCURRENT_TIMESTAMP

Indexes:

  • idx_lens (lens_id)

Access control list for lenses.

ColumnTypeDefaultDescription
idCHAR(36) PKUUID v4
lens_idCHAR(36) NOT NULLTarget lens
grantee_typeVARCHAR(16) NOT NULLuser, group, role, everyone
grantee_idVARCHAR(128)Account ID, group name, or role name (null for everyone)
permission_levelVARCHAR(32) NOT NULLview, edit, control
created_atTIMESTAMPCURRENT_TIMESTAMP

Indexes:

  • idx_lens (lens_id)
  • idx_unique_grant (lens_id, grantee_type, grantee_id) UNIQUE — note: NULL != NULL in SQL, so everyone grants need separate dedup (see v31 migration)

AI-generated portfolio insights.

ColumnTypeDescription
idCHAR(36) PKUUID v4
lens_idCHAR(36) NOT NULLTarget lens
insight_typeVARCHAR(32) NOT NULLType of insight
severityVARCHAR(16) NOT NULLinfo, warning, critical
summaryTEXT NOT NULLHuman-readable summary
affected_nodesTEXTJSON array of affected node IDs
suggested_actionTEXTJSON suggested action object
created_atBIGINT NOT NULLUnix timestamp (ms)
resolved_atBIGINTUnix timestamp when resolved

Indexes:

  • idx_ai_insights_lens (lens_id, created_at DESC)

Resource profiles (linked to Jira users or placeholder roles).

ColumnTypeDefaultDescription
idVARCHAR(36) PKUUID v4
jira_account_idVARCHAR(128)Jira account ID (null for placeholders)
display_nameVARCHAR(255)Display name
emailVARCHAR(255)Email address
avatar_urlVARCHAR(512)Avatar URL
hours_per_dayDECIMAL(4,2)8.00Available hours per day
efficiency_multiplierDECIMAL(3,2)1.00Productivity multiplier
is_placeholderBOOLEANFALSEWhether this is a placeholder resource
placeholder_roleVARCHAR(255)Role name (for placeholders)
is_activeBOOLEANTRUEActive status
created_atTIMESTAMPCURRENT_TIMESTAMP
updated_atTIMESTAMPCURRENT_TIMESTAMP

Indexes:

  • idx_resources_account (jira_account_id) UNIQUE

Work schedule templates (hours per weekday).

ColumnTypeDefault
idVARCHAR(36) PK
nameVARCHAR(255) NOT NULL
hours_monday - hours_sundayDECIMAL(4,2)8.00 (M-F), 0.00 (S-S)
is_defaultBOOLEANFALSE

Links resources to work schedules for specific date ranges.

ColumnType
idVARCHAR(36) PK
resource_idVARCHAR(36) NOT NULL
schedule_idVARCHAR(36) NOT NULL
start_dateDATE NOT NULL
end_dateDATE

Team definitions.

ColumnTypeDefault
idVARCHAR(36) PK
nameVARCHAR(255) NOT NULL
descriptionVARCHAR(512)
colorVARCHAR(7)'#4C9AFF'
is_globalBOOLEANTRUE
lens_idVARCHAR(36)(null for global teams)
default_hourly_rateDECIMAL(10,2)
created_at / updated_atTIMESTAMPCURRENT_TIMESTAMP

Team member assignments with availability.

ColumnTypeDefault
idVARCHAR(36) PK
team_idVARCHAR(36) NOT NULL
resource_idVARCHAR(36) NOT NULL
availability_pctDECIMAL(5,2)100.00
start_dateDATE NOT NULL
end_dateDATE

Named holiday calendars (optionally region-specific).

ColumnType
idVARCHAR(36) PK
nameVARCHAR(255) NOT NULL
regionVARCHAR(64)
is_defaultBOOLEAN (default FALSE)
created_atTIMESTAMP

Individual holiday entries in a calendar.

ColumnType
idVARCHAR(36) PK
calendar_idVARCHAR(36) NOT NULL
holiday_dateDATE NOT NULL
nameVARCHAR(255)

Resource absence records (vacation, sick leave, etc.).

ColumnTypeDefault
idVARCHAR(36) PK
resource_idVARCHAR(36) NOT NULL
start_dateDATE NOT NULL
end_dateDATE NOT NULL
is_half_dayBOOLEANFALSE
absence_typeVARCHAR(64)'vacation'
notesVARCHAR(512)
created_atTIMESTAMPCURRENT_TIMESTAMP

Links resources to holiday calendars.

ColumnType
idVARCHAR(36) PK
resource_idVARCHAR(36) NOT NULL
calendar_idVARCHAR(36) NOT NULL
start_dateDATE
end_dateDATE

Point-in-time snapshots for schedule comparison.

ColumnType
idVARCHAR(36) PK
lens_idVARCHAR(36) NOT NULL
nameVARCHAR(255) NOT NULL
created_byVARCHAR(128)
created_atTIMESTAMP

Individual node snapshots within a baseline.

ColumnType
idVARCHAR(36) PK
baseline_idVARCHAR(36) NOT NULL
node_idVARCHAR(36) NOT NULL
jira_issue_keyVARCHAR(32)
start_dateDATE
end_dateDATE

Skill taxonomy (global, not per-lens).

ColumnTypeDefault
idVARCHAR(36) PK
nameVARCHAR(255) NOT NULL
categoryVARCHAR(128)
colorVARCHAR(7)'#8777D9'
created_atTIMESTAMPCURRENT_TIMESTAMP

Indexes:

  • idx_skills_name (name) UNIQUE

Links resources to skills with proficiency levels.

ColumnTypeDefault
idVARCHAR(36) PK
resource_idVARCHAR(36) NOT NULL
skill_idVARCHAR(36) NOT NULL
proficiency_levelSMALLINT3 (scale 1-5)
start_dateDATE
end_dateDATE

Async resource leveling job tracking.

ColumnTypeDefault
idVARCHAR(36) PK
lens_idVARCHAR(36) NOT NULL
statusVARCHAR(16)'pending'
started_atTIMESTAMP
completed_atTIMESTAMP
tasks_leveledINT0
error_messageTEXT
result_jsonTEXT
created_byVARCHAR(128)
created_atTIMESTAMPCURRENT_TIMESTAMP

Per-lens dependency metadata (lag days, dependency type).

ColumnTypeDefault
idVARCHAR(36) PK
lens_idVARCHAR(36) NOT NULL
from_issue_keyVARCHAR(32) NOT NULL
to_issue_keyVARCHAR(32) NOT NULL
jira_link_idVARCHAR(64)Jira issue link ID
dependency_typeVARCHAR(2) NOT NULL'FS' (FS, SS, FF, SF)
lag_daysINT NOT NULL0
updated_byVARCHAR(128)
updated_atTIMESTAMPCURRENT_TIMESTAMP

Indexes:

  • idx_dep_lag_unique (lens_id, from_issue_key, to_issue_key) UNIQUE
  • idx_dep_lag_link_id (jira_link_id)

Resolver performance metrics.

ColumnTypeDefault
idINT AUTO_INCREMENT PK
metric_nameVARCHAR(128) NOT NULL
metric_valueFLOAT NOT NULL
metric_typeVARCHAR(32)'gauge'
tags_jsonTEXT
recorded_atTIMESTAMPCURRENT_TIMESTAMP

Daily health snapshots for trend tracking.

ColumnTypeDefault
idVARCHAR(64) PK
lens_idVARCHAR(64) NOT NULL
snapshot_dateVARCHAR(10) NOT NULLYYYY-MM-DD
total_itemsINTEGER0
completed_itemsINTEGER0
overdue_itemsINTEGER0
unassigned_itemsINTEGER0
total_pointsREAL0
completed_pointsREAL0
health_scoreINTEGER0
status_breakdownTEXTJSON
assignee_breakdownTEXTJSON
created_atBIGINT NOT NULLUnix timestamp

User-configured automation rules.

ColumnTypeDefault
idVARCHAR(64) PK
lens_idVARCHAR(64) NOT NULL
rule_typeVARCHAR(64) NOT NULL
configTEXT NOT NULLJSON
created_byVARCHAR(128) NOT NULL
enabledINTEGER1
last_triggered_atINTEGERUnix timestamp
trigger_countINTEGER0
created_atINTEGER NOT NULLUnix timestamp

System-generated and user-visible alerts.

ColumnTypeDefault
idVARCHAR(64) PK
lens_idVARCHAR(64) NOT NULL
account_idVARCHAR(128) NOT NULLTarget user
alert_typeVARCHAR(64) NOT NULL
severityVARCHAR(16) NOT NULL
titleVARCHAR(512) NOT NULL
detailTEXT
related_issue_keyVARCHAR(32)
suggested_actionTEXT
acknowledgedINTEGER0
created_atINTEGER NOT NULLUnix timestamp
acknowledged_atINTEGER

  1. ensureSchema() runs on every Forge cold-start (called by withSchema guard or inline in resolvers)
  2. It reads the current schema version from KVS key foundation:schema_version
  3. If the version is current, it returns immediately
  4. If no version exists (fresh install), it runs createFreshSchema() which executes all CREATE TABLE IF NOT EXISTS statements
  5. If an older version exists, it calls runMigrations(currentVersion, CURRENT_SCHEMA_VERSION) which sequentially executes each migration’s up() function
  6. A KVS lease (lease:foundation:schema-bootstrap) prevents concurrent bootstrap from multiple cold-starts

Checklist:

  1. Bump CURRENT_SCHEMA_VERSION in foundation/src/db/migrations.ts
  2. Add ONE migration entry to the migrations array:
    {
    version: 33,
    description: 'Add new_column to some_table',
    up: async () => {
    await safeDDL('ALTER TABLE some_table ADD COLUMN new_column VARCHAR(128)');
    },
    },
  3. Add the column to the fresh-install CREATE TABLE in foundation/src/db/schema.ts
  4. Use safeDDL() for ALTER TABLE / RENAME TABLE / CHANGE COLUMN (idempotent on re-run)
  5. Use sql.executeDDL() for CREATE TABLE IF NOT EXISTS (inherently idempotent)
  6. Run migration tests: cd foundation && npx jest --testPathPattern='schema|migration' --no-coverage
FunctionUse ForBehavior on Re-run
safeDDL()ALTER TABLE, RENAME TABLE, CHANGE COLUMNSwallows errors (idempotent)
sql.executeDDL()CREATE TABLE IF NOT EXISTS, CREATE INDEX IF NOT EXISTSInherently idempotent
VersionDescription
v14Renamed structures -> lenses, structure_id -> lens_id across 9 tables
v15Promoted start_date from custom_fields JSON to dedicated column
v23Added numeric_id to lenses with sequential backfill
v27Deduplicated same-lens issue nodes + added UNIQUE constraint
v30Collapsed edit_generators permission level into edit
v31Deduplicated everyone grants (NULL != NULL workaround)
v32Added last_accessed_at for auto-expiry

All structured, relational data that needs querying, indexing, or joins:

  • Lenses, hierarchy nodes, generators, views, permissions
  • Issue cache, dependency overrides, baselines
  • Resources, teams, schedules, skills, absences
  • AI insights, alerts, automation rules, monitoring metrics, snapshots

Ephemeral, per-user, or counter data that doesn’t need relational queries:

Key PatternDataTTL
foundation:schema_versionCurrent schema version (integer)Permanent
user_prefs:${accountId}User preferences (JSON)Permanent
announcements_dismissed:${accountId}Dismissed announcement IDs (array)Permanent
support_chat:${accountId}Support chat messagesPermanent
rate_limit:hourly:*Hourly point usage shards1 hour (logical)
tier:budget_overrideAdmin-set tier budgetPermanent
active-gen-job:${lensId}Active sync job marker15 min (logical)
job:${jobId}Async job statusPermanent
cacheRefreshJob:${jobId}Cache refresh job statusPermanent
admin_refresh_last:globalLast cache refresh timestampPermanent
admin:css_overrideCustom CSS (string)Permanent
admin:reset_in_progressReset crash recovery flagCleared on completion
project_components:${projectKey}Cached project components1 hour TTL
project_versions:${projectKey}Cached project versions1 hour TTL
last-issue-event-ts:${projectKey}Last issue event timestampPermanent
browse_check:${project}:${user}BROWSE permission cache30 min TTL
feature_flags:*Feature flag valuesPermanent
lease:*Distributed lock leasesTTL-based