Architecture

Table of Contents

Overview

The GitHub Projects plugin integrates GitHub's Projects V2 API into Obsidian's plugin ecosystem, providing a Kanban board interface for project management. The architecture emphasizes:

High-Level Architecture

┌─────────────────────────────────────────────────────────────┐
│                     Obsidian Application                     │
│  ┌───────────────────────────────────────────────────────┐  │
│  │              GitHub Projects Plugin                    │  │
│  │                                                         │  │
│  │  ┌──────────────┐      ┌──────────────┐              │  │
│  │  │   Settings   │      │  Project     │              │  │
│  │  │     Tab      │◄────►│  Board View  │              │  │
│  │  └──────────────┘      └──────┬───────┘              │  │
│  │                               │                       │  │
│  │                        ┌──────▼───────┐              │  │
│  │                        │    State     │              │  │
│  │                        │  Management  │              │  │
│  │                        │ (EventEmitter)│             │  │
│  │                        └──────┬───────┘              │  │
│  │                               │                       │  │
│  │         ┌─────────────────────┼─────────────────┐    │  │
│  │         │                     │                 │    │  │
│  │    ┌────▼────┐         ┌──────▼──────┐   ┌─────▼────┐  │
│  │    │ GitHub  │         │   Storage   │   │   UI     │  │
│  │    │   API   │         │  (localStorage)│ │Components│  │
│  │    │ Client  │         └─────────────┘   │ (Preact) │  │
│  │    └────┬────┘                           └──────────┘  │
│  │         │                                              │  │
│  └─────────┼──────────────────────────────────────────────┘  │
│            │                                                │
└────────────┼────────────────────────────────────────────────┘
             │
       ┌─────▼──────┐
       │  GitHub    │
       │ Projects V2│
       │ GraphQL API│
       └────────────┘

Core Components

1. Plugin Entry Point (main.tsx)

The main plugin class extends Obsidian's Plugin base class.

Responsibilities:

Key Methods:

class GitHubProjectsPlugin extends Plugin {
  async onload() {
    // Load settings
    // Register view
    // Add ribbon icon
    // Register commands
  }

async onunload() { // Cleanup resources // Detach views } }

2. Settings Tab (settings.ts)

Manages plugin configuration UI.

Responsibilities:

Settings Schema:

interface PluginSettings {
  githubToken: string;       // Stored in localStorage
  organization: string;      // GitHub org name
  projectNumber: number;     // Project number from URL
  autoRefreshInterval: number; // Minutes
}

3. Project Board View (views/ProjectBoardView.tsx)

Custom ItemView displaying the Kanban board.

Responsibilities:

Lifecycle:

class ProjectBoardView extends ItemView {
  async onOpen() {
    // Initialize state
    // Fetch project data
    // Render UI
  }

async onClose() { // Cleanup event listeners // Save state } }

4. State Management (state/ProjectState.ts)

Centralized state container using event-driven pattern.

Responsibilities:

Events:

5. API Client (api/client.ts)

GraphQL client for GitHub Projects V2 API.

Responsibilities:

Key Methods:

class GitHubAPIClient {
  async fetchProject(org, projectNumber): Promise
  async updateItemStatus(itemId, statusId): Promise
  async addItemToProject(projectId, contentId): Promise
}

6. UI Components (views/components/)

Preact components for rendering the board.

Component Hierarchy:

ProjectBoard
├── FilterBar
├── Column (multiple)
│   └── Card (multiple)
│       ├── Avatar (multiple)
│       └── Badge
└── DetailModal

Data Flow

1. Loading Project Data

User Opens Board
       ↓
ProjectBoardView.onOpen()
       ↓
ProjectState.loadProject()
       ↓
GitHubAPIClient.fetchProject()
       ↓
GitHub GraphQL API
       ↓
ProjectState.setProject()
       ↓
Event: 'project-loaded'
       ↓
ProjectBoardView re-renders

2. Moving a Card (Optimistic Update)

User Drags Card
       ↓
onDrop handler
       ↓
ProjectState.moveItem() (optimistic)
       ↓
Event: 'item-moved'
       ↓
UI updates immediately
       ↓
GitHubAPIClient.updateItemStatus()
       ↓
GitHub GraphQL API
       ↓
Background verification on next refresh

3. Auto-Refresh

Timer triggers (every N minutes)
       ↓
ProjectState.refresh()
       ↓
Event: 'project-refreshing'
       ↓
UI shows loading indicator
       ↓
GitHubAPIClient.fetchProject()
       ↓
ProjectState.setProject()
       ↓
Event: 'project-loaded'
       ↓
UI updates with fresh data

State Management

Event-Driven Pattern

Using Obsidian's Events class for pub/sub pattern:

class ProjectState {
  private events = new Events();
  private projectData: Project | null = null;

on(event: string, callback: (...args: any[]) => void) { this.events.on(event, callback); }

setProject(project: Project) { this.projectData = project; this.events.trigger('project-loaded', project); } }

State Immutability

State updates create new objects:

moveItem(itemId: string, newStatusId: string) {
  const newItems = this.projectData.items.map(item =>
    item.id === itemId
      ? { ...item, statusId: newStatusId }
      : item
  );

this.setProject({ ...this.projectData, items: newItems }); }

Caching Strategy

API Integration

GraphQL Queries

Fetch Project:

query GetProject($org: String!, $number: Int!) {
  organization(login: $org) {
    projectV2(number: $number) {
      id
      title
      fields(first: 20) {
        nodes {
          ... on ProjectV2SingleSelectField {
            id
            name
            options {
              id
              name
            }
          }
        }
      }
      items(first: 100) {
        nodes {
          id
          content {
            ... on Issue {
              title
              number
              state
            }
            ... on PullRequest {
              title
              number
              state
            }
          }
          fieldValues(first: 20) {
            nodes {
              ... on ProjectV2ItemFieldSingleSelectValue {
                field { id }
                optionId
              }
            }
          }
        }
      }
    }
  }
}

Update Item Status:

mutation UpdateItemStatus($projectId: ID!, $itemId: ID!, $fieldId: ID!, $value: String!) {
  updateProjectV2ItemFieldValue(
    input: {
      projectId: $projectId
      itemId: $itemId
      fieldId: $fieldId
      value: { singleSelectOptionId: $value }
    }
  ) {
    projectV2Item {
      id
    }
  }
}

Rate Limiting

GitHub GraphQL API limits:

Mitigation:

Error Handling

try {
  const response = await this.fetchProject(org, number);
  return response;
} catch (error) {
  if (error.status === 401) {
    throw new Error('Invalid token');
  } else if (error.status === 404) {
    throw new Error('Project not found');
  } else {
    throw new Error('Network error');
  }
}

UI Layer

Preact Components

We use Preact (React-compatible, 3KB) for:

Drag and Drop

Using SortableJS for drag-and-drop:

import Sortable from 'sortablejs';

const sortable = Sortable.create(columnElement, { group: 'board', animation: 150, onEnd: (evt) => { const itemId = evt.item.dataset.itemId; const newStatusId = evt.to.dataset.statusId; projectState.moveItem(itemId, newStatusId); } });

Styling

.github-projects-board {
  --bg-color: var(--background-primary);
  --text-color: var(--text-normal);
}

.github-projects-board__column { } .github-projects-board__card { }

Storage & Security

Token Storage

Tokens stored in localStorage (NOT vault files):

// Save token
localStorage.setItem('github-projects-token', token);

// Retrieve token const token = localStorage.getItem('github-projects-token');

Security considerations:

Recommendations:

Settings Storage

Non-sensitive settings stored in vault:

.obsidian/plugins/github-projects/data.json

Contains: organization, project number, refresh interval (NOT token)

Performance Considerations

Virtual Scrolling

For boards with 100+ cards, implement virtual scrolling:

import { VirtualScroller } from './VirtualScroller';

} />

Debouncing

Debounce expensive operations:

const debouncedRefresh = debounce(
  () => projectState.refresh(),
  1000
);

Memoization

Memoize expensive calculations:

const columns = useMemo(() => {
  return groupItemsByStatus(items);
}, [items]);

Bundle Size

Current bundle size: ~120KB (minified)

Future Enhancements

Planned Features

Architecture Changes

References