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
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 nodesPOST /api/v1/prompts– create prompt nodesPOST /api/v1/insights– create insight nodesGET /api/v1/nodes/{node_id}– fetch any node by IDPOST /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
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:
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.