lundie.io Get In Touch

Phase 3: Early Implementation

31 July – 13 August 2025

First Commits

It was time to see which bits of the plan survived 'first (code) contact'. This phase had narrow ambitions:

  • backend serving real data
  • frontend rendering real components
  • graph model implemented end-to-end (for reviewing Phase 2 architecture)

Backend Foundation (inpromptout-api)

The backend went first. Python and FastAPI were familiar territory - initial boilerplate and implementation was simple.

Repository Pattern

One repository class per entity type, all routed through the same Firestore client. The payoff was testability: the service layer could be run against in-memory fakes (the Firestore emulator was set up later).

  • NodeRepository – CRUD for all five node types
  • EdgeRepository – relationship storage and retrieval
  • registry_repo – fast per-user/per-type lookups
  • UserRepository – profile data
Repository Pattern Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class NodeRepository:
    def __init__(self, firestore_client):
        self.db = firestore_client
        self.collection = self.db.collection('nodes')

    async def create(self, node_data: dict) -> dict:
        # Validation
        # Firestore write
        # Return created node

    async def get_by_id(self, node_id: str) -> Optional[dict]:
        # Firestore read
        # Transform to API model

    async def update(self, node_id: str, updates: dict) -> dict:
        # Ownership check
        # Firestore update
        # Return updated node
    

Service Layer

Business logic stayed out of both the repositories and the routers.

  • NodeService – validation, capability checks, graph operations
  • EdgeService – edge creation, traversal, relationship rules
  • OpenAIService – scaffolding with a mock-mode toggle

API Routers

  • POST /api/v1/thoughts – create thought nodes
  • POST /api/v1/prompts – create prompt nodes
  • POST /api/v1/insights – create insight nodes
  • GET /api/v1/nodes/{node_id} – fetch any node by ID
  • POST /api/v1/edges – create relationships between nodes

These were straightforward single-resource endpoints. A batch mutations endpoint (applying create-edge, move-node, delete-edge and update-node operations together) landed in Phase 4 once the tree component made the need obvious.

Frontend Initialisation (inpromptout-web)

The first frontend commit landed on 31 July.

  • Vite + React 19 + TypeScript
  • Feature-based folder layout (/features/auth, /features/nodes, /features/editor)
  • Path aliases (@/, @nodes) to keep imports readable
  • Tailwind CSS for styling, later supplemented by a Radiant-based design system

One component per node type, each owning its own editing, rendering and media state. No graph layout logic yet – how these would sit together in a tree was deferred until Phase 4.

  • ThoughtNode.tsx – editable thought cards
  • PromptNode.tsx – prompt display with optional image
  • InsightNode.tsx – insight capture form
  • ResultNode.tsx – media attachment preview
ResultNode – trimmed from the 31 July commit, roughly 200 lines in the original
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import { useState } from 'react';
import type { ResultNode as ResultNodeType, MediaItem } from '../types/treeNode';

interface ResultNodeProps {
  result: ResultNodeType;
  isEditable?: boolean;
  onChange?: (updatedResult: ResultNodeType) => void;
}

interface MediaPreviewProps {
  media: MediaItem;
  onClick?: (media: MediaItem) => void;
}

function MediaPreview({ media, onClick }: MediaPreviewProps) {
  // image thumbnail, icon fallback for video/audio/document, hover overlay...
}

export function ResultNode({ result, isEditable, onChange }: ResultNodeProps) {
  const [isExpanded, setIsExpanded] = useState(true);
  const [selectedMedia, setSelectedMedia] = useState<MediaItem | null>(null);

  return (
    <div className="space-y-3">
      <h3>{result.title}</h3>
      {result.description && <p>{result.description}</p>}

      {isExpanded && result.media.length > 0 && (
        <div className="grid gap-3 ...">
          {result.media.map((media) => (
            <MediaPreview key={media.id} media={media} onClick={setSelectedMedia} />
          ))}
        </div>
      )}

      {/* modal viewer for the selected media item */}
    </div>
  );
}
    

Graph Traversal – Initial Implementation (10–13 Aug)

Fetching a user's full graph was the first feature that stretched the data model. The first version walked it breadth-first, but the Firestore read pattern was the classic N+1:

  • Query for the root node
  • For each node popped from the queue, one query for its outgoing edges
  • For each edge, one query for the target node
  • Repeat until the queue is empty

Reads scaled linearly with graph size. Batching was the obvious fix but wasn't a two-weeks-in priority – the goal was testing the data model, not optimising reads. The overhaul landed in Phase 8.

Unified Node Registry (13 Aug)

Commit: ef8722d – "Implement unified node registry system and standardise schemas"

One piece of cost thinking did land early. Firestore bills per document read and querying all of a user's nodes for some type X was going to be a common pattern. Against the main nodes collection, that meant reading full node documents – content, metadata, timestamps – just to build a list view.

A simple solution was a per-node row in a dedicated nodes_registry collection, keyed on node_id. This would allow for efficient ownership and type-filter queries:

Node Registry Schema
1
2
3
4
5
6
7
nodes_registry/
  {node_id}/
    id: str
    node_type: "thought"
    user_id: "user123"
    created_at: str
    

As it was cheap to maintain and cheap to query, this same pattern was carried through every optimisation that followed. By the time Phase 9 came around and the graph traversal got its proper rewrite, the registry was already there and already indexed. A speculative choice from two weeks in had quietly paid for itself.

OpenAI Service Scaffolding

The OpenAI integration was stubbed out behind a mock-mode flag from the start. USE_MOCK_API=true returned predefined prompts so the frontend could be developed without burning API credits. Real OpenAI calls waited until Phase 7.

First Tests

  • Backend: pytest with fixtures running against the Firestore emulator
  • Frontend: Vitest + jsdom, with thin component coverage to start
  • Integration: a shell script running the full API test suite against the emulator

What This Phase Established

By mid-August the loop was working end-to-end. Backend APIs served nodes and edges, the frontend rendered them while Firestore stored and returned them correctly. The parts of the Phase 2 plan I'd been least sure about – the service-layer separation, the registry collection, strict typing across the boundary – paid off quickly enough that I stopped second-guessing them.

The rough edges were explicit rather than hidden. N+1 graph queries, a hardcoded test_user in place of real auth, list-based UI with no tree yet, no optimistic updates. Each had a phase already attached to it. The next piece of work was the tree component, which turned out to be considerably more involved than "build a tree component" suggests.

Key Commits from This Phase

Web 96f922e 2025-07-31
Initial commit for Web
Web ed98da9 2025-07-31
Add ResultNode component with media support and integrate into tree structure
API f31dc21 2025-08-08
Reinitialising repo with Phase 3 commit
API 156faca 2025-08-08
[Bug Fix] Added fixes for failing tests
API 9584b4f 2025-08-10
Reinitialising repo with Phase 3 commit
API ef8722d 2025-08-13
[Refactor] Implement unified node registry system and standardise schemas

Get In Touch

Prefer using email? Say hi at hello@lundie.io