nyxcore-systems
6 min read

The Art of the Sync: An End-to-End Journey into Real-time Project Synchronization

Dive deep into the full-stack implementation of a robust project synchronization feature, from crafting database schemas and fetching GitHub data to building real-time UI updates with SSE and tRPC.

TypeScriptNext.jstRPCPrismaSSEGitHub APIFullstackDevelopment WorkflowRealtime

Ever found yourself wishing your local development environment could effortlessly mirror the latest state of your remote repository, including historical context and specific branch selections? That's precisely the challenge we tackled head-on in our latest development sprint: building a powerful "Project Sync" feature.

This past session was a whirlwind of activity, pushing us closer to a fully integrated, real-time synchronization experience. We set out to implement Phase 1 of Project Sync, focusing on branch selection and a robust synchronization pipeline. With 13 tasks on our plate, we're thrilled to report significant progress, completing the first nine and gearing up for the final integration push.

The Mission: Synchronizing with Precision

Our core goal was to create a system that could:

  1. Fetch project metadata and file structures from GitHub.
  2. Allow users to select a specific branch for synchronization.
  3. Provide real-time feedback on the sync progress.
  4. Maintain a history of synchronization events.

Let's break down how we built the engine and the interface for this ambitious feature.

The Engine Room: Backend Foundations

Building a feature like Project Sync requires a solid backend that can handle data fetching, processing, and real-time streaming.

1. Schema Design: The Blueprint for History At the heart of any data-driven feature is the database schema. We introduced the ProjectSync model, which not only tracks the current state but also references MemoryEntry, RepositoryFile, and Repository models. A critical decision here was adding a self-referencing @unique constraint on previousSyncId within the ProjectSync model. This seemingly small detail is crucial for maintaining an efficient history, allowing us to easily link syncs and identify superseded states without complex queries later on.

typescript
// Simplified ProjectSync Schema (Prisma)
model ProjectSync {
  id              String   @id @default(cuid())
  projectId       String
  branch          String
  status          SyncStatus
  createdAt       DateTime @default(now())
  completedAt     DateTime?
  // ... other fields
  previousSyncId  String?  @unique // Critical for linking history!
  previousSync    ProjectSync? @relation("SyncHistory", fields: [previousSyncId], references: [id])
  nextSync        ProjectSync? @relation("SyncHistory")
}

2. GitHub Connector: Tapping into the Source To fetch the raw material, our github-connector.ts received some vital upgrades:

  • fetchBranches(): To populate the branch selection dropdown.
  • fetchBranchHead(): To get the latest commit SHA of a chosen branch.
  • fetchRepoTreeWithSha(): To retrieve the entire file tree for a specific commit, enabling deep synchronization.

3. The AsyncGenerator Pipeline: Streaming Data in Real-time The true magic happens in project-sync-service.ts. This is where we implemented a full AsyncGenerator pipeline. Why AsyncGenerator? Because synchronization is a long-running process, involving multiple steps (fetching, processing, storing). An AsyncGenerator allows us to yield progress updates and intermediate results in real-time, making it perfect for driving a Server-Sent Events (SSE) stream to the frontend. It's like having a dedicated conveyor belt for our sync process.

4. Real-time Updates with SSE To keep the frontend informed, we set up a dedicated SSE endpoint at /api/v1/events/project-sync/[syncId]. As the AsyncGenerator yields new progress or status updates, these are immediately pushed through this endpoint, creating a seamless, real-time user experience.

5. tRPC: Our Type-Safe API Gateway Finally, our projects.sync sub-router in tRPC acts as the robust, type-safe API gateway to all these backend capabilities. It provides endpoints for fetching branches, checking sync status, initiating a sync, viewing history, and even restoring memory entries from previous syncs.

Bringing it to Life: The Frontend Experience

With a powerful backend in place, the next step was to craft an intuitive user interface that leverages its real-time capabilities.

1. useProjectSync Hook: The Frontend Nerve Center We developed a custom useProjectSync React hook. This hook cleverly wraps our useSSE hook, connecting directly to the SSE endpoint and providing the frontend with real-time updates from the backend sync pipeline. This abstraction makes it incredibly easy for components to react to sync progress.

2. SyncBanner Component: Visualizing Progress To give users immediate feedback, we built the SyncBanner component. This banner displays:

  • Phase dots: Indicating the current stage of the sync.
  • A progress bar: Showing overall completion.
  • Key statistics: Such as files processed or time elapsed.

3. SyncControls Component: User Interaction The SyncControls component is where users interact with the sync feature. It includes:

  • A branch dropdown: Populated by fetchBranches(), allowing users to select their desired branch.
  • A "Sync Now" button: Kicking off the AsyncGenerator pipeline and the SSE stream.

Navigating the Treacherous Waters: Lessons Learned

Even with a clear plan, development always throws a few curveballs. These "pain points" are often the most valuable learning experiences.

1. The previousSyncId @unique Constraint: Initially, we considered a simple foreign key. However, realizing the need to efficiently query for the latest sync and to prevent multiple "next" syncs from pointing to the same "previous" sync, the @unique constraint became essential. It ensures data integrity for our history tracking and simplifies future queries to determine superseded entries.

2. The Unpushed Schema: A classic oversight! After defining the new ProjectSync model and extensions, it's easy to forget the crucial step of actually pushing the schema changes to the database (e.g., running npx prisma migrate dev). This was quickly caught and rectified, a good reminder that schema migrations are an integral part of the development cycle.

3. ctx.prisma vs. Bare prisma in tRPC: In our tRPC router, we encountered a potential inconsistency: using ctx.prisma (the Prisma client injected into the tRPC context) versus directly importing a bare prisma client instance. It's critical to verify consistency in Task 12. Using ctx.prisma is generally preferred as it allows for better dependency injection, potential transaction management across multiple operations, and easier testing/mocking.

The Road Ahead: The Final Stretch

With the core components built and interacting, we're now entering the crucial integration and verification phase. Our immediate next steps are:

  1. Integrate SyncControls: Embed our SyncControls component directly into project-overview.tsx, making the sync feature accessible within the project's main view.
  2. Filter Superseded Entries: Implement logic to filter out superseded memory entries from active queries, ensuring users always see the most relevant data. This leverages our previousSyncId design.
  3. Typecheck + Build Verification: The big one! This will be our first full compilation check of the entire new feature, ensuring type safety and catching any integration issues before deployment.
  4. Production Deployment: Once verified, pushing this powerful new feature live!

Conclusion

This session has been incredibly productive, transforming a complex feature idea into a tangible, real-time experience. From the careful design of our database schema and the strategic use of AsyncGenerator and SSE on the backend, to the intuitive SyncBanner and SyncControls on the frontend, every piece is falling into place.

While the journey had its learning moments, overcoming these challenges has only strengthened our understanding and the robustness of the system. We're excited to push Project Sync Phase 1 live and look forward to the next phases, bringing even more powerful synchronization capabilities to our users!