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)
Table of Contents
Section titled “Table of Contents”- Entity Relationships
- Tables
- lenses
- hierarchy_nodes
- generators
- issue_cache
- views
- lens_permissions
- ai_insights
- resources
- work_schedules
- resource_schedules
- teams
- team_memberships
- holiday_calendars
- holiday_dates
- resource_absences
- resource_calendar_assignments
- baselines
- baseline_entries
- skills
- resource_skills
- leveling_jobs
- dependency_lag_overrides
- monitoring_metrics
- lens_snapshots
- automation_rules
- alerts
- Migration System
- Storage Split: SQL vs KVS
Entity Relationships
Section titled “Entity Relationships”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_datesTables
Section titled “Tables”lenses
Section titled “lenses”Top-level entity. Each lens is a curated view of Jira issues organized as a hierarchy tree.
| Column | Type | Default | Description |
|---|---|---|---|
id | CHAR(36) PK | UUID v4 | |
numeric_id | INT UNIQUE | Auto-assigned sequential ID for friendly URLs | |
name | VARCHAR(255) NOT NULL | Display name | |
description | TEXT | Optional description | |
mode | VARCHAR(16) | 'power' | Mode: power, deleting |
owner_account_id | VARCHAR(128) NOT NULL | Jira account ID of the owner | |
item_limit | INT | 1000 | Max issues allowed in this lens |
archived | INTEGER | 0 | 0=active, 1=archived |
created_at | TIMESTAMP | CURRENT_TIMESTAMP | |
updated_at | TIMESTAMP | CURRENT_TIMESTAMP |
hierarchy_nodes
Section titled “hierarchy_nodes”Tree structure for a lens. Each node is either a Jira issue, a flex item (grouping node), a milestone, or a sync agent group.
| Column | Type | Default | Description |
|---|---|---|---|
id | CHAR(36) PK | UUID v4 | |
lens_id | CHAR(36) NOT NULL | Parent lens | |
parent_id | CHAR(36) | Parent node (null = root level) | |
position | INT NOT NULL | Sort order within parent (gap=100 strategy) | |
node_type | VARCHAR(32) NOT NULL | issue, flex, generator_group, milestone | |
jira_issue_id | VARCHAR(64) | Jira issue ID (for issue nodes) | |
jira_issue_key | VARCHAR(32) | Jira issue key (e.g., PROJ-123) | |
flex_name | VARCHAR(255) | Display name (for flex/milestone nodes) | |
flex_description | TEXT | Description (for flex nodes) | |
flex_icon | VARCHAR(128) | Icon identifier (for flex nodes) | |
hidden | BOOLEAN | FALSE | Hidden by filter sync agents |
generator_id | CHAR(36) | Links to the sync agent that created this node | |
milestone_date | DATE | Target date (for milestone nodes) | |
scheduling_mode | VARCHAR(16) | 'auto_bottom_up' | Scheduling mode for auto-scheduling |
leveling_delay_days | INT | 0 | Leveling delay in days |
depth | INT NOT NULL | 0 | Tree depth (0 = root) |
created_by | VARCHAR(128) | Jira account ID of creator | |
created_at | TIMESTAMP | CURRENT_TIMESTAMP | |
updated_at | TIMESTAMP | CURRENT_TIMESTAMP |
Indexes:
idx_lens_parent (lens_id, parent_id, position)— tree traversalidx_lens_issue (lens_id, jira_issue_id)UNIQUE — prevents duplicate issues per lensidx_jira_key (jira_issue_key)— issue key lookupsidx_hierarchy_generator_id (generator_id)— sync agent lookups
generators
Section titled “generators”Sync agent configurations. Each generator defines a JQL query or rule that populates a lens.
| Column | Type | Default | Description |
|---|---|---|---|
id | CHAR(36) PK | UUID v4 | |
lens_id | CHAR(36) NOT NULL | Parent lens | |
parent_node_id | CHAR(36) | Target parent node for generated issues | |
generator_type | VARCHAR(32) NOT NULL | Type: jql_insert, child_extend, hierarchy_builder, jpd_links, auto_project, etc. | |
position | INT NOT NULL | Execution order | |
config | JSON NOT NULL | Type-specific config (JQL query, project key, etc.) | |
created_by | VARCHAR(128) | Creator’s account ID | |
last_executed_at | TIMESTAMP | Last execution timestamp | |
last_execution_status | VARCHAR(16) | success, error, partial | |
last_execution_message | TEXT | Human-readable status message | |
last_known_total | INT | NULL | JQL result count (for freshness checks) |
created_at | TIMESTAMP | CURRENT_TIMESTAMP |
Indexes:
idx_lens (lens_id)
issue_cache
Section titled “issue_cache”Local cache of Jira issue data. Kept fresh by product event triggers. This is the primary read source for the frontend.
| Column | Type | Default | Description |
|---|---|---|---|
jira_issue_id | VARCHAR(64) PK | Jira issue ID | |
jira_issue_key | VARCHAR(32) NOT NULL | Issue key (e.g., PROJ-123) | |
project_id | VARCHAR(64) NOT NULL | Jira project ID | |
project_key | VARCHAR(16) NOT NULL | Project key (e.g., PROJ) | |
issue_type_id | VARCHAR(32) | Issue type ID | |
issue_type_name | VARCHAR(128) | Issue type name (Story, Bug, etc.) | |
summary | VARCHAR(512) NOT NULL | Issue summary/title | |
status_id | VARCHAR(32) | Status ID | |
status_name | VARCHAR(128) | Status display name | |
status_category | VARCHAR(32) | new, indeterminate, done | |
assignee_account_id | VARCHAR(128) | Assignee’s Jira account ID | |
assignee_display_name | VARCHAR(255) | Assignee name | |
assignee_avatar_url | VARCHAR(512) | Assignee avatar URL | |
reporter_account_id | VARCHAR(128) | Reporter’s Jira account ID | |
reporter_display_name | VARCHAR(255) | Reporter name | |
reporter_avatar_url | VARCHAR(512) | Reporter avatar URL | |
resolution_id | VARCHAR(32) | Resolution ID | |
resolution_name | VARCHAR(128) | Resolution name (Done, Won’t Do, etc.) | |
resolution_date | TIMESTAMP | When the issue was resolved | |
priority_id | VARCHAR(16) | Priority ID | |
priority_name | VARCHAR(64) | Priority name (Highest, High, etc.) | |
sprint_id | VARCHAR(32) | Current sprint ID | |
sprint_name | VARCHAR(128) | Current sprint name | |
sprint_state | VARCHAR(32) | active, closed, future | |
parent_issue_id | VARCHAR(64) | Jira parent issue ID (Epic) | |
story_points | DECIMAL(10,2) | Story points / estimation | |
labels | JSON | Array of label strings | |
components | JSON | Array of component objects | |
fix_versions | JSON | Array of fix version objects | |
affects_versions | JSON | Array of affects version objects | |
due_date | DATE | Due date | |
start_date | DATE | Start date (promoted from customfield_10015 in v15) | |
created_at | TIMESTAMP | Issue creation date in Jira | |
updated_at | TIMESTAMP | Last update date in Jira | |
cached_at | TIMESTAMP | CURRENT_TIMESTAMP | When this cache entry was last refreshed |
custom_fields | JSON | All non-standard custom fields as JSON blob | |
links_json | TEXT | Issue links as JSON array | |
description_text | TEXT | Plain text from ADF description (max 500 chars) | |
environment_text | TEXT | Plain text from ADF environment (max 500 chars) | |
original_estimate_seconds | INT | Original time estimate | |
remaining_estimate_seconds | INT | Remaining time estimate | |
time_spent_seconds | INT | Time spent | |
hierarchy_level | INT | Jira hierarchy level (-1=Subtask, 0=Story, 1=Epic) | |
last_accessed_at | TIMESTAMP | CURRENT_TIMESTAMP | For auto-expiry (v32) |
Indexes:
idx_project (project_key)— project-scoped queriesidx_parent (parent_issue_id)— parent issue lookupsidx_key (jira_issue_key)— key-based lookups
Saved view configurations for a lens (column order, formatting, Gantt settings).
| Column | Type | Default | Description |
|---|---|---|---|
id | CHAR(36) PK | UUID v4 | |
lens_id | CHAR(36) NOT NULL | Parent lens | |
name | VARCHAR(255) NOT NULL | View display name | |
is_default | BOOLEAN | FALSE | Whether this is the default view |
visibility | VARCHAR(16) | 'public' | public or private |
owner_account_id | VARCHAR(128) NOT NULL | Creator’s account ID | |
columns | JSON NOT NULL | Column configuration array | |
settings_json | TEXT | Additional settings (density, Gantt config, formatting, etc.) | |
created_at | TIMESTAMP | CURRENT_TIMESTAMP | |
updated_at | TIMESTAMP | CURRENT_TIMESTAMP |
Indexes:
idx_lens (lens_id)
lens_permissions
Section titled “lens_permissions”Access control list for lenses.
| Column | Type | Default | Description |
|---|---|---|---|
id | CHAR(36) PK | UUID v4 | |
lens_id | CHAR(36) NOT NULL | Target lens | |
grantee_type | VARCHAR(16) NOT NULL | user, group, role, everyone | |
grantee_id | VARCHAR(128) | Account ID, group name, or role name (null for everyone) | |
permission_level | VARCHAR(32) NOT NULL | view, edit, control | |
created_at | TIMESTAMP | CURRENT_TIMESTAMP |
Indexes:
idx_lens (lens_id)idx_unique_grant (lens_id, grantee_type, grantee_id)UNIQUE — note: NULL != NULL in SQL, soeveryonegrants need separate dedup (see v31 migration)
ai_insights
Section titled “ai_insights”AI-generated portfolio insights.
| Column | Type | Description |
|---|---|---|
id | CHAR(36) PK | UUID v4 |
lens_id | CHAR(36) NOT NULL | Target lens |
insight_type | VARCHAR(32) NOT NULL | Type of insight |
severity | VARCHAR(16) NOT NULL | info, warning, critical |
summary | TEXT NOT NULL | Human-readable summary |
affected_nodes | TEXT | JSON array of affected node IDs |
suggested_action | TEXT | JSON suggested action object |
created_at | BIGINT NOT NULL | Unix timestamp (ms) |
resolved_at | BIGINT | Unix timestamp when resolved |
Indexes:
idx_ai_insights_lens (lens_id, created_at DESC)
resources
Section titled “resources”Resource profiles (linked to Jira users or placeholder roles).
| Column | Type | Default | Description |
|---|---|---|---|
id | VARCHAR(36) PK | UUID v4 | |
jira_account_id | VARCHAR(128) | Jira account ID (null for placeholders) | |
display_name | VARCHAR(255) | Display name | |
email | VARCHAR(255) | Email address | |
avatar_url | VARCHAR(512) | Avatar URL | |
hours_per_day | DECIMAL(4,2) | 8.00 | Available hours per day |
efficiency_multiplier | DECIMAL(3,2) | 1.00 | Productivity multiplier |
is_placeholder | BOOLEAN | FALSE | Whether this is a placeholder resource |
placeholder_role | VARCHAR(255) | Role name (for placeholders) | |
is_active | BOOLEAN | TRUE | Active status |
created_at | TIMESTAMP | CURRENT_TIMESTAMP | |
updated_at | TIMESTAMP | CURRENT_TIMESTAMP |
Indexes:
idx_resources_account (jira_account_id)UNIQUE
work_schedules
Section titled “work_schedules”Work schedule templates (hours per weekday).
| Column | Type | Default |
|---|---|---|
id | VARCHAR(36) PK | |
name | VARCHAR(255) NOT NULL | |
hours_monday - hours_sunday | DECIMAL(4,2) | 8.00 (M-F), 0.00 (S-S) |
is_default | BOOLEAN | FALSE |
resource_schedules
Section titled “resource_schedules”Links resources to work schedules for specific date ranges.
| Column | Type |
|---|---|
id | VARCHAR(36) PK |
resource_id | VARCHAR(36) NOT NULL |
schedule_id | VARCHAR(36) NOT NULL |
start_date | DATE NOT NULL |
end_date | DATE |
Team definitions.
| Column | Type | Default |
|---|---|---|
id | VARCHAR(36) PK | |
name | VARCHAR(255) NOT NULL | |
description | VARCHAR(512) | |
color | VARCHAR(7) | '#4C9AFF' |
is_global | BOOLEAN | TRUE |
lens_id | VARCHAR(36) | (null for global teams) |
default_hourly_rate | DECIMAL(10,2) | |
created_at / updated_at | TIMESTAMP | CURRENT_TIMESTAMP |
team_memberships
Section titled “team_memberships”Team member assignments with availability.
| Column | Type | Default |
|---|---|---|
id | VARCHAR(36) PK | |
team_id | VARCHAR(36) NOT NULL | |
resource_id | VARCHAR(36) NOT NULL | |
availability_pct | DECIMAL(5,2) | 100.00 |
start_date | DATE NOT NULL | |
end_date | DATE |
holiday_calendars
Section titled “holiday_calendars”Named holiday calendars (optionally region-specific).
| Column | Type |
|---|---|
id | VARCHAR(36) PK |
name | VARCHAR(255) NOT NULL |
region | VARCHAR(64) |
is_default | BOOLEAN (default FALSE) |
created_at | TIMESTAMP |
holiday_dates
Section titled “holiday_dates”Individual holiday entries in a calendar.
| Column | Type |
|---|---|
id | VARCHAR(36) PK |
calendar_id | VARCHAR(36) NOT NULL |
holiday_date | DATE NOT NULL |
name | VARCHAR(255) |
resource_absences
Section titled “resource_absences”Resource absence records (vacation, sick leave, etc.).
| Column | Type | Default |
|---|---|---|
id | VARCHAR(36) PK | |
resource_id | VARCHAR(36) NOT NULL | |
start_date | DATE NOT NULL | |
end_date | DATE NOT NULL | |
is_half_day | BOOLEAN | FALSE |
absence_type | VARCHAR(64) | 'vacation' |
notes | VARCHAR(512) | |
created_at | TIMESTAMP | CURRENT_TIMESTAMP |
resource_calendar_assignments
Section titled “resource_calendar_assignments”Links resources to holiday calendars.
| Column | Type |
|---|---|
id | VARCHAR(36) PK |
resource_id | VARCHAR(36) NOT NULL |
calendar_id | VARCHAR(36) NOT NULL |
start_date | DATE |
end_date | DATE |
baselines
Section titled “baselines”Point-in-time snapshots for schedule comparison.
| Column | Type |
|---|---|
id | VARCHAR(36) PK |
lens_id | VARCHAR(36) NOT NULL |
name | VARCHAR(255) NOT NULL |
created_by | VARCHAR(128) |
created_at | TIMESTAMP |
baseline_entries
Section titled “baseline_entries”Individual node snapshots within a baseline.
| Column | Type |
|---|---|
id | VARCHAR(36) PK |
baseline_id | VARCHAR(36) NOT NULL |
node_id | VARCHAR(36) NOT NULL |
jira_issue_key | VARCHAR(32) |
start_date | DATE |
end_date | DATE |
skills
Section titled “skills”Skill taxonomy (global, not per-lens).
| Column | Type | Default |
|---|---|---|
id | VARCHAR(36) PK | |
name | VARCHAR(255) NOT NULL | |
category | VARCHAR(128) | |
color | VARCHAR(7) | '#8777D9' |
created_at | TIMESTAMP | CURRENT_TIMESTAMP |
Indexes:
idx_skills_name (name)UNIQUE
resource_skills
Section titled “resource_skills”Links resources to skills with proficiency levels.
| Column | Type | Default |
|---|---|---|
id | VARCHAR(36) PK | |
resource_id | VARCHAR(36) NOT NULL | |
skill_id | VARCHAR(36) NOT NULL | |
proficiency_level | SMALLINT | 3 (scale 1-5) |
start_date | DATE | |
end_date | DATE |
leveling_jobs
Section titled “leveling_jobs”Async resource leveling job tracking.
| Column | Type | Default |
|---|---|---|
id | VARCHAR(36) PK | |
lens_id | VARCHAR(36) NOT NULL | |
status | VARCHAR(16) | 'pending' |
started_at | TIMESTAMP | |
completed_at | TIMESTAMP | |
tasks_leveled | INT | 0 |
error_message | TEXT | |
result_json | TEXT | |
created_by | VARCHAR(128) | |
created_at | TIMESTAMP | CURRENT_TIMESTAMP |
dependency_lag_overrides
Section titled “dependency_lag_overrides”Per-lens dependency metadata (lag days, dependency type).
| Column | Type | Default |
|---|---|---|
id | VARCHAR(36) PK | |
lens_id | VARCHAR(36) NOT NULL | |
from_issue_key | VARCHAR(32) NOT NULL | |
to_issue_key | VARCHAR(32) NOT NULL | |
jira_link_id | VARCHAR(64) | Jira issue link ID |
dependency_type | VARCHAR(2) NOT NULL | 'FS' (FS, SS, FF, SF) |
lag_days | INT NOT NULL | 0 |
updated_by | VARCHAR(128) | |
updated_at | TIMESTAMP | CURRENT_TIMESTAMP |
Indexes:
idx_dep_lag_unique (lens_id, from_issue_key, to_issue_key)UNIQUEidx_dep_lag_link_id (jira_link_id)
monitoring_metrics
Section titled “monitoring_metrics”Resolver performance metrics.
| Column | Type | Default |
|---|---|---|
id | INT AUTO_INCREMENT PK | |
metric_name | VARCHAR(128) NOT NULL | |
metric_value | FLOAT NOT NULL | |
metric_type | VARCHAR(32) | 'gauge' |
tags_json | TEXT | |
recorded_at | TIMESTAMP | CURRENT_TIMESTAMP |
lens_snapshots
Section titled “lens_snapshots”Daily health snapshots for trend tracking.
| Column | Type | Default |
|---|---|---|
id | VARCHAR(64) PK | |
lens_id | VARCHAR(64) NOT NULL | |
snapshot_date | VARCHAR(10) NOT NULL | YYYY-MM-DD |
total_items | INTEGER | 0 |
completed_items | INTEGER | 0 |
overdue_items | INTEGER | 0 |
unassigned_items | INTEGER | 0 |
total_points | REAL | 0 |
completed_points | REAL | 0 |
health_score | INTEGER | 0 |
status_breakdown | TEXT | JSON |
assignee_breakdown | TEXT | JSON |
created_at | BIGINT NOT NULL | Unix timestamp |
automation_rules
Section titled “automation_rules”User-configured automation rules.
| Column | Type | Default |
|---|---|---|
id | VARCHAR(64) PK | |
lens_id | VARCHAR(64) NOT NULL | |
rule_type | VARCHAR(64) NOT NULL | |
config | TEXT NOT NULL | JSON |
created_by | VARCHAR(128) NOT NULL | |
enabled | INTEGER | 1 |
last_triggered_at | INTEGER | Unix timestamp |
trigger_count | INTEGER | 0 |
created_at | INTEGER NOT NULL | Unix timestamp |
alerts
Section titled “alerts”System-generated and user-visible alerts.
| Column | Type | Default |
|---|---|---|
id | VARCHAR(64) PK | |
lens_id | VARCHAR(64) NOT NULL | |
account_id | VARCHAR(128) NOT NULL | Target user |
alert_type | VARCHAR(64) NOT NULL | |
severity | VARCHAR(16) NOT NULL | |
title | VARCHAR(512) NOT NULL | |
detail | TEXT | |
related_issue_key | VARCHAR(32) | |
suggested_action | TEXT | |
acknowledged | INTEGER | 0 |
created_at | INTEGER NOT NULL | Unix timestamp |
acknowledged_at | INTEGER |
Migration System
Section titled “Migration System”How It Works
Section titled “How It Works”ensureSchema()runs on every Forge cold-start (called bywithSchemaguard or inline in resolvers)- It reads the current schema version from KVS key
foundation:schema_version - If the version is current, it returns immediately
- If no version exists (fresh install), it runs
createFreshSchema()which executes allCREATE TABLE IF NOT EXISTSstatements - If an older version exists, it calls
runMigrations(currentVersion, CURRENT_SCHEMA_VERSION)which sequentially executes each migration’sup()function - A KVS lease (
lease:foundation:schema-bootstrap) prevents concurrent bootstrap from multiple cold-starts
Adding a New Migration
Section titled “Adding a New Migration”Checklist:
- Bump
CURRENT_SCHEMA_VERSIONinfoundation/src/db/migrations.ts - Add ONE migration entry to the
migrationsarray:{version: 33,description: 'Add new_column to some_table',up: async () => {await safeDDL('ALTER TABLE some_table ADD COLUMN new_column VARCHAR(128)');},}, - Add the column to the fresh-install
CREATE TABLEinfoundation/src/db/schema.ts - Use
safeDDL()forALTER TABLE/RENAME TABLE/CHANGE COLUMN(idempotent on re-run) - Use
sql.executeDDL()forCREATE TABLE IF NOT EXISTS(inherently idempotent) - Run migration tests:
cd foundation && npx jest --testPathPattern='schema|migration' --no-coverage
safeDDL() vs sql.executeDDL()
Section titled “safeDDL() vs sql.executeDDL()”| Function | Use For | Behavior on Re-run |
|---|---|---|
safeDDL() | ALTER TABLE, RENAME TABLE, CHANGE COLUMN | Swallows errors (idempotent) |
sql.executeDDL() | CREATE TABLE IF NOT EXISTS, CREATE INDEX IF NOT EXISTS | Inherently idempotent |
Notable Migrations
Section titled “Notable Migrations”| Version | Description |
|---|---|
| v14 | Renamed structures -> lenses, structure_id -> lens_id across 9 tables |
| v15 | Promoted start_date from custom_fields JSON to dedicated column |
| v23 | Added numeric_id to lenses with sequential backfill |
| v27 | Deduplicated same-lens issue nodes + added UNIQUE constraint |
| v30 | Collapsed edit_generators permission level into edit |
| v31 | Deduplicated everyone grants (NULL != NULL workaround) |
| v32 | Added last_accessed_at for auto-expiry |
Storage Split: SQL vs KVS
Section titled “Storage Split: SQL vs KVS”Forge SQL
Section titled “Forge SQL”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
Forge KVS
Section titled “Forge KVS”Ephemeral, per-user, or counter data that doesn’t need relational queries:
| Key Pattern | Data | TTL |
|---|---|---|
foundation:schema_version | Current schema version (integer) | Permanent |
user_prefs:${accountId} | User preferences (JSON) | Permanent |
announcements_dismissed:${accountId} | Dismissed announcement IDs (array) | Permanent |
support_chat:${accountId} | Support chat messages | Permanent |
rate_limit:hourly:* | Hourly point usage shards | 1 hour (logical) |
tier:budget_override | Admin-set tier budget | Permanent |
active-gen-job:${lensId} | Active sync job marker | 15 min (logical) |
job:${jobId} | Async job status | Permanent |
cacheRefreshJob:${jobId} | Cache refresh job status | Permanent |
admin_refresh_last:global | Last cache refresh timestamp | Permanent |
admin:css_override | Custom CSS (string) | Permanent |
admin:reset_in_progress | Reset crash recovery flag | Cleared on completion |
project_components:${projectKey} | Cached project components | 1 hour TTL |
project_versions:${projectKey} | Cached project versions | 1 hour TTL |
last-issue-event-ts:${projectKey} | Last issue event timestamp | Permanent |
browse_check:${project}:${user} | BROWSE permission cache | 30 min TTL |
feature_flags:* | Feature flag values | Permanent |
lease:* | Distributed lock leases | TTL-based |