# **File: COMMIT\_06\_WRITE\_FLOW.md** ## **Commit 6: Core Write-Through Cache API** ### **Objective** Implement the POST /api/nodes route. This is the core "write-through cache" logic, which is the architectural foundation of the application. It must: 1. Authenticate the user via their SurrealDB JWT. 2. Retrieve their ATproto access token (from the encrypted cookie). 3. **Step 1 (Truth):** Publish the new node to their PDS using the com.ponderants.node lexicon. 4. **Step 2 (Cache):** Generate a gemini-embedding-001 vector from the node's body. 5. **Step 3 (Cache):** Write the node, its atp\_uri, and its embedding to our SurrealDB cache. ### **Implementation Specification** **1\. Create lib/db.ts** Create a helper file at /lib/db.ts for connecting to SurrealDB: TypeScript import { Surreal } from 'surrealdb.js'; const db \= new Surreal(); /\*\* \* Connects to the SurrealDB instance. \* @param {string} token \- The user's app-specific (SurrealDB) JWT. \*/ export async function connectToDB(token: string) { if (\!db.connected) { await db.connect(process.env.SURREALDB\_URL\!); } // Authenticate as the user for this request. // This enforces the row-level security (PERMISSIONS) // defined in the schema for all subsequent queries. await db.authenticate(token); return db; } **2\. Create lib/ai.ts** Create a helper file at /lib/ai.ts for AI operations: TypeScript import { GoogleGenerativeAI } from '@google/generative-ai'; const genAI \= new GoogleGenerativeAI(process.env.GOOGLE\_API\_KEY\!); const embeddingModel \= genAI.getGenerativeModel({ model: 'gemini-embedding-001', }); /\*\* \* Generates a vector embedding for a given text. \* @param text The text to embed. \* @returns A 1536-dimension vector (Array\). \*/ export async function generateEmbedding(text: string): Promise\ { try { const result \= await embeddingModel.embedContent(text); return result.embedding.values; } catch (error) { console.error('Error generating embedding:', error); throw new Error('Failed to generate AI embedding.'); } } **3\. Create Write API Route (app/api/nodes/route.ts)** Create the main API file at /app/api/nodes/route.ts: TypeScript import { NextRequest, NextResponse } from 'next/server'; import { cookies } from 'next/headers'; import { AtpAgent, RichText } from '@atproto/api'; import { connectToDB } from '@/lib/db'; import { generateEmbedding } from '@/lib/ai'; export async function POST(request: NextRequest) { const surrealJwt \= cookies().get('ponderants-auth')?.value; const atpAccessToken \= cookies().get('atproto\_access\_token')?.value; if (\!surrealJwt ||\!atpAccessToken) { return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); } let userDid: string; try { // Decode the JWT to get the DID for the SurrealDB query // In a real app, we'd verify it, but for now we just // pass it to connectToDB which authenticates with it. const { payload } \= jwt.decode(surrealJwt, { complete: true })\!; userDid \= (payload as { did: string }).did; } catch (e) { return NextResponse.json({ error: 'Invalid auth token' }, { status: 401 }); } const { title, body, links } \= (await request.json()) as { title: string; body: string; links: string; // Array of at-uri strings }; if (\!title ||\!body) { return NextResponse.json({ error: 'Title and body are required' }, { status: 400 }); } const createdAt \= new Date().toISOString(); // \--- Step 1: Write to Source of Truth (ATproto) \--- let atp\_uri: string; let atp\_cid: string; try { const agent \= new AtpAgent({ service: 'https://bsky.social' }); // The service URL may need to be dynamic await agent.resumeSession({ accessJwt: atpAccessToken, did: userDid, handle: '' }); // Simplified resume // Format the body as RichText const rt \= new RichText({ text: body }); await rt.detectFacets(agent); // Detect links, mentions const response \= await agent.post({ $type: 'com.ponderants.node', repo: userDid, collection: 'com.ponderants.node', record: { title, body: rt.text, facets: rt.facets, // Include facets for rich text links: links?.map(uri \=\> ({ $link: uri })) ||, // Convert URIs to strong refs createdAt, }, }); atp\_uri \= response.uri; atp\_cid \= response.cid; } catch (error) { console.error('ATproto write error:', error); return NextResponse.json({ error: 'Failed to publish to PDS' }, { status: 500 }); } // \--- Step 2: Generate AI Embedding (Cache) \--- let embedding: number; try { embedding \= await generateEmbedding(title \+ '\\n' \+ body); } catch (error) { console.error('Embedding error:', error); return NextResponse.json({ error: 'Failed to generate embedding' }, { status: 500 }); } // \--- Step 3: Write to App View Cache (SurrealDB) \--- try { const db \= await connectToDB(surrealJwt); // Create the node record in our cache. // The \`user\_did\` field is set, satisfying the 'PERMISSIONS' // clause defined in the schema. const newNode \= await db.create('node', { user\_did: userDid, atp\_uri: atp\_uri, title: title, body: body, // Store the raw text body embedding: embedding, // coords\_3d will be calculated later }); // Handle linking if (links && links.length \> 0) { // Find the corresponding cache nodes for the AT-URIs const targetNodes: { id: string } \= await db.query( 'SELECT id FROM node WHERE user\_did \= $did AND atp\_uri IN $links', { did: userDid, links: links } ); // Create graph relations for (const targetNode of targetNodes) { await db.query('RELATE $from-\>links\_to-\>$to', { from: (newNode as any).id, to: targetNode.id, }); } } return NextResponse.json(newNode); } catch (error) { console.error('SurrealDB write error:', error); // TODO: Implement rollback for the ATproto post? return NextResponse.json({ error: 'Failed to save to app cache' }, { status: 500 }); } } ### **Test Specification** This is an API-only commit. It will be tested via the end-to-end flow in **Commit 10 (Linking)**, which will provide the UI (the "Publish" button) to trigger this route.