How to Design Linear — Project Management Platform: A Senior+ Guide
System Design Deep-Dive: Real-Time Sync, Workflow Engine, Keyboard-First UI & Scalable Architecture
1. Introduction: Linear at Speed
Linear has redefined what it means to manage software projects. In a world dominated by bloated, sluggish project management tools, Linear emerged as a lightning-fast, keyboard-first platform that treats speed as a feature, not an afterthought. Every pixel, every interaction, every data structure in Linear is designed around a single principle: eliminate friction between thought and action. When an engineer thinks "I need to create a bug," they should be able to press a keyboard shortcut, type a title, and hit enter — done. No page loads, no modal dialogs that freeze, no waiting for a spinner to resolve.
The platform was founded by Karri Saarinen and Tuomas Artman, both former Airbnb designers and engineers who had lived through the pain of using Jira, Asana, and other legacy tools at scale. They recognized that existing tools were built for managers who planned quarterly, not for engineers who shipped daily. Linear was designed from the ground up for product teams who value velocity, clarity, and minimal overhead. The result is a tool that feels like a native desktop application running in the browser — responsive, predictable, and extraordinarily fast.
At its core, Linear solves several fundamental problems in software project management. First, it eliminates the cognitive overhead of tool usage through its keyboard-first design. Second, it provides real-time synchronization so every team member sees the same state instantly. Third, it automates workflow transitions so engineers spend less time on bookkeeping and more time shipping. Fourth, it offers powerful filtering, views, and insights that help teams understand their velocity and bottlenecks without manual reporting.
The technical architecture behind Linear is fascinating. The client is built with React and optimized aggressively for performance. The backend uses a combination of PostgreSQL for persistent storage, Redis for caching and real-time pub/sub, and a custom GraphQL API that enables precise data fetching. Real-time synchronization is achieved through GraphQL subscriptions over WebSocket connections, with optimistic updates ensuring the UI responds instantly even before the server confirms changes.
What makes Linear particularly interesting from a system design perspective is its approach to "instant UI." When you navigate between views, create issues, or update statuses, the response is immediate. There is no loading spinner. This is achieved through a sophisticated client-side state management system that pre-fetches data, caches aggressively, and uses optimistic mutations to update the local state before network round-trips complete. The server acts as the source of truth, but the client never waits for it unnecessarily.
In this deep-dive, we will dissect every major subsystem of Linear's architecture. We will explore the data model that supports issues, projects, cycles, and views. We will examine the real-time sync engine that keeps thousands of collaborators in lockstep. We will design the workflow engine that automates state transitions. We will analyze the command palette that makes Linear feel like a keyboard-driven IDE. And we will discuss the integration layer that connects Linear with GitHub, GitLab, Slack, and other tools in the modern development stack.
By the end of this guide, you will have a comprehensive understanding of how to design a project management platform that rivals Linear in speed, scalability, and developer experience. Whether you are preparing for a system design interview, building your own project management tool, or simply curious about how top-tier SaaS products are architected, this guide will provide the depth and breadth you need.
Why Linear Wins
Linear's competitive advantage is not just features — it is the obsessive focus on performance and user experience. While Jira offers thousands of configuration options, Linear offers speed. While Asana provides visual project management, Linear provides keyboard-driven efficiency. This focus has attracted thousands of engineering teams who are willing to pay a premium for a tool that respects their time and attention. The platform manages over 100 million issues for tens of thousands of teams, processing millions of state transitions daily, all while maintaining sub-100ms response times for client interactions.
2. Platform Overview
Understanding Linear's architecture requires first understanding its domain model. Linear organizes work into a clear hierarchy: Workspaces contain Teams, which contain Projects, which contain Issues. Each entity has specific behaviors, permissions, and lifecycle rules that the system must enforce consistently.
Core Entities
A Workspace is the top-level organizational unit. It represents a company or organization and contains all teams, members, and settings. Workspaces are isolated from each other — data never crosses workspace boundaries. Each workspace has its own billing, authentication settings, and integration configurations.
Teams represent functional groups like "Frontend," "Backend," or "Infrastructure." Each team has its own issue workflow, labels, and members. Teams map directly to engineering squads in most organizations. A user can belong to multiple teams within a workspace, and each team membership has its own role (admin, member, guest).
Projects are collections of issues that represent a larger initiative. A project might be "v2.0 Release" or "Authentication Redesign." Projects have start and target dates, status indicators (planned, in progress, completed, canceled), and progress tracking based on the issues they contain.
Issues are the atomic unit of work. Each issue has a title, description, priority, status, assignee, labels, and a unique identifier (e.g., ENG-1234). Issues belong to a team and can optionally belong to a project. The status of an issue follows the team's workflow, moving through states like Todo, In Progress, In Review, and Done.
Cycles are time-boxed iterations, similar to sprints in Scrum. A cycle has a start date, end date, and a set of issues that the team commits to completing during that period. Cycles help teams maintain a steady velocity and provide a cadence for planning and retrospectives.
Views are saved filter configurations that allow users to quickly access specific slices of their data. For example, a view might show "all high-priority bugs assigned to me in the current cycle." Views can be shared with the team or kept private.
| Entity | Purpose | Key Properties | Hierarchy |
|---|---|---|---|
| Workspace | Top-level org | name, slug, billing | Root |
| Team | Engineering squad | name, key, workflow | Workspace → Team |
| Project | Initiative grouping | name, dates, status | Team → Project |
| Issue | Atomic work unit | title, priority, status | Team/Project → Issue |
| Cycle | Time-boxed iteration | start, end, issues | Team → Cycle |
| View | Saved filter | filters, share scope | User/Team → View |
Triage
Linear's triage system is a unique feature that helps teams manage incoming issues. When an issue is created, it can land in the team's triage inbox instead of being directly assigned. Team members then review triaged issues, assign them to projects, set priorities, and move them into the workflow. This prevents the chaos of unstructured issue creation and ensures every piece of work is properly categorized before it enters the active backlog.
Labels and Custom Fields
Labels provide a flexible categorization system. Issues can have multiple labels (e.g., "bug," "performance," "P0"). Teams can define their own label sets with custom colors. Custom fields extend the data model further, allowing teams to track domain-specific metadata like "customer impact," "affected service," or "regression type." The system must index these custom fields efficiently to support fast filtering and aggregation across large issue sets.
Workflow Customization
Each team can customize its workflow by defining the states an issue can occupy and the transitions between them. A typical workflow might include states like Backlog, Todo, In Progress, In Review, Done, and Canceled. Teams can add custom states like "Ready for QA" or "Blocked." The workflow engine enforces valid transitions and triggers automation rules when issues enter specific states.
Built-in Automation
Linear provides built-in automations that reduce manual work. When an issue is moved to "Done," it can automatically update the project progress. When a pull request is merged, the linked issue can be moved to "Done" automatically. When a new cycle starts, issues from the backlog can be auto-assigned based on priority. These automations are implemented as event-driven processors that react to state changes in real-time.
3. System Architecture Overview
Linear's system architecture is designed around three core principles: low latency, real-time collaboration, and horizontal scalability. The platform serves millions of API requests per day while maintaining sub-100ms p99 latency for client interactions. This section presents the high-level architecture and explains how each component contributes to the overall system.
Client Layer
The client is a React single-page application optimized for instant interactions. It uses a custom state management layer built on top of Apollo Client's cache, with aggressive pre-fetching and optimistic updates. The client maintains a persistent WebSocket connection for real-time updates and falls back to HTTP long-polling when WebSocket connections are unavailable. Bundle splitting ensures the initial page load is under 200KB gzipped, with additional code loaded on demand as users navigate between views.
API Gateway
The API Gateway is the single entry point for all client requests. It handles authentication token validation, rate limiting, request routing, and response caching. The gateway is implemented as a stateless service that can be horizontally scaled behind a load balancer. It uses connection pooling to the downstream GraphQL servers and maintains circuit breakers for graceful degradation when individual services are unhealthy.
GraphQL Server
The GraphQL server is the core of the backend. It implements the full schema for all entities (issues, projects, cycles, teams, etc.) and handles query resolution, mutation execution, and subscription management. The server uses DataLoader for efficient batch loading of related entities, N+1 query prevention, and query complexity analysis to prevent expensive queries from degrading performance.
Database Layer
PostgreSQL serves as the primary data store. Linear uses a multi-tenant schema where workspace data is partitioned by workspace ID. Read replicas handle read-heavy workloads, while the primary handles writes. Redis provides caching for frequently accessed data (user sessions, team configurations, recent issues) and powers the real-time pub/sub system for WebSocket notifications.
Search Infrastructure
Elasticsearch provides full-text search across issues, projects, and comments. The search index is updated asynchronously through a change data capture pipeline that reads PostgreSQL WAL events and indexes them in near real-time. This separation ensures search indexing never impacts write performance.
| Component | Technology | Responsibility | Scaling Strategy |
|---|---|---|---|
| Client | React + Apollo | UI rendering, local state | CDN, code splitting |
| API Gateway | Node.js / Go | Auth, rate limiting, routing | Horizontal, stateless |
| GraphQL Server | Node.js + TypeScript | Schema, resolvers, subscriptions | Horizontal, query caching |
| Database | PostgreSQL 15 | Persistent storage | Read replicas, partitioning |
| Cache / PubSub | Redis Cluster | Caching, real-time events | Cluster sharding |
| Search | Elasticsearch | Full-text search | Index sharding |
| Analytics | ClickHouse | Metrics, insights | Columnar compression |
Background Job Processing
Background jobs handle async tasks like sending notifications, syncing with GitHub, computing analytics, and executing automation rules. The job queue is backed by Redis with at-least-once delivery guarantees. Jobs are partitioned by workspace to ensure fair scheduling across tenants. Failed jobs are retried with exponential backoff, and dead-letter queues capture permanently failed jobs for manual inspection.
4. Real-Time Sync Architecture
Real-time synchronization is one of Linear's defining features. When one team member updates an issue status, every other team member sees the change within milliseconds. This is achieved through a combination of GraphQL subscriptions, WebSocket connections, optimistic updates, and a sophisticated conflict resolution strategy.
GraphQL Subscriptions
Linear uses GraphQL subscriptions built on top of WebSocket connections. Each client establishes a persistent WebSocket connection upon authentication. The server maintains a registry of active subscriptions per connection. When a mutation modifies an entity, the server publishes an event to the appropriate channel. Redis Pub/Sub distributes these events to all server instances, ensuring every connected client receives updates regardless of which server instance they are connected to.
The subscription system is organized around entity types and workspace scope. For example, a subscription to issue updates for team "ENG" will receive events whenever any issue in that team is modified. The server filters events at the subscription level, ensuring clients only receive updates they have explicitly subscribed to. This selective broadcasting reduces bandwidth and processing overhead for both clients and servers.
Optimistic Updates
Optimistic updates are the key to Linear's instant feel. When a user performs a mutation (e.g., changing an issue's status), the client immediately updates its local state to reflect the expected outcome. It then sends the mutation to the server. If the server confirms the mutation, the local state is already correct. If the server rejects the mutation (e.g., due to a conflict or validation error), the client rolls back the local state and displays an error message.
This approach requires careful handling of edge cases. The client must track pending mutations and their expected outcomes. When a subscription event arrives that corresponds to a pending mutation, the client must merge the server's state with its local predictions. The implementation uses a transaction ID system where each mutation carries a unique ID, and subscription events include the originating mutation's ID for correlation.
C#
public class OptimisticUpdateManager
{
private readonly ConcurrentDictionary<string, PendingUpdate> _pendingUpdates = new();
private readonly ILocalStateStore _stateStore;
public async Task<T> ApplyOptimistically<T>(
string mutationId,
Func<Task<T>> serverMutation,
Func<T, T> optimisticTransform)
{
var currentState = _stateStore.GetCurrent<T>();
var optimisticState = optimisticTransform(currentState);
var pending = new PendingUpdate
{
MutationId = mutationId,
Timestamp = DateTime.UtcNow,
ExpectedState = optimisticState,
OriginalState = currentState
};
_pendingUpdates.TryAdd(mutationId, pending);
_stateStore.Apply(optimisticState);
try
{
var result = await serverMutation();
_pendingUpdates.TryRemove(mutationId, out _);
return result;
}
catch (Exception)
{
if (_pendingUpdates.TryRemove(mutationId, out var rollback))
{
_stateStore.Apply(rollback.OriginalState);
}
throw;
}
}
public void ReconcileWithServerEvent(string mutationId, object serverState)
{
if (_pendingUpdates.TryRemove(mutationId, out _))
{
_stateStore.Apply(serverState);
}
}
}
Conflict Resolution
When two users modify the same issue simultaneously, Linear uses a last-writer-wins strategy with server-side validation. The server compares the mutation's expected version against the current database version. If they match, the mutation is applied and the version is incremented. If they do not match, the server returns the current state and a conflict error. The client then merges the server's state with any local changes and retries the mutation if necessary.
WebSocket Connection Management
The WebSocket layer uses a heartbeat mechanism to detect stale connections. Clients send ping messages every 30 seconds, and the server responds with pong. If a client fails to respond to two consecutive heartbeats, the connection is terminated. Reconnection logic on the client side uses exponential backoff with jitter to prevent thundering herd problems when many clients reconnect simultaneously after a server restart.
| Strategy | Purpose | Latency Impact | Consistency |
|---|---|---|---|
| Optimistic Updates | Instant UI response | 0ms (local) | Eventual |
| GraphQL Subscriptions | Real-time propagation | <100ms | Strong |
| Last-Writer-Wins | Conflict resolution | N/A | Eventual |
| Version Vectors | Mutation ordering | N/A | Causal |
| Heartbeat | Connection health | 30s interval | N/A |
Scaling Real-Time
As the user base grows, the real-time system must scale horizontally. Linear uses Redis Pub/Sub as the cross-server message bus. When a mutation occurs on server instance A, it publishes an event to Redis. All other server instances (B, C, D, ...) receive the event and forward it to their connected clients. This approach works well up to thousands of concurrent connections per server instance. Beyond that, the WebSocket servers can be partitioned by workspace, ensuring each server handles a bounded number of active workspaces.
5. Issue Tracking Data Model
The issue tracking data model is the foundation of Linear's system. Every feature — from views and filters to analytics and automations — operates on top of this model. Designing it correctly requires balancing flexibility (custom fields, labels, relations) with performance (fast queries, efficient indexing, minimal joins).
Core Tables
The issues table is the most heavily queried table in the system. It stores the issue title, description (stored as JSON for rich text support), status, priority, assignee, team, project, cycle, and various metadata fields. The table is partitioned by workspace ID to support multi-tenancy and enable efficient workspace-scoped queries.
C#
public class Issue
{
public Guid Id { get; set; }
public Guid WorkspaceId { get; set; }
public Guid TeamId { get; set; }
public string Identifier { get; set; } // e.g., "ENG-1234"
public string Title { get; set; }
public JsonDocument Description { get; set; }
public IssueStatus Status { get; set; }
public IssuePriority Priority { get; set; }
public Guid? AssigneeId { get; set; }
public Guid? ProjectId { get; set; }
public Guid? CycleId { get; set; }
public Guid? ParentId { get; set; }
public int SortOrder { get; set; }
public int EstimatedMinutes { get; set; }
public Guid CreatedById { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public DateTime? CompletedAt { get; set; }
public DateTime? StartedAt { get; set; }
public long Version { get; set; }
public ICollection<Label> Labels { get; set; }
public ICollection<Comment> Comments { get; set; }
public ICollection<IssueRelation> Relations { get; set; }
public Dictionary<string, object> CustomFields { get; set; }
}
Issue Relations
Issues can have typed relationships with other issues. Common relation types include "blocks" (this issue blocks another), "blocked by" (this issue is blocked by another), "duplicates" (this issue duplicates another), and "relates to" (this issue is related to another). The issue_relations table stores these relationships as directed edges in a graph, enabling traversal queries like "find all issues blocking this issue" or "find the transitive closure of blocked issues."
Label System
Labels are defined at the workspace level and applied to issues through a many-to-many join table. Each label has a name, color, and an optional description. The issue_labels join table links issues to their labels and includes a sort order for display purposes. Labels are indexed to support fast filtering: finding all issues with a specific label is a common operation in views and filters.
Custom Fields
Custom fields extend the issue model with team-specific metadata. They are stored as a JSONB column on the issues table, allowing flexible key-value pairs without schema changes. The system supports field types like text, number, single select, multi select, date, and person. Custom fields are indexed using GIN indexes on the JSONB column, enabling efficient filtering on arbitrary field combinations.
Identifier Generation
Each issue receives a unique identifier like "ENG-1234" where "ENG" is the team key and "1234" is a sequential number. The sequence is maintained per team using a PostgreSQL sequence. The identifier serves as a human-readable reference that users can type in search, comments, and commit messages. The system also supports searching by numeric ID alone (e.g., "1234") for convenience.
| Table | Primary Key | Key Indexes | Partitioning |
|---|---|---|---|
| issues | UUID | team_id, status, assignee_id, project_id | By workspace_id |
| comments | UUID | issue_id, created_at | By workspace_id |
| labels | UUID | workspace_id, name | By workspace_id |
| issue_labels | Composite | issue_id, label_id | By workspace_id |
| issue_relations | UUID | source_id, target_id | By workspace_id |
| custom_fields | UUID | workspace_id, team_id | By workspace_id |
Soft Deletes and Archival
Issues are never hard-deleted. Instead, they are soft-deleted by setting a deleted_at timestamp. Soft-deleted issues are excluded from queries by default but can be restored by team admins. Projects and cycles follow the same pattern. This approach preserves data integrity for historical analytics and prevents accidental data loss. Background jobs periodically archive soft-deleted records to cold storage after a configurable retention period.
6. Workflow Engine
The workflow engine is the heart of Linear's process automation. It defines how issues move through their lifecycle, enforces valid state transitions, and triggers automated actions when specific conditions are met. A well-designed workflow engine must be flexible enough to accommodate different team processes while remaining fast enough to execute in real-time during user interactions.
State Definitions
Each team defines its own set of workflow states. Linear provides a default set (Backlog, Todo, In Progress, Done, Canceled) that teams can customize. Each state has a category (Unstarted, InProgress, Completed, Canceled) that determines how issues in that state are counted in project progress and cycle statistics. The category system is critical for analytics: a "Done" category issue counts toward completed work regardless of whether the specific state is called "Done," "Shipped," or "Deployed."
C#
public class WorkflowState
{
public Guid Id { get; set; }
public Guid TeamId { get; set; }
public string Name { get; set; }
public StateCategory Category { get; set; }
public int Position { get; set; }
public string Color { get; set; }
public bool IsDefault { get; set; }
public ICollection<WorkflowTransition> AllowedTransitions { get; set; }
}
public enum StateCategory
{
Unstarted = 0,
InProgress = 1,
Completed = 2,
Canceled = 3
}
public class WorkflowTransition
{
public Guid FromStateId { get; set; }
public Guid ToStateId { get; set; }
public bool RequiresApproval { get; set; }
public Guid? AutomationRuleId { get; set; }
}
Transition Validation
When a user attempts to move an issue to a new state, the system validates the transition against the team's workflow configuration. Only explicitly defined transitions are allowed. For example, if the workflow does not define a transition from "Backlog" to "Done," that move is rejected. This enforcement prevents process violations and ensures issues follow the team's agreed-upon workflow. The transition validation runs in-memory on the application server with the workflow configuration cached in Redis, ensuring sub-millisecond validation times.
Automation Rules
Automation rules are event-driven actions that execute when issues enter specific states. Linear supports several built-in automations and allows teams to define custom rules. Common automations include: auto-assigning an issue when it moves from triage to todo, updating project progress when an issue moves to done, notifying Slack when an issue enters the blocked state, and creating follow-up issues when an issue is completed.
The automation engine is implemented as a pipeline of processors. When a state transition occurs, the system collects all matching automation rules for the new state and executes them in order. Each rule has a condition (which issues it applies to) and an action (what to do). The condition is evaluated against the issue's current state, and the action is executed asynchronously through the background job system to avoid blocking the mutation response.
Workflow Configuration API
Team admins can configure workflows through the UI or the API. The configuration includes adding/removing states, defining transitions between states, setting default states for new issues, and configuring automation rules. Changes to the workflow configuration are applied immediately and affect all issues in the team. The system maintains a version history of workflow changes, allowing admins to audit and roll back modifications.
Automation Rule Engine
C#
public class AutomationEngine
{
private readonly IAutomationRuleRepository _ruleRepo;
private readonly IAutomationActionExecutor _executor;
private readonly ILogger<AutomationEngine> _logger;
public async Task ExecuteTransitionHandlers(
Guid teamId,
Guid issueId,
Guid fromStateId,
Guid toStateId)
{
var rules = await _ruleRepo.GetByTeamAndState(teamId, toStateId);
foreach (var rule in rules.Where(r => r.IsEnabled))
{
try
{
if (rule.Condition.Evaluate(issueId))
{
await _executor.ExecuteAsync(rule.Action, issueId);
_logger.LogInformation(
"Automation rule {RuleId} executed for issue {IssueId}",
rule.Id, issueId);
}
}
catch (Exception ex)
{
_logger.LogError(ex,
"Failed to execute automation rule {RuleId} for issue {IssueId}",
rule.Id, issueId);
}
}
}
}
public interface IAutomationCondition
{
bool Evaluate(Guid issueId);
}
public class PriorityCondition : IAutomationCondition
{
private readonly IssuePriority _minPriority;
private readonly IIssueRepository _issueRepo;
public bool Evaluate(Guid issueId)
{
var issue = _issueRepo.GetById(issueId);
return issue.Priority >= _minPriority;
}
}
| Automation Type | Trigger | Action | Use Case |
|---|---|---|---|
| Auto-Assign | State change | Set assignee | Triage to Todo |
| Progress Update | State change | Recalculate % | Issue Done |
| Notification | State change | Send Slack message | Issue Blocked |
| Auto-Close | PR merged | Move to Done | Linked issue |
| Cycle Assignment | Issue created | Add to cycle | Backlog management |
| Sub-Issue Creation | State change | Create child | Breaking down work |
7. Keyboard-First Command Palette
Linear's command palette is arguably its most celebrated UX innovation. Inspired by VS Code's command palette and Spotlight on macOS, it provides instant access to every action in the application without touching the mouse. Users press Cmd+K (or Ctrl+K on Windows) to open the palette, then type natural-language-like commands to create issues, change statuses, navigate to views, and execute virtually any action.
Command Registration and Discovery
Every action in Linear is registered as a command with the command palette system. Each command has a unique identifier, a human-readable label, a set of keyboard shortcuts, an optional icon, and a handler function. Commands can be scoped globally (available anywhere) or contextually (available only when a specific view or entity is active). The command registry is built at application startup and updated dynamically as the user's permissions change.
C#
public class CommandDefinition
{
public string Id { get; set; }
public string Label { get; set; }
public string Description { get; set; }
public string Category { get; set; }
public KeyBinding[] Shortcuts { get; set; }
public Func<CommandContext, Task> Handler { get; set; }
public Func<CommandContext, bool> CanExecute { get; set; }
public CommandScope Scope { get; set; }
public string Icon { get; set; }
}
public class CommandRegistry
{
private readonly Dictionary<string, CommandDefinition> _commands = new();
private readonly IFuzzyMatcher _fuzzyMatcher;
public void Register(CommandDefinition command)
{
_commands[command.Id] = command;
}
public IReadOnlyList<CommandResult> Search(string query, CommandContext context)
{
return _commands.Values
.Where(cmd => cmd.Scope.Matches(context))
.Where(cmd => cmd.CanExecute?.Invoke(context) ?? true)
.Select(cmd => new
{
Command = cmd,
Score = _fuzzyMatcher.Score(cmd.Label, query)
})
.Where(x => x.Score > 0)
.OrderByDescending(x => x.Score)
.Take(10)
.Select(x => new CommandResult
{
Command = x.Command,
Score = x.Score,
HighlightedLabel = _fuzzyMatcher.Highlight(x.Command.Label, query)
})
.ToList();
}
public async Task Execute(string commandId, CommandContext context)
{
if (_commands.TryGetValue(commandId, out var command))
{
await command.Handler(context);
}
}
}
Fuzzy Matching Algorithm
The command palette uses fuzzy matching to allow typos and partial input. Typing "crt iss" matches "Create Issue" by matching consecutive characters in order. The scoring algorithm considers character matches, consecutive matches, word boundary matches, and exact matches. Characters matched at word boundaries (after spaces or camelCase transitions) receive higher scores. This ensures that "crt iss" ranks "Create Issue" higher than "Increment Counter Start" even though both match the pattern.
Issue Creation Flow
The most common command is issue creation. When a user types "crt" or presses the keyboard shortcut, the palette transforms into an inline issue creation form. The user can type a title, set priority (P0-P4 using number keys), assign labels (by typing label names), and assign the issue to a project. The entire creation process takes less than 3 seconds for experienced users. The issue is created optimistically in the local state, and the server mutation fires in the background. If the server rejects the creation, the user sees a brief toast notification and the optimistic state is rolled back.
Navigation Commands
The command palette doubles as a navigation tool. Users can type the name of a team, project, cycle, or view to jump directly to it. This eliminates the need to navigate through sidebar menus. The search indexes team names, project names, cycle names, and view names, providing instant access to any destination. Recently accessed items are boosted in ranking for faster access to frequently used views.
| Command | Shortcut | Scope | Action |
|---|---|---|---|
| Create Issue | C | Global | Opens inline issue form |
| Change Status | S | Issue selected | Shows status picker |
| Set Priority | 1-5 | Issue selected | Sets priority level |
| Assign to Me | A | Issue selected | Assigns current user |
| Move to Project | P | Issue selected | Shows project picker |
| Toggle Sidebar | [ | Global | Collapses sidebar |
| Quick Search | Cmd+K | Global | Opens search palette |
| Go to Team | G then T | Global | Shows team navigation |
Keyboard Shortcut System
Linear supports over 80 keyboard shortcuts organized by context. Global shortcuts work everywhere (Cmd+K for palette, Cmd+N for new issue). View shortcuts work within specific views (J/K for next/previous issue, Enter to open). Issue shortcuts work on the detail page (E to edit, M to move, Backspace to archive). The shortcut system uses a key binding resolver that handles modifier keys, sequences (like Vim's G-then-T), and conflicts between overlapping scopes. Users can view and customize all shortcuts through the settings panel.
Performance Considerations
The command palette must open and respond to input in under 16ms to maintain the 60fps feel that Linear is known for. This requires the command list to be pre-computed and cached. Fuzzy matching runs on every keystroke but uses an optimized algorithm that short-circuits early for obviously non-matching commands. The result list is virtualized so only visible items are rendered. The entire component is memoized and avoids unnecessary re-renders during typing.
8. Project and Roadmap Management
Projects in Linear represent larger initiatives that span multiple issues. Unlike issues, which are tactical, projects are strategic — they represent features, improvements, or initiatives that the team plans to complete over weeks or months. Linear's project management goes beyond simple grouping; it provides progress tracking, timeline visualization, roadmap views, and integration with cycles for sprint planning.
Project Lifecycle
Projects progress through four statuses: Planned, In Progress, Completed, and Canceled. The status is either set manually by the project owner or automatically based on the issues within the project. When the first issue in a project moves to an InProgress state, the project automatically transitions to "In Progress." When all issues are in a Completed state, the project transitions to "Completed." This automation ensures project status always reflects actual work progress without requiring manual updates.
Progress Tracking
Linear calculates project progress as the percentage of issues in a completed state. The progress bar updates in real-time as issues are completed. For more nuanced tracking, Linear also calculates "scope changes" — the addition or removal of issues from a project over time. This metric helps teams understand whether a project is growing in scope (scope creep) or remaining stable. The scope chart visualizes the total issue count over time, overlaid with the completed issue count.
Roadmap Views
The roadmap view provides a timeline visualization of projects across teams. Projects are displayed as horizontal bars on a Gantt-like chart, with start and target dates defining their duration. The roadmap allows leadership to see at a glance which teams are working on what, when projects are expected to complete, and whether any teams are over-allocated. The view supports zooming from weekly to monthly to quarterly perspectives.
C#
public class ProjectService
{
private readonly IProjectRepository _projectRepo;
private readonly IIssueRepository _issueRepo;
private readonly IProgressCalculator _progressCalc;
public async Task<ProjectDto> GetProjectWithProgress(Guid projectId)
{
var project = await _projectRepo.GetByIdWithIssues(projectId);
var issues = project.Issues;
var totalIssues = issues.Count;
var completedIssues = issues
.Count(i => i.Status.Category == StateCategory.Completed);
var startedIssues = issues
.Count(i => i.Status.Category == StateCategory.InProgress
|| i.Status.Category == StateCategory.Completed);
var progress = new ProjectProgress
{
TotalIssues = totalIssues,
CompletedIssues = completedIssues,
StartedIssues = startedIssues,
Percentage = totalIssues > 0
? Math.Round((double)completedIssues / totalIssues * 100, 1)
: 0,
ScopeHistory = await _progressCalc.GetScopeHistory(projectId),
Velocity = await _progressCalc.CalculateVelocity(projectId)
};
return MapToDto(project, progress);
}
public async Task AutoUpdateProjectStatus(Guid projectId)
{
var project = await _projectRepo.GetByIdWithIssues(projectId);
if (project.Status == ProjectStatus.Completed ||
project.Status == ProjectStatus.Canceled) return;
var allCompleted = project.Issues.All(
i => i.Status.Category == StateCategory.Completed);
var anyInProgress = project.Issues.Any(
i => i.Status.Category == StateCategory.InProgress);
if (allCompleted && project.Issues.Any())
{
project.Status = ProjectStatus.Completed;
project.CompletedAt = DateTime.UtcNow;
}
else if (anyInProgress && project.Status == ProjectStatus.Planned)
{
project.Status = ProjectStatus.InProgress;
project.StartedAt = DateTime.UtcNow;
}
await _projectRepo.Update(project);
}
}
Project Marking System
Projects use colored status indicators to communicate health. Green indicates the project is on track, yellow indicates potential risk, and red indicates the project is behind schedule or blocked. These health indicators are automatically calculated based on the project's progress relative to its timeline, the number of blocked issues, and the team's historical velocity. Manual overrides are available for project owners who have additional context.
Initiatives
At the workspace level, initiatives group related projects into strategic themes. For example, an initiative called "2026 Platform Modernization" might include projects like "Database Migration," "API v3," and "Infrastructure Upgrade." Initiatives provide a higher-level view for executive stakeholders and help ensure that individual projects align with organizational strategy.
| Feature | Description | Automation | Visibility |
|---|---|---|---|
| Status Tracking | Planned/In Progress/Completed/Canceled | Auto from issues | Project members |
| Progress Bar | Percentage of completed issues | Real-time update | All team members |
| Scope Chart | Issue count over time | Tracked automatically | Project owner |
| Timeline | Start and target dates | None | Roadmap viewers |
| Health Indicator | Green/Yellow/Red status | Calculated from velocity | All team members |
| Initiatives | Groups of projects | None | Workspace admins |
9. Cycles and Sprint Planning
Cycles are Linear's implementation of time-boxed iterations. They provide the cadence for planning, execution, and retrospection. Each cycle has a defined start and end date, a set of issues the team has committed to, and metrics that help the team understand their throughput and scope management.
Cycle Lifecycle
A cycle progresses through three states: Active, Completed, and Upcoming. Only one cycle can be active at a time for a given team. When a cycle is started, its start date is set to the current time and its status changes to Active. Issues in the cycle are considered "committed" — the team has pledged to complete them during the cycle period. When the cycle ends (or all issues are completed), the cycle is marked as Completed and a new cycle can be started.
Cycle Scope and Scope Change
The scope of a cycle is the set of issues assigned to it at the start. However, teams often need to add or remove issues during a cycle. Linear tracks these scope changes and visualizes them alongside the completion data. The scope change chart shows three lines over the cycle duration: the initial scope (flat line from start), the current scope (may increase or decrease), and the completed work (increasing line). This visualization helps teams understand whether they are taking on too much work mid-cycle or if their initial estimates were accurate.
C#
public class CycleService
{
private readonly ICycleRepository _cycleRepo;
private readonly IIssueRepository _issueRepo;
private readonly ICycleMetricsCalculator _metricsCalc;
public async Task<CycleDto> StartCycle(Guid cycleId)
{
var cycle = await _cycleRepo.GetByIdWithIssues(cycleId);
if (cycle.Status != CycleStatus.Upcoming)
throw new InvalidOperationException("Cycle is not in Upcoming status");
cycle.Status = CycleStatus.Active;
cycle.StartedAt = DateTime.UtcNow;
cycle.InitialScopeSnapshot = new CycleScopeSnapshot
{
IssueCount = cycle.Issues.Count,
IssueIds = cycle.Issues.Select(i => i.Id).ToList(),
SnapshotDate = DateTime.UtcNow
};
foreach (var issue in cycle.Issues)
{
issue.CycleId = cycleId;
issue.CycleEntryDate = DateTime.UtcNow;
await _issueRepo.Update(issue);
}
await _cycleRepo.Update(cycle);
return MapToDto(cycle);
}
public async Task<CycleMetrics> GetCurrentMetrics(Guid cycleId)
{
var cycle = await _cycleRepo.GetByIdWithIssues(cycleId);
var issues = cycle.Issues;
var initialCount = cycle.InitialScopeSnapshot?.IssueCount ?? 0;
return new CycleMetrics
{
TotalIssues = issues.Count,
ScopeChange = issues.Count - initialCount,
CompletedIssues = issues.Count(i =>
i.Status.Category == StateCategory.Completed),
InProgressIssues = issues.Count(i =>
i.Status.Category == StateCategory.InProgress),
UnstartedIssues = issues.Count(i =>
i.Status.Category == StateCategory.Unstarted),
ScopeCompleteRatio = issues.Count(i =>
i.Status.Category == StateCategory.Completed),
DaysRemaining = Math.Max(0,
(cycle.EndDate - DateTime.UtcNow).TotalDays),
Velocity = await _metricsCalc.CalculateTeamVelocity(
cycle.TeamId, cycle.StartDate, cycle.EndDate)
};
}
}
Cycle Metrics
Linear calculates several metrics for each cycle to help teams improve their planning accuracy over time. The completion rate is the percentage of committed issues that were completed. The scope change is the net number of issues added or removed during the cycle. The velocity is the number of story points or issues completed, which can be compared across cycles to identify trends. The average cycle time measures how long issues typically take from start to completion within the cycle.
Auto-Assignment
Linear supports automatic issue assignment to cycles based on configurable rules. When a new cycle starts, the system can automatically pull issues from the backlog based on priority, age, or project alignment. This reduces the manual overhead of cycle planning and ensures the team always has a prioritized backlog ready for the next cycle.
| Metric | Calculation | Purpose | Trend Analysis |
|---|---|---|---|
| Completion Rate | Completed / Total | Planning accuracy | Higher is better |
| Scope Change | Final scope - Initial scope | Scope stability | Lower is better |
| Velocity | Issues completed per cycle | Team throughput | Consistent is ideal |
| Avg Cycle Time | Sum(issue durations) / count | Efficiency | Lower is better |
| Carry-Over Rate | Uncompleted / Total | Estimation accuracy | Lower is better |
Retrospective Integration
At the end of each cycle, Linear provides a summary view that helps teams prepare for retrospectives. The summary includes a burndown chart showing work completed over time, a scope change chart, individual contributor metrics, and a list of carried-over issues. This data-driven approach to retrospectives helps teams identify concrete improvements rather than relying on vague recollections of how the cycle went.
10. Views, Filters, and Custom Views
Views are saved filter configurations that provide instant access to specific slices of work. In a large team with thousands of issues, the ability to quickly filter and sort is critical. Linear's view system is designed for speed and flexibility, supporting complex filter combinations that can be saved, shared, and accessed through the command palette.
Filter Architecture
The filter system operates on a composable predicate model. Each filter is a predicate that evaluates to true or false for a given issue. Multiple filters are combined with AND logic by default (all filters must match) but can be combined with OR logic for specific use cases. The filter set is serialized as a JSON object that the client sends with every query. The server translates the filter JSON into SQL WHERE clauses with parameterized values for safety and performance.
C#
public class FilterClause
{
public string Field { get; set; }
public FilterOperator Operator { get; set; }
public object Value { get; set; }
public FilterConjunction Conjunction { get; set; } = FilterConjunction.And;
}
public class IssueQueryService
{
private readonly IIssueRepository _repo;
private readonly IFilterCompiler _filterCompiler;
public async Task<PagedResult<IssueDto>> Query(
Guid teamId,
FilterClause[] filters,
SortClause[] sorts,
int page,
int pageSize)
{
var query = _repo.Query()
.Where(i => i.TeamId == teamId);
foreach (var filter in filters)
{
query = _filterCompiler.ApplyFilter(query, filter);
}
query = sorts.Aggregate(query, (q, sort) =>
_filterCompiler.ApplySort(q, sort));
var total = await query.CountAsync();
var issues = await query
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return new PagedResult<IssueDto>
{
Items = issues.Select(MapToDto).ToList(),
TotalCount = total,
Page = page,
PageSize = pageSize
};
}
}
Default Views
Linear provides several default views for each team. The My Issues view shows all issues assigned to the current user across projects and cycles. The Active Issues view shows all issues in an in-progress state. The Backlog view shows all unstarted issues sorted by priority. The Board view displays issues in a Kanban board organized by workflow state. These default views are read-only and cannot be modified, but users can create custom views based on them.
Custom Views
Users can create custom views by applying any combination of filters and saving the configuration. Custom views support filtering by status, priority, assignee, labels, project, cycle, created date, updated date, and custom fields. Views can be sorted by any field and can be displayed in list or board format. Users can share views with their team or keep them private. Shared views appear in the team's sidebar for quick access.
View Performance Optimization
Since views are frequently accessed and involve complex filtering, performance is critical. Linear uses several optimization strategies: materialized view caches for commonly accessed filter combinations, partial indexes on frequently filtered columns (status, assignee, priority), and query result caching with invalidation tied to issue mutation events. The filter compiler also optimizes filter ordering to apply the most selective filters first, reducing the number of rows processed by subsequent filters.
| Filter Type | Operators | Index Support | Example |
|---|---|---|---|
| Status | is, is not | B-tree index | Status is "In Progress" |
| Priority | is, is not, above, below | B-tree index | Priority is above P2 |
| Assignee | is, is not, is set | B-tree index | Assignee is me |
| Labels | has any, has all, has none | GIN index | Has label "bug" |
| Project | is, is not, is set | B-tree index | Project is "Auth Revamp" |
| Created | before, after, between | B-tree index | Created after 2026-01-01 |
| Custom Fields | varies by type | GIN index (JSONB) | Field "impact" is "High" |
Board View
The board view transforms the flat issue list into a Kanban-style board where columns represent workflow states. Issues are displayed as cards that can be dragged between columns to change their status. The board supports horizontal scrolling for workflows with many states and vertical scrolling within columns when a state contains many issues. Drag-and-drop operations trigger optimistic updates for instant visual feedback, followed by a mutation to persist the status change.
Filter Sharing and URL Encoding
Views can be shared via URL. The filter configuration is encoded in the URL query string using a compact JSON format. When a user opens a shared view URL, the client deserializes the filter configuration and applies it to the current context. This approach allows users to share complex filter configurations through Slack, email, or documentation without requiring both parties to have saved the view.
11. GitHub/GitLab Integration
The GitHub and GitLab integrations are among Linear's most popular features. They bridge the gap between issue tracking and code development, enabling automatic status updates when pull requests are merged, bidirectional linking between issues and PRs, and branch creation from within Linear.
Integration Architecture
The integration system uses a combination of webhooks and API polling to stay synchronized with GitHub and GitLab. When a user connects a team to a GitHub repository, Linear registers webhooks for push, pull request, and issue events. These webhooks are processed by the integration service, which updates the corresponding Linear issues. When a user creates an issue in Linear, they can optionally create a linked branch or pull request in GitHub, with the issue identifier automatically included in the branch name and PR description.
C#
public class GitHubWebhookProcessor
{
private readonly IIssueRepository _issueRepo;
private readonly ILinkResolver _linkResolver;
private readonly IWorkflowEngine _workflowEngine;
public async Task ProcessPullRequestEvent(GitHubWebhookEvent webhook)
{
var pr = webhook.PullRequest;
var linkedIssues = await _linkResolver
.FindIssuesLinkedToPR(pr.Repository.FullName, pr.Number);
foreach (var issue in linkedIssues)
{
switch (webhook.Action)
{
case "opened":
issue.Status = WorkflowState.InReview;
issue.PullRequestUrl = pr.HtmlUrl;
break;
case "closed" when pr.Merged:
await _workflowEngine.TransitionIssue(
issue.Id, WorkflowState.Done);
issue.MergedAt = pr.MergedAt;
break;
case "closed" when !pr.Merged:
issue.Status = WorkflowState.InProgress;
break;
}
await _issueRepo.Update(issue);
}
}
public async Task ProcessBranchPush(GitHubWebhookEvent webhook)
{
var branchName = webhook.Ref.Replace("refs/heads/", "");
var match = Regex.Match(branchName, @"^linear/(ENG-\d+)");
if (match.Success)
{
var identifier = match.Groups[1].Value;
var issue = await _issueRepo.GetByIdentifier(identifier);
if (issue != null)
{
issue.Status = WorkflowState.InProgress;
await _issueRepo.Update(issue);
}
}
}
}
Bidirectional Linking
When an issue is linked to a PR, Linear parses the PR description and commit messages for the issue identifier (e.g., "ENG-1234"). If found, it creates a bidirectional link. The issue shows the linked PR with its status (open, merged, closed), and the PR shows a reference to the Linear issue. This linking enables developers to stay in their code editor while understanding the project management context of their work.
Branch Creation
Linear can create branches directly from the issue detail page. The branch name follows a configurable convention, typically linear/{issue-identifier}-{slugified-title}. The branch is created from a configurable base branch (usually main or develop) and checked out locally using the GitHub API. This feature eliminates the manual step of creating a branch with the correct naming convention.
Commit Integration
When developers mention an issue identifier in a commit message (e.g., "fix: resolve auth timeout ENG-1234"), Linear automatically associates the commit with the issue. This is achieved by the integration service parsing push events from GitHub webhooks and extracting issue identifiers from commit messages using regex pattern matching. The associated commits are displayed on the issue detail page, providing a complete picture of all code changes related to an issue.
| Feature | Trigger | Direction | Automation Level |
|---|---|---|---|
| PR Linking | PR creation | Bidirectional | Manual + Auto |
| Branch Creation | User action | Linear → GitHub | Manual |
| Status Update | PR merge | GitHub → Linear | Automatic |
| Commit Parsing | Push event | GitHub → Linear | Automatic |
| Deployment Status | CI/CD event | GitHub → Linear | Automatic |
| Issue Sync | GitHub issue created | GitHub ↔ Linear | Configurable |
Webhook Reliability
GitHub webhooks can be delayed or duplicated. Linear implements idempotent webhook processing to handle these scenarios. Each webhook delivery has a unique delivery ID that is tracked in a deduplication table. Before processing, the system checks whether this delivery ID has already been processed. If so, the webhook is acknowledged without reprocessing. The system also validates webhook signatures using HMAC-SHA256 to prevent spoofed webhook attacks.
12. Linear Insights and Analytics
Linear Insights provides data-driven visibility into team performance, project health, and process efficiency. Unlike traditional reporting that requires manual data extraction, Linear Insights are computed in real-time and available directly within the application. The analytics engine processes millions of issue state transitions to generate actionable metrics.
Key Metrics
Cycle Time measures the time from when an issue enters an in-progress state to when it reaches a completed state. This metric is critical for understanding team efficiency and setting realistic expectations for delivery timelines. Linear calculates cycle time for individual issues and aggregates it across teams, projects, and time periods to identify trends and bottlenecks.
Throughput measures the number of issues completed per unit of time (daily, weekly, sprint). Throughput is a better indicator of team capacity than story points because it is based on actual completed work rather than estimates. Linear tracks throughput at the team level and can segment it by priority, label, or project to identify which types of work the team handles most efficiently.
Velocity measures the average throughput over a rolling window (typically the last 3-5 cycles). Velocity helps teams plan future cycles by providing a realistic expectation of how much work they can complete. Linear displays velocity trends over time, helping teams identify whether their capacity is increasing, decreasing, or remaining stable.
C#
public class InsightsService
{
private readonly IAnalyticsRepository _analyticsRepo;
public async Task<TeamInsightsDto> GetTeamInsights(
Guid teamId, DateTime from, DateTime to)
{
var issues = await _analyticsRepo
.GetCompletedIssuesInRange(teamId, from, to);
var cycleTimes = issues
.Where(i => i.StartedAt.HasValue && i.CompletedAt.HasValue)
.Select(i => (i.CompletedAt.Value - i.StartedAt.Value).TotalHours)
.ToList();
var throughputByWeek = issues
.GroupBy(i => GetWeekStart(i.CompletedAt.Value))
.ToDictionary(g => g.Key, g => g.Count());
var averageCycleTime = cycleTimes.Any()
? cycleTimes.Average()
: 0;
var p85CycleTime = cycleTimes.Any()
? Percentile(cycleTimes, 85)
: 0;
return new TeamInsightsDto
{
AverageCycleTimeHours = Math.Round(averageCycleTime, 1),
P85CycleTimeHours = Math.Round(p85CycleTime, 1),
ThroughputByWeek = throughputByWeek,
TotalCompleted = issues.Count,
AverageThroughput = throughputByWeek.Values.Any()
? throughputByWeek.Values.Average()
: 0,
CycleTimeTrend = CalculateTrend(cycleTimes),
BottleneckStates = await IdentifyBottlenecks(teamId, from, to)
};
}
private double Percentile(List<double> values, int percentile)
{
var sorted = values.OrderBy(v => v).ToList();
var index = (percentile / 100.0) * (sorted.Count - 1);
var lower = (int)Math.Floor(index);
var upper = (int)Math.Ceiling(index);
if (lower == upper) return sorted[lower];
return sorted[lower] + (index - lower) * (sorted[upper] - sorted[lower]);
}
}
Burndown and Burnup Charts
The burndown chart shows remaining work over the duration of a cycle. It plots the total issue count at cycle start as the starting point and draws a line showing remaining issues as they are completed. An ideal burndown line connects the starting count to zero at the cycle end date. The actual burndown line shows real progress, allowing teams to see whether they are ahead of or behind schedule. The burnup chart adds a scope line, showing whether changes in total scope are affecting the team's ability to complete work.
Distribution Charts
Linear provides distribution charts for several metrics. The cycle time distribution shows a histogram of how long issues take to complete, helping teams identify whether most issues are completed quickly with a few outliers, or if cycle times are consistently long. The priority distribution shows the breakdown of issues by priority level, helping teams understand whether they are spending time on high-priority work. The status distribution shows the current state of all issues, indicating whether work is flowing smoothly or accumulating in specific states.
Team Comparison
For organizations with multiple teams, Linear Insights provides cross-team comparisons. This helps leadership understand relative performance, identify best practices from high-performing teams, and allocate resources effectively. The comparison respects data access controls, so team members only see data for teams they have access to.
| Metric | Time Range | Granularity | Insight |
|---|---|---|---|
| Cycle Time | Last 30/90/365 days | Per issue, averaged | Team efficiency |
| Throughput | Current cycle, trend | Daily, weekly | Team capacity |
| Velocity | Last 3-5 cycles | Per cycle | Planning accuracy |
| Burndown | Current cycle | Daily | Schedule adherence |
| Scope Change | Current cycle | Daily | Scope management |
| Cycle Time Distribution | Configurable | Per issue | Process consistency |
Data Pipeline
The analytics data pipeline uses event sourcing to track all issue state changes. Each state transition is recorded with a timestamp, the previous state, the new state, and the user who made the change. This event log is the source of truth for all analytics calculations. ClickHouse serves as the analytical data store, providing fast aggregation queries over large volumes of event data. The pipeline processes events in near real-time, with a lag of less than 30 seconds between an event occurring and it being available in analytics.
13. SLA and Priority Management
Linear's priority system helps teams focus on the most important work first. Issues are assigned priority levels from P0 (urgent) to P4 (no priority), and SLA tracking ensures critical issues are addressed within defined timeframes. This system is essential for teams that handle production incidents, customer escalations, or time-sensitive features.
Priority Levels
Linear defines five priority levels with clear semantics. P0 (Urgent) is reserved for production outages and critical security vulnerabilities that require immediate attention. P1 (High) is for high-impact bugs and features that must be completed in the current cycle. P2 (Medium) represents standard work items that are important but not time-critical. P3 (Low) covers nice-to-have improvements and minor bugs. P4 (No Priority) is for backlog items that have not been evaluated for priority.
SLA Tracking
SLA tracking monitors the time between an issue being created (or transitioning to a specific state) and the team's response or resolution. For example, a P0 issue might have an SLA of 4 hours for initial response and 24 hours for resolution. Linear tracks these timelines and provides visual indicators when an SLA is at risk of being breached (yellow) or has already been breached (red). The SLA configuration is customizable per team, allowing different teams to set different expectations based on their operational requirements.
C#
public class SLAService
{
private readonly ISLAPolicyRepository _policyRepo;
private readonly IIssueRepository _issueRepo;
private readonly ISLAAlertService _alertService;
public async Task CheckSLACompliance(Guid teamId)
{
var policies = await _policyRepo.GetByTeam(teamId);
var activeIssues = await _issueRepo.GetActiveWithTimestamps(teamId);
foreach (var issue in activeIssues)
{
var matchingPolicy = policies.FirstOrDefault(p =>
p.MatchesPriority(issue.Priority));
if (matchingPolicy == null) continue;
var elapsedTime = CalculateElapsedTime(issue, matchingPolicy);
var slaStatus = EvaluateSLAStatus(elapsedTime, matchingPolicy);
if (slaStatus == SLAStatus.AtRisk)
{
await _alertService.SendSLAWarning(issue, matchingPolicy,
elapsedTime);
}
else if (slaStatus == SLAStatus.Breached)
{
await _alertService.SendSLABreach(issue, matchingPolicy,
elapsedTime);
}
issue.SLAStatus = slaStatus;
issue.SLATimeRemaining = CalculateTimeRemaining(
elapsedTime, matchingPolicy);
await _issueRepo.Update(issue);
}
}
private SLAStatus EvaluateSLAStatus(
TimeSpan elapsed, SLAPolicy policy)
{
var totalBudget = policy.ResponseTime + policy.ResolutionTime;
var warningThreshold = totalBudget * 0.8;
if (elapsed >= totalBudget) return SLAStatus.Breached;
if (elapsed >= warningThreshold) return SLAStatus.AtRisk;
return SLAStatus.OnTrack;
}
}
Priority-Based Sorting
Throughout the Linear interface, issues are sorted by priority by default. P0 issues appear at the top of every list, followed by P1, P2, P3, and P4. Within the same priority level, issues are sorted by recency (newest first) or by manual ordering set by the user. This consistent priority-based sorting ensures that the most important work is always visible and accessible, regardless of which view the user is looking at.
SLA Dashboards
The SLA dashboard provides an at-a-glance view of SLA compliance across the team. It displays the total number of active issues, the percentage currently within SLA, the number approaching breach, and the number already breached. A trend chart shows SLA compliance over time, helping teams identify whether their response and resolution times are improving or degrading. This dashboard is particularly valuable for team leads and engineering managers who need to ensure operational excellence.
Escalation Rules
When an SLA is breached, Linear can automatically escalate the issue. Escalation rules are configurable per priority level and can include actions like notifying the team lead, adding additional assignees, or moving the issue to a higher visibility queue. Escalation notifications are sent through Slack, email, or in-app notifications based on the team's notification preferences.
| Priority | Response SLA | Resolution SLA | Escalation |
|---|---|---|---|
| P0 - Urgent | 1 hour | 4 hours | Immediate page to on-call |
| P1 - High | 4 hours | 24 hours | Notify team lead |
| P2 - Medium | 1 business day | 5 business days | Weekly review |
| P3 - Low | 3 business days | Current cycle | Backlog review |
| P4 - No Priority | None | None | None |
Priority Adjustment Rules
Linear supports automatic priority adjustment based on signals. For example, if an issue is linked to a customer escalation ticket, its priority can be automatically boosted. If an issue has been in the backlog for more than 6 months without being started, its priority can be downgraded. These rules help keep the backlog healthy and ensure that priority levels reflect current reality rather than initial estimates.
14. API and Webhook System
Linear provides a comprehensive GraphQL API that enables programmatic access to all platform features. The API is used by third-party integrations, internal automation scripts, and the Linear mobile app. It follows GraphQL best practices with strong typing, query complexity limits, and rate limiting to ensure reliability and fairness.
GraphQL Schema Design
The GraphQL schema mirrors the domain model with types for Workspace, Team, Issue, Project, Cycle, and all related entities. Queries provide read access to entities with filtering, pagination, and field selection. Mutations handle create, update, and delete operations with input validation and permission checks. Subscriptions provide real-time updates for entity changes.
C#
public class IssueType : ObjectType<Issue>
{
protected override void Configure(IObjectTypeDescriptor<Issue> descriptor)
{
descriptor.Name("Issue");
descriptor.Field(i => i.Id).Type<IdType>();
descriptor.Field(i => i.Identifier).Type<StringType>();
descriptor.Field(i => i.Title).Type<StringType>();
descriptor.Field(i => i.Description).Type<JsonType>();
descriptor.Field(i => i.Status).Type<WorkflowStateType>();
descriptor.Field(i => i.Priority).Type<IntType>();
descriptor.Field(i => i.Assignee)
.Type<UserType>()
.ResolveWith<IssueResolvers>(r => r.GetAssignee(default!, default!));
descriptor.Field(i => i.Project)
.Type<ProjectType>()
.ResolveWith<IssueResolvers>(r => r.GetProject(default!, default!));
descriptor.Field(i => i.Comments)
.Type<ListType<CommentType>>()
.ResolveWith<IssueResolvers>(r => r.GetComments(default!, default!));
}
private class IssueResolvers
{
public User GetAssignee(Issue issue, [Service] IUserRepository repo)
=> issue.AssigneeId.HasValue
? repo.GetById(issue.AssigneeId.Value)
: null;
public Project GetProject(Issue issue, [Service] IProjectRepository repo)
=> issue.ProjectId.HasValue
? repo.GetById(issue.ProjectId.Value)
: null;
public IEnumerable<Comment> GetComments(
Issue issue, [Service] ICommentRepository repo)
=> repo.GetByIssueId(issue.Id);
}
}
Rate Limiting
The API implements rate limiting at multiple levels. Per-user limits prevent individual users from consuming excessive resources (1000 requests per minute for authenticated users). Per-workspace limits ensure fair usage across teams (10,000 requests per minute per workspace). Query complexity analysis assigns a cost to each field in a query and rejects queries that exceed a complexity budget. This prevents expensive nested queries from degrading API performance for other users.
Webhook System
Linear's webhook system allows external services to receive notifications when events occur in Linear. Webhooks can be configured to fire for specific event types (issue created, issue updated, project status changed, etc.) and filtered by team or project. Each webhook delivery includes a JSON payload with the event details and a signature header for verification.
Webhook Delivery
C#
public class WebhookDeliveryService
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly IWebhookRepository _webhookRepo;
private readonly ISignatureService _signatureService;
public async Task<WebhookDeliveryResult> Deliver(
Webhook webhook, WebhookEvent event)
{
var payload = JsonSerializer.Serialize(event);
var signature = _signatureService.ComputeSignature(
webhook.Secret, payload);
var request = new HttpRequestMessage(HttpMethod.Post, webhook.Url);
request.Content = new StringContent(payload,
Encoding.UTF8, "application/json");
request.Headers.Add("X-Linear-Signature", signature);
request.Headers.Add("X-Linear-Event", event.Type);
request.Headers.Add("X-Linear-Delivery", event.DeliveryId.ToString());
var client = _httpClientFactory.CreateClient();
client.Timeout = TimeSpan.FromSeconds(10);
var response = await client.SendAsync(request);
var success = response.IsSuccessStatusCode;
await _webhookRepo.RecordDelivery(new WebhookDelivery
{
WebhookId = webhook.Id,
EventId = event.Id,
StatusCode = (int)response.StatusCode,
ResponseBody = await response.Content.ReadAsStringAsync(),
DeliveredAt = DateTime.UtcNow,
Success = success
});
return new WebhookDeliveryResult
{
Success = success,
StatusCode = (int)response.StatusCode
};
}
}
Webhook Reliability
Failed webhook deliveries are retried with exponential backoff: first retry after 1 minute, second after 5 minutes, third after 30 minutes, and a final attempt after 2 hours. After four failed attempts, the webhook is marked as failing and the workspace admin is notified. The webhook delivery log provides full visibility into each delivery attempt, including request/response details and error messages.
| API Feature | Limit | Scope | Enforcement |
|---|---|---|---|
| Request Rate | 1000/min per user | Per API key | Sliding window |
| Workspace Rate | 10,000/min | Per workspace | Token bucket |
| Query Complexity | 5000 points | Per query | Static analysis |
| Pagination | 50 items/page max | Per query | Schema limit |
| Webhook Payload | 1 MB max | Per delivery | Truncation |
| Webhook Retries | 4 attempts | Per event | Exponential backoff |
API Authentication
The API supports two authentication methods: OAuth 2.0 for user-authorized applications and API keys for server-to-server communication. OAuth flows follow the standard authorization code flow with PKCE for enhanced security. API keys are scoped to specific workspaces and can be restricted to read-only or read-write access. All API traffic is encrypted with TLS 1.3, and API keys are hashed at rest to prevent exposure in case of a database breach.
15. Team and Workspace Management
Workspace and team management is the organizational backbone of Linear. Workspaces provide tenant isolation, billing management, and workspace-level settings. Teams organize users into functional groups with their own workflows, labels, and permissions. Understanding the workspace and team model is essential for designing a multi-tenant SaaS platform.
Workspace Structure
A workspace represents an organization or company. It contains all teams, projects, users, and settings. Workspace data is completely isolated from other workspaces through row-level security policies in PostgreSQL. Every database query is filtered by workspace ID, ensuring that data never leaks across tenants. The workspace entity stores configuration like the workspace name, URL slug, authentication settings (SSO configuration), and billing information.
Team Organization
Teams are the primary unit of collaboration within a workspace. Each team has its own issue workflow, labels, projects, and cycles. A user can belong to multiple teams, and their role in each team can differ (admin in one team, member in another). Team membership determines what issues a user can see and what actions they can perform. The team model supports both physical teams (e.g., "Frontend Team") and cross-functional groups (e.g., "Incident Response").
Role-Based Access Control
C#
public class PermissionService
{
private readonly IPermissionRepository _permissionRepo;
private readonly IMemberRepository _memberRepo;
public async Task<bool> HasPermission(
Guid userId, Guid workspaceId, Permission permission)
{
var membership = await _memberRepo
.GetMembership(userId, workspaceId);
if (membership == null) return false;
return permission switch
{
Permission.CreateIssue =>
membership.Role >= TeamRole.Member,
Permission.EditIssue =>
membership.Role >= TeamRole.Member,
Permission.DeleteIssue =>
membership.Role >= TeamRole.Admin,
Permission.ManageTeam =>
membership.Role >= TeamRole.Admin,
Permission.ManageWorkspace =>
membership.Role == WorkspaceRole.Owner,
Permission.ViewAnalytics =>
membership.Role >= TeamRole.Member,
Permission.ManageBilling =>
membership.Role == WorkspaceRole.Owner,
Permission.ManageIntegrations =>
membership.Role >= TeamRole.Admin,
_ => false
};
}
public async Task<bool> CanAccessIssue(
Guid userId, Guid issueId)
{
var issue = await _issueRepo.GetById(issueId);
var membership = await _memberRepo
.GetMembership(userId, issue.WorkspaceId);
if (membership == null) return false;
if (membership.Teams.Contains(issue.TeamId)) return true;
return membership.Role >= WorkspaceRole.Admin;
}
}
Roles and Permissions Matrix
| Permission | Guest | Member | Admin | Owner |
|---|---|---|---|---|
| View Issues | Yes | Yes | Yes | Yes |
| Create Issues | No | Yes | Yes | Yes |
| Edit Own Issues | No | Yes | Yes | Yes |
| Edit All Issues | No | No | Yes | Yes |
| Manage Team | No | No | Yes | Yes |
| Manage Workspace | No | No | No | Yes |
| Manage Billing | No | No | No | Yes |
| Delete Workspace | No | No | No | Yes |
SSO and Authentication
Linear supports SAML-based single sign-on (SSO) for enterprise workspaces. When SSO is enabled, all workspace members must authenticate through the identity provider (e.g., Okta, Azure AD, Google Workspace). The SSO integration handles user provisioning and deprovisioning, ensuring that when an employee leaves the organization, their Linear access is automatically revoked. SCIM 2.0 is supported for automated user and group synchronization.
Invitation and Onboarding
Workspace admins can invite users by email address. Invited users receive an email with a link to join the workspace. If the user does not have a Linear account, they are prompted to create one. Upon joining, the user is added to the workspace with a default team membership. The onboarding flow guides new users through key features like keyboard shortcuts, issue creation, and view configuration.
Workspace Settings
Workspace settings control global behaviors like the default issue numbering format, allowed authentication methods, webhook policies, and integration configurations. Settings are organized into categories (General, Security, Integrations, Billing) and can only be modified by workspace owners and admins. All setting changes are logged for audit purposes.
16. Performance and Instant UI Techniques
Linear's signature feature is its speed. Every interaction feels instant, whether navigating between views, creating issues, or updating statuses. Achieving this level of performance requires deliberate architectural decisions at every layer of the stack, from client-side rendering optimizations to server-side query performance.
Data Prefetching
Linear's client predicts which data the user will need next and fetches it before they ask for it. When a user hovers over a project in the sidebar, the client prefetches that project's issues. When a user starts typing in the command palette, the client prefetches the issue list for the currently selected team. This predictive prefetching ensures that by the time the user navigates to a new view, the data is already in the local cache. The prefetch logic uses a scoring model that considers the user's navigation history, the current context, and the probability of each navigation path.
Virtual Scrolling
Issue lists can contain thousands of items. Rendering all of them would cause significant performance degradation. Linear uses virtual scrolling (windowing) to render only the visible items in the viewport plus a small buffer. As the user scrolls, items are recycled and re-rendered with their current data. This technique keeps the DOM node count constant regardless of list size, ensuring smooth 60fps scrolling even with 10,000+ issues in a list.
Query Optimization
The GraphQL server uses DataLoader to batch and deduplicate database queries within a single request. When resolving a list of issues with their assignees, instead of executing N+1 queries (one for issues, one per assignee), DataLoader batches all assignee lookups into a single IN query. The server also implements query plan caching for frequently executed queries, pre-computing optimal query plans and reusing them across requests.
Client-Side Cache Architecture
C#
public class LinearClientCache
{
private readonly IMemoryCache _memoryCache;
private readonly Dictionary<string, CacheEntry> _entityCache = new();
private readonly Dictionary<string, List<string>> _queryIndex = new();
public T GetOrFetch<T>(string key, Func<Task<T>> fetcher,
TimeSpan? ttl = null)
{
if (_entityCache.TryGetValue(key, out var cached) &
amp;& !cached.IsExpired)
{
return (T)cached.Value;
}
return fetcher().ContinueWith(task =>
{
var entry = new CacheEntry
{
Value = task.Result,
Expiry = DateTime.UtcNow + (ttl ?? TimeSpan.FromMinutes(5)),
Version = Interlocked.Increment(ref _version)
};
_entityCache[key] = entry;
return task.Result;
}).Result;
}
public void InvalidateByPrefix(string prefix)
{
var keysToRemove = _entityCache.Keys
.Where(k => k.StartsWith(prefix))
.ToList();
foreach (var key in keysToRemove)
{
_entityCache.Remove(key);
}
foreach (var query in _queryIndex.Keys
.Where(q => q.StartsWith(prefix)))
{
_queryIndex.Remove(query);
}
}
public void ApplyOptimisticUpdate<T>(
string entityKey, Func<T, T> transform)
{
if (_entityCache.TryGetValue(entityKey, out var cached))
{
var transformed = transform((T)cached.Value);
_entityCache[entityKey] = new CacheEntry
{
Value = transformed,
Expiry = cached.Expiry,
Version = Interlocked.Increment(ref _version),
IsOptimistic = true
};
}
}
}
Network Optimizations
Linear's API responses are compressed with Brotli (preferred) or gzip, reducing transfer sizes by 70-80%. The client uses HTTP/2 multiplexing to send multiple GraphQL queries over a single connection, eliminating the overhead of multiple TCP handshakes. Persistent connections are maintained to avoid repeated TLS negotiation. Response payloads use a compact JSON format where field names are abbreviated on the wire and expanded by the client.
Rendering Performance
The React application uses several rendering optimizations. React.memo prevents re-rendering components whose props have not changed. useMemo and useCallback memoize expensive computations and function references. The virtual DOM diffing algorithm is optimized for the specific patterns common in issue lists (mostly append/update operations). Bundle splitting ensures only the code needed for the current view is loaded, with the initial bundle under 200KB gzipped.
| Technique | Impact | Implementation | Measurable Result |
|---|---|---|---|
| Data Prefetching | Eliminates navigation latency | Hover-triggered fetches | 0ms perceived load time |
| Virtual Scrolling | Constant DOM size | react-window | 60fps with 10K items |
| DataLoader Batching | Eliminates N+1 queries | Server-side batching | 90% fewer DB queries |
| Optimistic Updates | Instant mutation feedback | Client state prediction | 0ms perceived update |
| Code Splitting | Faster initial load | React.lazy + Suspense | <200KB initial bundle |
| Brotli Compression | Smaller payloads | Server-side compression | 70-80% size reduction |
Measuring Performance
Linear tracks client-side performance metrics including First Contentful Paint (FCP), Largest Contentful Paint (LCP), First Input Delay (FID), and Cumulative Layout Shift (CLS). These Core Web Vitals are reported to a monitoring dashboard that alerts the team when metrics degrade. Server-side metrics include API response times (p50, p95, p99), database query durations, cache hit rates, and WebSocket connection counts. Both client and server metrics are sampled and aggregated to identify performance trends and regressions.
17. Interview Q&A
Q1: How would you design the real-time synchronization system for a project management tool like Linear?
Start with the core requirements: when one user changes an issue status, all other users viewing that issue should see the update within milliseconds. The solution involves GraphQL subscriptions over WebSocket connections, with Redis Pub/Sub as the cross-server message bus. Each server instance maintains active WebSocket connections and subscribes to relevant channels in Redis. When a mutation occurs, the server publishes an event to Redis, which fans out to all server instances. Each instance then forwards the event to connected clients. Client-side, optimistic updates ensure the mutator sees the change instantly, while subscription events update all other clients.
Q2: How does Linear achieve its "instant UI" feel?
The instant UI is achieved through a combination of techniques. Optimistic updates apply mutations to local state before server confirmation. Data prefetching loads data for anticipated navigations before the user triggers them. Virtual scrolling renders only visible items in long lists. The GraphQL client uses aggressive caching with normalized entities, so fetched data is available across views. The initial JavaScript bundle is under 200KB gzipped through code splitting, and subsequent code is loaded on demand. Server responses are compressed with Brotli and served over HTTP/2 with persistent connections.
Q3: How would you design the workflow engine that supports customizable state transitions and automation rules?
The workflow engine stores team-specific state configurations in a database, with each state having a category (Unstarted, InProgress, Completed, Canceled). Transitions between states are explicitly defined per team. When a user attempts a state transition, the engine validates it against the team's allowed transitions. Automation rules are stored as condition-action pairs. When a transition occurs, the engine evaluates all rules matching the target state and executes their actions asynchronously. The workflow configuration is cached in Redis for fast validation, and changes are propagated to all server instances via a cache invalidation event.
Q4: How would you handle conflict resolution when two users modify the same issue simultaneously?
Linear uses last-writer-wins with version vectors. Each issue has a version number that increments on every update. When a mutation arrives, the server checks if the client's expected version matches the current database version. If they match, the mutation is applied and the version is incremented. If they do not match, the server returns a conflict error along with the current state. The client then merges the current state with the user's intended changes and optionally retries. For non-conflicting fields (e.g., one user changes priority while another changes status), a three-way merge can resolve the conflict automatically.
Q5: Design the data model for supporting complex filters and views.
Filters are represented as composable predicate objects with a field, operator, and value. Multiple predicates are combined with AND/OR logic. The filter JSON is sent to the server, which compiles it into SQL WHERE clauses with parameterized values. Performance is maintained through strategic indexing: B-tree indexes for equality and range queries on status, priority, and assignee; GIN indexes for label and custom field searches on JSONB columns. Materialized view caches store results for commonly accessed filter combinations, invalidated by mutation events. The filter compiler optimizes predicate ordering to apply the most selective filters first.
Q6: How would you design the API rate limiting system?
Implement a multi-tier rate limiting system. Per-user limits use a sliding window algorithm stored in Redis, allowing 1000 requests per minute per API key. Per-workspace limits use a token bucket algorithm allowing 10,000 requests per minute per workspace. Query complexity analysis assigns costs to each GraphQL field (e.g., listing issues costs 10 points, resolving a nested field costs 5 points) and rejects queries exceeding a 5000-point budget. The rate limiter runs at the API gateway level, rejecting requests before they reach the GraphQL server. Rate limit headers (X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset) are included in every response to help clients self-throttle.
Q7: How would you scale the system to support 100 million issues?
Partition the database by workspace ID using PostgreSQL table partitioning. Each workspace's data resides in its own partition, enabling partition-level operations (backup, vacuum) without affecting other tenants. Read replicas handle read-heavy workloads, with connection routing based on query type (reads go to replicas, writes go to primary). Redis caches frequently accessed data (team configs, recent issues per user). Elasticsearch handles full-text search with its own sharding strategy. For analytics, export event data to ClickHouse which provides columnar storage and fast aggregation over billions of rows. Background job queues are partitioned by workspace to ensure fair scheduling.
Q8: Design the webhook delivery system for external integrations.
Webhooks are stored in a database table with URL, secret, event filters, and active status. When an event occurs, the system queries for matching webhooks and enqueues delivery jobs. Each delivery job generates a JSON payload, computes an HMAC-SHA256 signature using the webhook secret, and sends an HTTP POST to the webhook URL with a 10-second timeout. Failed deliveries are retried with exponential backoff (1min, 5min, 30min, 2hrs). A deduplication table prevents duplicate deliveries from retried events. The delivery log records each attempt's request, response, and status for debugging. After 4 failed attempts, the webhook is marked as failing and the workspace admin is notified.
Q9: How would you implement the command palette with fuzzy search?
The command palette maintains an in-memory registry of all available commands, each with a label, category, keyboard shortcut, and handler. Fuzzy matching scores each command against the user's input by checking character-by-character matches with bonuses for consecutive matches, word boundary matches, and exact matches. Results are ranked by score and limited to the top 10. The command list is virtualized to render only visible items. Opening the palette pre-computes the filtered list based on the current context (selected issue, active view) to scope available commands. Keyboard navigation (arrow keys, Enter, Escape) is handled by a key event listener that prevents default browser behavior.
Q10: How would you design the analytics pipeline for computing cycle time, throughput, and velocity metrics?
Implement event sourcing: every issue state change is recorded as an immutable event with timestamps for when the issue entered each state. The event stream is processed by an ETL pipeline that extracts cycle time (time between InProgress and Completed events), counts completed issues per time period for throughput, and computes rolling averages for velocity. Raw events are stored in PostgreSQL for recent data and exported to ClickHouse for historical analytics. Materialized views pre-compute commonly requested aggregations (team velocity by cycle, average cycle time by priority). The analytics API queries these materialized views with time-range filters, returning results in under 100ms even for teams with millions of historical events.