Linear Database Schema
Database entities and querying synced data
The Linear plugin automatically syncs data from Linear to your database whenever you make API calls. This allows you to query Linear data efficiently without making additional API requests.
New to Corsair? Learn about database operations, data synchronization, and multi-tenancy.
Full Implementation: See the Linear plugin source code.
Synced Entities
The Linear plugin syncs the following entities to your database:
- issues - Issue tracking data (synced on list, get, create, update operations)
- teams - Team information (synced on list, get operations)
- projects - Project data (synced on list, get, create, update operations)
- comments - Comment data (synced on list, create, update operations)
- users - User information (synced as part of other entities)
Data is automatically synced whenever you fetch, create, or update entities through the API. Webhook events do not automatically sync to the database.
Database API
Access synced data through the database API:
// Search for entities
const issues = await corsair.linear.db.issues.search({
data: { teamId: "team-id" },
});
// Get by entity ID
const issue = await corsair.linear.db.issues.getByEntityId("issue-id");
// List all entities (with pagination)
const allProjects = await corsair.linear.db.projects.list({
limit: 50,
offset: 0,
});Issues
Issues are the core work items in Linear representing tasks, bugs, and features.
Schema
{
id: string; // Internal database ID
entityId: string; // Linear issue ID
tenantId: string; // Tenant ID for multi-tenancy
// Core fields
title: string; // Issue title
description: string?; // Issue description (markdown)
number: number; // Issue number (e.g., 123)
identifier: string; // Issue identifier (e.g., "ENG-123")
url: string; // Linear URL to the issue
// Status and priority
priority: 0 | 1 | 2 | 3 | 4; // Priority level
estimate: number?; // Estimate points
sortOrder: number?; // Sort order in lists
// Relations
stateId: string; // Workflow state ID
teamId: string; // Team ID
assigneeId: string?; // Assigned user ID
creatorId: string; // Creator user ID
projectId: string?; // Project ID (if in a project)
cycleId: string?; // Cycle ID (if in a cycle)
parentId: string?; // Parent issue ID (for sub-issues)
// Dates
dueDate: string?; // Due date
startedAt: string?; // When work started
completedAt: string?; // When completed
canceledAt: string?; // When canceled
createdAt: Date; // When created
updatedAt: Date; // Last updated
archivedAt: string?; // When archived
}Querying Issues
Search by team:
const teamIssues = await corsair.linear.db.issues.search({
data: { teamId: "team-id" },
});Search by assignee:
const myIssues = await corsair.linear.db.issues.search({
data: { assigneeId: "user-id" },
});Search by priority:
const urgentIssues = await corsair.linear.db.issues.search({
data: { priority: 1 },
});Get specific issue:
const issue = await corsair.linear.db.issues.getByEntityId("issue-id"); Example: Find Overdue Issues
Find all issues past their due date:
const allIssues = await corsair.linear.db.issues.list();
const overdueIssues = allIssues.filter(issue => {
if (!issue.dueDate || issue.completedAt || issue.canceledAt) {
return false;
}
return new Date(issue.dueDate) < new Date();
});
console.log(`Found ${overdueIssues.length} overdue issues`);Example: Issues by Project
Find all issues in a specific project:
const projectIssues = await corsair.linear.db.issues.search({
data: { projectId: "project-id" },
});
console.log(`Project has ${projectIssues.length} issues`);Teams
Teams represent groups of users that collaborate on issues and projects.
Schema
{
id: string; // Internal database ID
entityId: string; // Linear team ID
tenantId: string; // Tenant ID for multi-tenancy
// Core fields
name: string; // Team name
key: string; // Team key (e.g., "ENG")
description: string?; // Team description
icon: string?; // Team icon
color: string?; // Team color
private: boolean; // Whether team is private
// Dates
createdAt: Date; // When created
updatedAt: Date; // Last updated
archivedAt: string?; // When archived
}Querying Teams
List all teams:
const teams = await corsair.linear.db.teams.list();Get specific team:
const team = await corsair.linear.db.teams.getByEntityId("team-id"); Search by key:
const engTeam = await corsair.linear.db.teams.search({
data: { key: "ENG" },
});Example: Team Statistics
Calculate issue counts per team:
const teams = await corsair.linear.db.teams.list();
const allIssues = await corsair.linear.db.issues.list();
const teamStats = teams.map(team => {
const issues = allIssues.filter(i => i.teamId === team.entityId);
const completed = issues.filter(i => i.completedAt);
return {
team: team.name,
total: issues.length,
completed: completed.length,
inProgress: issues.length - completed.length,
};
});
console.table(teamStats);Projects
Projects help organize and track work across multiple issues and teams.
Schema
{
id: string; // Internal database ID
entityId: string; // Linear project ID
tenantId: string; // Tenant ID for multi-tenancy
// Core fields
name: string; // Project name
description: string?; // Project description
icon: string?; // Project icon (emoji)
color: string?; // Project color (hex)
// Status
state: 'planned' | 'started' | 'paused' | 'completed' | 'canceled' | 'backlog';
priority: number; // Project priority
sortOrder: number?; // Sort order in lists
// Relations
leadId: string?; // Project lead user ID
// Dates
startDate: string?; // Planned start date
targetDate: string?; // Target completion date
completedAt: string?; // When completed
canceledAt: string?; // When canceled
createdAt: Date; // When created
updatedAt: Date; // Last updated
archivedAt: string?; // When archived
}Querying Projects
Search by state:
const activeProjects = await corsair.linear.db.projects.search({
data: { state: "started" },
});Search by lead:
const myProjects = await corsair.linear.db.projects.search({
data: { leadId: "user-id" },
});Get specific project:
const project = await corsair.linear.db.projects.getByEntityId("project-id"); Example: Project Dashboard
Build a project dashboard with statistics:
const projects = await corsair.linear.db.projects.search({
data: { state: "started" },
});
const allIssues = await corsair.linear.db.issues.list();
const dashboard = await Promise.all(
projects.map(async project => {
const issues = allIssues.filter(i => i.projectId === project.entityId);
const completed = issues.filter(i => i.completedAt);
const inProgress = issues.filter(i => i.startedAt && !i.completedAt);
return {
name: project.name,
state: project.state,
targetDate: project.targetDate,
total: issues.length,
completed: completed.length,
inProgress: inProgress.length,
progress: issues.length > 0
? Math.round((completed.length / issues.length) * 100)
: 0,
};
})
);
console.table(dashboard);Example: Overdue Projects
Find projects past their target date:
const projects = await corsair.linear.db.projects.search({
data: { state: "started" },
});
const overdueProjects = projects.filter(project => {
if (!project.targetDate) return false;
return new Date(project.targetDate) < new Date();
});
for (const project of overdueProjects) {
console.log(`⚠️ ${project.name} is overdue (target: ${project.targetDate})`);
}Comments
Comments enable discussion and collaboration on issues.
Schema
{
id: string; // Internal database ID
entityId: string; // Linear comment ID
tenantId: string; // Tenant ID for multi-tenancy
// Core fields
body: string; // Comment body (markdown)
// Relations
issueId: string; // Issue the comment belongs to
userId: string; // User who created the comment
parentId: string?; // Parent comment ID (for replies)
// Dates
editedAt: string?; // When last edited
createdAt: Date; // When created
updatedAt: Date; // Last updated
archivedAt: string?; // When archived
}Querying Comments
Search by issue:
const issueComments = await corsair.linear.db.comments.search({
data: { issueId: "issue-id" },
});Search by user:
const userComments = await corsair.linear.db.comments.search({
data: { userId: "user-id" },
});Get specific comment:
const comment = await corsair.linear.db.comments.getByEntityId("comment-id"); Example: Comment Activity Report
Generate a report of commenting activity:
const comments = await corsair.linear.db.comments.list();
// Group by user
const userActivity = comments.reduce((acc, comment) => {
acc[comment.userId] = (acc[comment.userId] || 0) + 1;
return acc;
}, {} as Record<string, number>);
// Sort by activity
const topCommenters = Object.entries(userActivity)
.sort(([, a], [, b]) => b - a)
.slice(0, 10);
console.log("Top 10 commenters:");
topCommenters.forEach(([userId, count]) => {
console.log(` ${userId}: ${count} comments`);
});Example: Find Recent Discussions
Find issues with recent comment activity:
const recentComments = await corsair.linear.db.comments.list();
// Get comments from last 7 days
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
const recentActivity = recentComments.filter(
comment => comment.createdAt >= sevenDaysAgo
);
// Group by issue
const issueActivity = recentActivity.reduce((acc, comment) => {
acc[comment.issueId] = (acc[comment.issueId] || 0) + 1;
return acc;
}, {} as Record<string, number>);
console.log(`${Object.keys(issueActivity).length} issues with recent comments`);Users
User data is synced as part of other entities (issues, comments, projects).
Schema
{
id: string; // Internal database ID
entityId: string; // Linear user ID
tenantId: string; // Tenant ID for multi-tenancy
// Core fields
name: string; // User name
email: string?; // User email
displayName: string; // Display name
avatarUrl: string?; // Avatar URL
active: boolean; // Whether user is active
admin: boolean; // Whether user is admin
// Dates
createdAt: Date; // When created
updatedAt: Date; // Last updated
}Querying Users
List all users:
const users = await corsair.linear.db.users.list();Get specific user:
const user = await corsair.linear.db.users.getByEntityId("user-id"); Search by email:
const user = await corsair.linear.db.users.search({
data: { email: "user@example.com" },
});Multi-Tenancy
All database operations are automatically scoped to the current tenant:
// Each tenant has isolated data
const tenant1 = corsair.withTenant("tenant-1");
const tenant2 = corsair.withTenant("tenant-2");
// These return different results
const tenant1Issues = await tenant1.linear.db.issues.list();
const tenant2Issues = await tenant2.linear.db.issues.list();See Multi-Tenancy for more details.
Best Practices
1. Query Database First
For data you've already fetched, query the database instead of making API calls:
// ❌ Unnecessary API call
const issue = await corsair.linear.api.issues.get({ id: "issue-id" });
// ✅ Query database
const issue = await corsair.linear.db.issues.getByEntityId("issue-id");2. Sync Before Complex Queries
For comprehensive reports, ensure data is synced:
// Sync latest issues
await corsair.linear.api.issues.list({ first: 100 });
// Now query database
const issues = await corsair.linear.db.issues.list();3. Use Pagination for Large Datasets
When working with large datasets, use pagination:
const PAGE_SIZE = 100;
let offset = 0;
let hasMore = true;
while (hasMore) {
const page = await corsair.linear.db.issues.list({
limit: PAGE_SIZE,
offset,
});
// Process page
for (const issue of page) {
// ... process issue
}
hasMore = page.length === PAGE_SIZE;
offset += PAGE_SIZE;
}4. Cache Complex Queries
For frequently accessed data, consider caching:
let cachedStats: any = null;
let cacheTime = 0;
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
async function getTeamStats() {
if (cachedStats && Date.now() - cacheTime < CACHE_TTL) {
return cachedStats;
}
const teams = await corsair.linear.db.teams.list();
const issues = await corsair.linear.db.issues.list();
cachedStats = calculateStats(teams, issues);
cacheTime = Date.now();
return cachedStats;
}