<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Tomoki Ikeda</title>
    <description>The latest articles on DEV Community by Tomoki Ikeda (@tomokiikeda).</description>
    <link>https://dev.to/tomokiikeda</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3826613%2Fd3337bc9-9e88-4416-83ad-ae7d9318e4a7.jpg</url>
      <title>DEV Community: Tomoki Ikeda</title>
      <link>https://dev.to/tomokiikeda</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tomokiikeda"/>
    <language>en</language>
    <item>
      <title>I Gave Claude Code a Memory — Here's How MCP Connects AI Tools to Your Knowledge Base</title>
      <dc:creator>Tomoki Ikeda</dc:creator>
      <pubDate>Tue, 24 Mar 2026 15:21:27 +0000</pubDate>
      <link>https://dev.to/tomokiikeda/i-gave-claude-code-a-memory-heres-how-mcp-connects-ai-tools-to-your-knowledge-base-3l56</link>
      <guid>https://dev.to/tomokiikeda/i-gave-claude-code-a-memory-heres-how-mcp-connects-ai-tools-to-your-knowledge-base-3l56</guid>
      <description>&lt;p&gt;&lt;strong&gt;Claude Code doesn't remember what you built last week.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Neither does ChatGPT. Neither does Cursor. Every AI tool starts fresh — no memory of your architecture decisions, debugging sessions, or that clever workaround you spent two hours on.&lt;/p&gt;

&lt;p&gt;In my &lt;a href="https://dev.to/tomokiikeda/how-i-auto-capture-coding-sessions-from-25-ai-tools-architecture-deep-dive-2p3i"&gt;previous article&lt;/a&gt;, I showed how Nokos auto-captures coding sessions. But capturing is only half the story. The other half: &lt;strong&gt;letting AI tools search that knowledge.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's where MCP comes in.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is MCP?
&lt;/h2&gt;

&lt;p&gt;Model Context Protocol is an open standard that lets AI tools call external functions. Think of it as "APIs for AI" — instead of you copying context into a prompt, the AI calls a tool to fetch what it needs.&lt;/p&gt;

&lt;p&gt;Nokos exposes three MCP tools:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;search_nokos&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Search memos + coding sessions by keyword or meaning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_nokos&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fetch full content of any item&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;save_session&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Save the current AI conversation to Nokos&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With these three tools, any MCP-compatible AI can read from and write to your personal knowledge base.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Aha" Moment
&lt;/h2&gt;

&lt;p&gt;Here's what it looks like in practice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nc"&gt;You &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;Claude&lt;/span&gt; &lt;span class="nx"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;How did I implement auth in this project?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="nx"&gt;Claude&lt;/span&gt; &lt;span class="nx"&gt;Code&lt;/span&gt; &lt;span class="nx"&gt;calls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;search_nokos&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auth implementation&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nx"&gt;Nokos&lt;/span&gt; &lt;span class="nx"&gt;returns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="nx"&gt;relevant&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;
  &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;Session&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="nx"&gt;Feb&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Implemented Firebase Auth with Device Authorization Flow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;Memo&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="nx"&gt;Feb&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Auth architecture: stateless JWT + RLS&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;Session&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="nx"&gt;Feb&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Added unified auth middleware for API key, JWT, and Firebase&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="nx"&gt;Claude&lt;/span&gt; &lt;span class="nx"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Based on your previous sessions, you implemented a three-layer
auth approach: Firebase for web, Device Auth JWT for CLI, and API keys
for programmatic access. The unified middleware is in unified-auth.ts...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude Code answered with &lt;strong&gt;your actual implementation history&lt;/strong&gt; — not generic advice, not hallucinated code, but what you actually built. The AI searched your knowledge base, found relevant sessions, and synthesized an answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two MCP Servers, One Protocol
&lt;/h2&gt;

&lt;p&gt;We run two MCP servers that expose the same three tools:&lt;/p&gt;

&lt;h3&gt;
  
  
  Local Agent (stdio)
&lt;/h3&gt;

&lt;p&gt;For AI tools running on your machine — Claude Code, Codex, any MCP client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Claude Code ←→ stdio ←→ @nokos/cli mcp ←→ Nokos API
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI starts an MCP server over stdio. Authentication uses Device Auth JWT tokens stored in &lt;code&gt;~/.nokos/credentials.json&lt;/code&gt;. Setup is one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nokos setup  &lt;span class="c"&gt;# registers MCP server + hooks&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Remote Server (Streamable HTTP)
&lt;/h3&gt;

&lt;p&gt;For cloud-based AI tools — claude.ai, ChatGPT (when MCP-enabled):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;claude.ai ←→ HTTPS ←→ mcp.nokos.ai ←→ PostgreSQL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This runs on Cloud Run with OAuth 2.1 + PKCE for authentication. The remote server connects directly to the database — no extra API hop.&lt;/p&gt;

&lt;p&gt;Why two servers? &lt;strong&gt;Latency and access patterns are different.&lt;/strong&gt; The local agent proxies through the API (simple, works everywhere). The remote server has direct DB access (faster for cloud-based tools that need quick responses).&lt;/p&gt;

&lt;h2&gt;
  
  
  How Search Works Behind the Scenes
&lt;/h2&gt;

&lt;p&gt;When an AI calls &lt;code&gt;search_nokos&lt;/code&gt;, it hits a hybrid search pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Full-text search&lt;/strong&gt; (pg_bigm) — exact keyword matching, great for Japanese + English&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vector search&lt;/strong&gt; (pgvector) — semantic matching via embeddings, finds related content even without keyword overlap&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The results are merged and deduplicated. This means "auth implementation" matches sessions that talk about "Firebase authentication middleware" even without the exact word "auth."&lt;/p&gt;

&lt;p&gt;Both memos and coding sessions live in the same index. Your handwritten notes and AI-generated session logs are searchable together.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Knowledge Loop
&lt;/h2&gt;

&lt;p&gt;The real power isn't any single tool — it's the loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. You work with Claude Code → session auto-captured to Nokos
2. You write a memo in Nokos → indexed and embedded
3. Next Claude Code session → AI searches Nokos for context
4. AI gives better answers → captured again
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each cycle adds to the knowledge base. Over time, your AI gets more useful because it has more of your context.&lt;/p&gt;

&lt;p&gt;This isn't AGI memory. It's a practical, queryable database of your actual work — sessions, notes, decisions — that any AI tool can tap into through a standard protocol.&lt;/p&gt;

&lt;h2&gt;
  
  
  What MCP Gets Right (And What's Still Hard)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What works well:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Standard protocol means one integration works across multiple AI tools&lt;/li&gt;
&lt;li&gt;Tool descriptions guide the AI on when to call what&lt;/li&gt;
&lt;li&gt;Structured responses let the AI reason about results&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What's still hard:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI tools don't always know when to search. Sometimes Claude Code will try to answer from training data when your actual implementation is one MCP call away&lt;/li&gt;
&lt;li&gt;Context window limits mean you can't dump everything — the AI needs to be selective about what it retrieves&lt;/li&gt;
&lt;li&gt;OAuth setup for remote MCP is still friction. The local stdio path is much smoother&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;If you use Claude Code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @nokos/cli
nokos login
nokos setup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After setup, try asking Claude Code: "Search my Nokos for recent sessions about [topic]." It will call &lt;code&gt;search_nokos&lt;/code&gt; and return your actual work history.&lt;/p&gt;

&lt;p&gt;For claude.ai, connect via the MCP settings panel at &lt;code&gt;mcp.nokos.ai&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://nokos.ai" rel="noopener noreferrer"&gt;&lt;strong&gt;nokos.ai&lt;/strong&gt;&lt;/a&gt; — free plan available.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is article 5 in my series about building a SaaS with AI. &lt;a href="https://dev.to/tomokiikeda/zero-lines-of-code-how-claude-code-and-gemini-built-my-saas-35cn"&gt;Article 1: Zero Lines of Code&lt;/a&gt;. &lt;a href="https://dev.to/tomokiikeda/claude-writes-the-code-gemini-runs-it-how-two-competing-ais-cut-my-saas-costs-by-30x-2hn3"&gt;Article 2: AI Cost Split&lt;/a&gt;. &lt;a href="https://dev.to/tomokiikeda/postgresql-row-level-security-saved-my-saas-from-bugs-i-didnt-know-i-had-48gl"&gt;Article 3: PostgreSQL RLS&lt;/a&gt;. &lt;a href="https://dev.to/tomokiikeda/how-i-auto-capture-coding-sessions-from-25-ai-tools-architecture-deep-dive-2p3i"&gt;Article 4: Session Capture&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Launching on &lt;a href="https://www.producthunt.com/products/nokos" rel="noopener noreferrer"&gt;Product Hunt&lt;/a&gt; March 31st — follow for updates!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>devtools</category>
      <category>architecture</category>
    </item>
    <item>
      <title>"How I Auto-Capture Coding Sessions From 25+ AI Tools (Architecture Deep Dive)</title>
      <dc:creator>Tomoki Ikeda</dc:creator>
      <pubDate>Sun, 22 Mar 2026 13:35:30 +0000</pubDate>
      <link>https://dev.to/tomokiikeda/how-i-auto-capture-coding-sessions-from-25-ai-tools-architecture-deep-dive-2p3i</link>
      <guid>https://dev.to/tomokiikeda/how-i-auto-capture-coding-sessions-from-25-ai-tools-architecture-deep-dive-2p3i</guid>
      <description>&lt;p&gt;&lt;strong&gt;How many AI conversations did you have this week?&lt;/strong&gt; 10? 50? 100?&lt;/p&gt;

&lt;p&gt;How many can you find right now?&lt;/p&gt;

&lt;p&gt;That's the problem. AI coding tools generate enormous amounts of knowledge — architecture decisions, debugging sessions, implementation discussions — and all of it vanishes when you close the terminal.&lt;/p&gt;

&lt;p&gt;I built a system that captures every AI conversation automatically. It works with 25+ tools. The entire architecture is a hook, a CLI, and a parser pipeline.&lt;/p&gt;

&lt;p&gt;Here's how it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Knowledge That Disappears
&lt;/h2&gt;

&lt;p&gt;Every AI coding tool stores conversations differently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Claude Code&lt;/strong&gt; writes JSONL to &lt;code&gt;~/.claude/projects/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Codex&lt;/strong&gt; writes JSONL to &lt;code&gt;~/.codex/sessions/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cursor&lt;/strong&gt; stores data in SQLite&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ChatGPT&lt;/strong&gt; is accessible only via export&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copilot Chat&lt;/strong&gt; logs to VS Code output channels&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some tools give you hooks. Some give you files. Some give you nothing.&lt;/p&gt;

&lt;p&gt;I needed one system that could ingest all of them, normalize the data, and make it searchable. Not a viewer for each tool's format — a &lt;strong&gt;unified knowledge base&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AI Tool (session ends)
  ↓ hook / file watcher / manual push
@nokos/cli (local)
  ↓ reads session file, gzip compresses
Nokos API (cloud)
  ↓ tool-specific parser extracts messages
  ↓ AI generates title + metadata + summary
  ↓ vector embedding for semantic search
PostgreSQL (storage)
  ↓ full-text search + vector search
Nokos UI (web/mobile)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fully automatic for tools that support hooks. One command for everything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capturing Sessions: Three Patterns
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Pattern 1: Session Hooks (Claude Code, Codex)
&lt;/h3&gt;

&lt;p&gt;Claude Code has a &lt;code&gt;SessionEnd&lt;/code&gt; hook. When a conversation ends, it fires automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"SessionEnd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nokos push --from-hook"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"timeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30000&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hook pipes session metadata to stdin. The CLI reads the transcript file, compresses it, and sends it to the API. The user does nothing.&lt;/p&gt;

&lt;p&gt;We also hook into &lt;code&gt;PreCompact&lt;/code&gt; — when Claude Code compresses a long conversation, we capture the intermediate state before it's compacted. This means you don't lose progress during long sessions.&lt;/p&gt;

&lt;p&gt;Codex has a similar model — &lt;code&gt;agent-turn-complete&lt;/code&gt; notifications with the conversation payload. Same CLI, different input format.&lt;/p&gt;

&lt;p&gt;As a safety net, &lt;code&gt;nokos setup&lt;/code&gt; also configures a cron job that runs &lt;code&gt;nokos watch&lt;/code&gt; every 30 minutes. It detects changed session files and pushes them automatically — catching anything the hooks might miss.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 2: Extension APIs (Roo Code, Cline)
&lt;/h3&gt;

&lt;p&gt;For VS Code-based tools, we built an extension that listens for task completion events and file changes, then sends conversations to the API automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 3: Manual Push
&lt;/h3&gt;

&lt;p&gt;For tools without hooks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nokos push session.jsonl &lt;span class="nt"&gt;--tool&lt;/span&gt; cursor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One command. The CLI auto-detects the tool from the file path when possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Parser Pipeline
&lt;/h2&gt;

&lt;p&gt;This is where it gets interesting. Every AI tool has a different conversation format. Claude Code uses JSONL with content blocks. Codex uses OpenAI-style messages. Copilot Chat uses request/response pairs. The Anthropic format is a JSON array, not JSONL.&lt;/p&gt;

&lt;p&gt;We have &lt;strong&gt;7 dedicated parsers&lt;/strong&gt; and a generic fallback:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parser&lt;/th&gt;
&lt;th&gt;Tools&lt;/th&gt;
&lt;th&gt;Key difference&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;userType: "human"&lt;/code&gt;, content blocks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codex&lt;/td&gt;
&lt;td&gt;Codex CLI&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;role&lt;/code&gt; field, OpenAI format&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anthropic&lt;/td&gt;
&lt;td&gt;Roo Code, Cline&lt;/td&gt;
&lt;td&gt;JSON array of &lt;code&gt;MessageParam[]&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Copilot Chat&lt;/td&gt;
&lt;td&gt;Copilot Chat&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;request&lt;/code&gt;/&lt;code&gt;response&lt;/code&gt; pairs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cursor&lt;/td&gt;
&lt;td&gt;Cursor&lt;/td&gt;
&lt;td&gt;Workspace JSON with threads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aider&lt;/td&gt;
&lt;td&gt;Aider&lt;/td&gt;
&lt;td&gt;Markdown chat history&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini CLI&lt;/td&gt;
&lt;td&gt;Gemini CLI&lt;/td&gt;
&lt;td&gt;Gemini-specific content parts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generic&lt;/td&gt;
&lt;td&gt;Everything else&lt;/td&gt;
&lt;td&gt;Best-effort: JSON, JSONL, plain text&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every parser produces the same output: a normalized array of &lt;code&gt;{role, content}&lt;/code&gt; messages plus token stats and file change lists.&lt;/p&gt;

&lt;p&gt;The real challenge is &lt;strong&gt;format detection&lt;/strong&gt; — each tool's JSONL looks similar but has different field names, nesting structures, and content representations. Each parser checks for its tool's fingerprint before parsing. The generic fallback handles anything we haven't seen before, and it covers more cases than you'd expect.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI Summary + Semantic Search
&lt;/h2&gt;

&lt;p&gt;Raw session data is noisy — hundreds of JSONL lines with tool calls, file edits, and internal reasoning. Nobody wants to read that.&lt;/p&gt;

&lt;p&gt;The API generates a structured summary (title, category, tags, sentiment) and a vector embedding for each session. This means you can search by meaning, not just keywords. "That session where I fixed the auth bug" matches even if the word "auth" never appears in the transcript.&lt;/p&gt;

&lt;p&gt;Sessions and handwritten memos live in the same search index. Ask "What did I work on last week?" and you get both. This also powers &lt;strong&gt;Personal AI (RAG)&lt;/strong&gt; — when you chat with Nokos, it retrieves relevant memos &lt;em&gt;and&lt;/em&gt; sessions as context.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Compaction Problem
&lt;/h2&gt;

&lt;p&gt;Here's a production quirk nobody warns you about: &lt;strong&gt;Claude Code fires "Compacting conversation" multiple times per session.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every compaction triggers the &lt;code&gt;SessionEnd&lt;/code&gt; hook. A single coding session can generate 3-5 hook events. Without deduplication, you'd store 5 copies of the same conversation.&lt;/p&gt;

&lt;p&gt;Our solution: &lt;strong&gt;daily session IDs&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;today&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// "2026-03-22"&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseSessionId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;today&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each push upserts by session ID. Multiple pushes in one day update the same record. The next day starts fresh. Simple, and it solved the problem completely.&lt;/p&gt;

&lt;h2&gt;
  
  
  MCP: Closing the Loop
&lt;/h2&gt;

&lt;p&gt;The capture pipeline gets data &lt;em&gt;into&lt;/em&gt; Nokos. MCP (Model Context Protocol) lets AI tools search &lt;em&gt;from&lt;/em&gt; Nokos.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You (in Claude Code): "What was the approach I used for auth last month?"
  ↓ MCP tool call: search_nokos({ query: "auth approach" })
  ↓ Returns relevant memos and sessions
Claude Code: "Based on your session from Feb 15, you implemented..."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three MCP tools: &lt;code&gt;search_nokos&lt;/code&gt;, &lt;code&gt;get_nokos&lt;/code&gt;, &lt;code&gt;save_session&lt;/code&gt;. Available via local MCP (stdio, for Claude Code/Codex) and remote MCP (HTTP, for claude.ai/ChatGPT).&lt;/p&gt;

&lt;p&gt;The loop closes: AI tools generate knowledge → Nokos captures it → AI tools retrieve it. &lt;strong&gt;Your AI remembers what you've built.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;Set up auto-capture in 2 minutes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @nokos/cli
nokos login
nokos setup   &lt;span class="c"&gt;# configures Claude Code SessionEnd hook&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Every Claude Code session is now automatically captured, summarized, and searchable.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://nokos.ai" rel="noopener noreferrer"&gt;&lt;strong&gt;nokos.ai&lt;/strong&gt;&lt;/a&gt; — free plan available to try it out.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is article 4 in my series about building a SaaS with AI. &lt;a href="https://dev.to/tomokiikeda/zero-lines-of-code-how-claude-code-and-gemini-built-my-saas-35cn"&gt;Article 1: Zero Lines of Code&lt;/a&gt;. &lt;a href="https://dev.to/tomokiikeda/claude-writes-the-code-gemini-runs-it-how-two-competing-ais-cut-my-saas-costs-by-30x-2hn3"&gt;Article 2: AI Cost Split&lt;/a&gt;. &lt;a href="https://dev.to/tomokiikeda/postgresql-row-level-security-saved-my-saas-from-bugs-i-didnt-know-i-had-48gl"&gt;Article 3: PostgreSQL RLS&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Launching on &lt;a href="https://www.producthunt.com/products/nokos" rel="noopener noreferrer"&gt;Product Hunt&lt;/a&gt; March 31st — follow for updates!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>devtools</category>
      <category>productivity</category>
    </item>
    <item>
      <title>PostgreSQL Row-Level Security Saved My SaaS From Bugs I Didn't Know I Had</title>
      <dc:creator>Tomoki Ikeda</dc:creator>
      <pubDate>Fri, 20 Mar 2026 12:58:04 +0000</pubDate>
      <link>https://dev.to/tomokiikeda/postgresql-row-level-security-saved-my-saas-from-bugs-i-didnt-know-i-had-1bb5</link>
      <guid>https://dev.to/tomokiikeda/postgresql-row-level-security-saved-my-saas-from-bugs-i-didnt-know-i-had-1bb5</guid>
      <description>&lt;h1&gt;
  
  
  PostgreSQL Row-Level Security Saved My SaaS From Bugs I Didn't Know I Had
&lt;/h1&gt;

&lt;p&gt;I build &lt;a href="https://nokos.ai" rel="noopener noreferrer"&gt;Nokos&lt;/a&gt;, an AI note-taking app. Every user's memos, diaries, and coding sessions are stored in one PostgreSQL database. One authorization bug = one user sees another's private data.&lt;/p&gt;

&lt;p&gt;Most apps have &lt;strong&gt;one layer of defense&lt;/strong&gt;: application-level auth checks. We have two. The second layer — PostgreSQL Row-Level Security — has already caught bugs that our application code missed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup: One Function, Total Isolation
&lt;/h2&gt;

&lt;p&gt;Our entire RLS system hinges on one PostgreSQL function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;current_app_user_id&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;NULLIF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_setting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'app.current_user_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt; &lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt; &lt;span class="k"&gt;STABLE&lt;/span&gt; &lt;span class="k"&gt;SECURITY&lt;/span&gt; &lt;span class="k"&gt;DEFINER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every table policy checks: &lt;code&gt;WHERE user_id = current_app_user_id()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;On every API request, we set the session variable inside a transaction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;withRLS&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TransactionClient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$executeRaw&lt;/span&gt;&lt;span class="s2"&gt;`SELECT set_config('app.current_user_id', &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, true)`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;true&lt;/code&gt; in &lt;code&gt;set_config&lt;/code&gt; makes it transaction-local. When the transaction ends, the variable resets. No leakage between requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bug That RLS Caught
&lt;/h2&gt;

&lt;p&gt;We use fire-and-forget patterns for non-critical async work — like generating embeddings after a memo is saved:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// After memo is created, generate embedding async&lt;/span&gt;
&lt;span class="nf"&gt;generateEmbedding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;contentText&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// BUG: This runs OUTSIDE the withRLS transaction&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$executeRawUnsafe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`UPDATE memos SET embedding = $1::vector WHERE id = $2::uuid`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;memoId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks correct. The UPDATE has the right memo ID. What could go wrong?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Everything.&lt;/strong&gt; Because &lt;code&gt;memos&lt;/code&gt; has &lt;code&gt;FORCE ROW LEVEL SECURITY&lt;/code&gt;, and this code runs outside &lt;code&gt;withRLS()&lt;/code&gt;. There's no &lt;code&gt;app.current_user_id&lt;/code&gt; set. The function returns &lt;code&gt;NULL&lt;/code&gt;. The policy evaluates &lt;code&gt;user_id = NULL&lt;/code&gt;, which is always false.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result: 0 rows updated. No error. No warning. Complete silence.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The embedding was generated, the API call to Gemini was paid for, and the UPDATE ran successfully — it just matched zero rows. PostgreSQL doesn't consider "zero rows updated" an error.&lt;/p&gt;

&lt;p&gt;The fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;generateEmbedding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;contentText&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// FIXED: wrap in withRLS so the policy can match&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;withRLS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$executeRawUnsafe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;`UPDATE memos SET embedding = $1::vector WHERE id = $2::uuid`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;memoId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without RLS, this bug would have silently worked in dev and production. The UPDATE would hit the row directly, no policy check. We'd never know the auth boundary was missing — until someone found a way to exploit it.&lt;/p&gt;

&lt;h2&gt;
  
  
  FORCE vs NO FORCE: The Decision That Matters
&lt;/h2&gt;

&lt;p&gt;PostgreSQL RLS has a subtle setting: &lt;code&gt;FORCE ROW LEVEL SECURITY&lt;/code&gt; applies policies &lt;strong&gt;even to the table owner&lt;/strong&gt;. Without &lt;code&gt;FORCE&lt;/code&gt;, the owner role bypasses all policies.&lt;/p&gt;

&lt;p&gt;We use &lt;code&gt;FORCE&lt;/code&gt; on every table except five:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Table&lt;/th&gt;
&lt;th&gt;NO FORCE&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;users&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;Auth middleware looks up users by &lt;code&gt;firebase_uid&lt;/code&gt; &lt;strong&gt;before&lt;/strong&gt; user_id is known&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;user_usage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;Usage records are created during first login, before RLS context exists&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;api_keys&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;API key auth looks up keys by SHA-256 hash before user_id is known&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;device_refresh_tokens&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;Token refresh happens before user auth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;device_codes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;Device Authorization Flow — codes exist before any user is authenticated&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pattern: tables that are accessed &lt;strong&gt;before authentication completes&lt;/strong&gt; need &lt;code&gt;NO FORCE&lt;/code&gt;. Everything else — memos, books, tags, chat sessions, media — uses &lt;code&gt;FORCE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This means if we accidentally run a query outside &lt;code&gt;withRLS()&lt;/code&gt; on a &lt;code&gt;FORCE&lt;/code&gt; table, it returns zero rows instead of leaking data. &lt;strong&gt;Default deny.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Roles: App vs Batch
&lt;/h2&gt;

&lt;p&gt;We use two PostgreSQL roles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;nokos_app&lt;/code&gt;&lt;/strong&gt; — Used by the API. Subject to RLS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;nokos_batch&lt;/code&gt;&lt;/strong&gt; — Used by batch jobs (diary generation, embedding backfill). Has &lt;code&gt;row_security = off&lt;/code&gt;, completely bypassing RLS.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why? Batch jobs need to process data across all users. The daily diary generator reads all users' memos to create personalized diaries. Running this through RLS would require setting &lt;code&gt;app.current_user_id&lt;/code&gt; for each user in a loop — technically possible, but fragile and slow.&lt;/p&gt;

&lt;p&gt;The trade-off: &lt;code&gt;nokos_batch&lt;/code&gt; has no authorization boundary. We accept this because batch code runs in a controlled environment (Cloud Scheduler → Cloud Run endpoint with secret-based auth), never exposed to user input.&lt;/p&gt;

&lt;h2&gt;
  
  
  Policy Patterns
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Direct ownership&lt;/strong&gt; (most tables):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="n"&gt;memos_select&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;memos&lt;/span&gt;
  &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_app_user_id&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;JOIN-based&lt;/strong&gt; (junction tables without user_id):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="n"&gt;memo_tags_select&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;memo_tags&lt;/span&gt;
  &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;memos&lt;/span&gt;
            &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;memos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;memo_tags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;memo_id&lt;/span&gt;
            &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;memos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_app_user_id&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Open insert, restricted read&lt;/strong&gt; (users table — anyone can sign up):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="n"&gt;users_insert&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;
  &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="n"&gt;users_select&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;
  &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_app_user_id&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each policy is idempotent (&lt;code&gt;DROP POLICY IF EXISTS&lt;/code&gt; before &lt;code&gt;CREATE POLICY&lt;/code&gt;), so we safely reapply them on every deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Start with FORCE on every table.&lt;/strong&gt; We initially had some tables without it and had to migrate. Starting strict is easier than tightening later.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Test the silent failure.&lt;/strong&gt; We now have integration tests that verify: without &lt;code&gt;set_config&lt;/code&gt;, a query on a FORCE table returns zero rows. This catches the exact bug class described above.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Without set_config, non-superuser sees no data&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;appPrisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// No set_config call — should see nothing&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;memo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Document the NO FORCE exceptions.&lt;/strong&gt; Every &lt;code&gt;NO FORCE&lt;/code&gt; table should have a comment explaining why. Future developers (or future AI agents writing your code) need to know the reasoning.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Five Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;RLS is your second line of defense&lt;/strong&gt;, not a replacement for application auth. Both layers should exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;FORCE ROW LEVEL SECURITY&lt;/code&gt; is the critical setting.&lt;/strong&gt; Without it, your table owner bypasses all policies — which is most ORM connections.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Silent zero-row updates are the real danger.&lt;/strong&gt; RLS doesn't throw errors; it just filters. Test for this explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-auth tables need &lt;code&gt;NO FORCE&lt;/code&gt;.&lt;/strong&gt; Any table accessed before you know the user_id must opt out of FORCE.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate roles for app vs batch.&lt;/strong&gt; Don't hack around RLS in batch jobs — give them a role that bypasses it cleanly.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://nokos.ai" rel="noopener noreferrer"&gt;Nokos&lt;/a&gt; is protected by this exact RLS setup across 20+ tables. Free plan available — your data is isolated at the database level, not just the application level.&lt;/p&gt;

&lt;p&gt;Have you implemented RLS in your project? What patterns worked for you?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is article 3 in my series about building a SaaS with AI. &lt;a href="https://dev.to/tomokiikeda/zero-lines-of-code-how-claude-code-and-gemini-built-my-saas-35cn"&gt;Article 1: Zero Lines of Code&lt;/a&gt;. &lt;a href="https://dev.to/tomokiikeda/claude-writes-the-code-gemini-runs-it-how-two-competing-ais-cut-my-saas-costs-by-30x-2hn3"&gt;Article 2: Claude + Gemini Cost Split&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Launching on &lt;a href="https://www.producthunt.com/products/nokos" rel="noopener noreferrer"&gt;Product Hunt&lt;/a&gt; March 31st — follow for updates!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>security</category>
      <category>webdev</category>
      <category>saas</category>
    </item>
    <item>
      <title>Claude Writes the Code, Gemini Runs It: How Two Competing AIs Cut My SaaS Costs by 30x</title>
      <dc:creator>Tomoki Ikeda</dc:creator>
      <pubDate>Wed, 18 Mar 2026 11:20:24 +0000</pubDate>
      <link>https://dev.to/tomokiikeda/claude-writes-the-code-gemini-runs-it-how-two-competing-ais-cut-my-saas-costs-by-30x-2hn3</link>
      <guid>https://dev.to/tomokiikeda/claude-writes-the-code-gemini-runs-it-how-two-competing-ais-cut-my-saas-costs-by-30x-2hn3</guid>
      <description>&lt;h1&gt;
  
  
  Claude Writes the Code, Gemini Runs It: How Two Competing AIs Cut My SaaS Costs by 30x
&lt;/h1&gt;

&lt;p&gt;I build &lt;a href="https://nokos.ai" rel="noopener noreferrer"&gt;Nokos&lt;/a&gt;, an AI-powered note-taking app that auto-captures conversations from 25+ AI tools. Here's the thing — the product itself runs entirely on AI, and picking the &lt;strong&gt;wrong model for the wrong job&lt;/strong&gt; almost killed the economics.&lt;/p&gt;

&lt;p&gt;This is the story of how I went from "this will never be profitable" to "break-even at 150 users" by splitting my AI stack between two competing providers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Original Architecture (And Why It Was Bleeding Money)
&lt;/h2&gt;

&lt;p&gt;When I first built Nokos, I used Anthropic's Claude for everything:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Cost per call&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Metadata generation&lt;/td&gt;
&lt;td&gt;Claude Haiku&lt;/td&gt;
&lt;td&gt;~¥0.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI Chat&lt;/td&gt;
&lt;td&gt;Claude Sonnet&lt;/td&gt;
&lt;td&gt;~¥4.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Personal AI (RAG)&lt;/td&gt;
&lt;td&gt;Claude Sonnet&lt;/td&gt;
&lt;td&gt;~¥4.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Daily Diary generation&lt;/td&gt;
&lt;td&gt;Claude Haiku&lt;/td&gt;
&lt;td&gt;~¥1.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session summary&lt;/td&gt;
&lt;td&gt;Claude Haiku&lt;/td&gt;
&lt;td&gt;~¥1.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Natural Language Search&lt;/td&gt;
&lt;td&gt;Claude Haiku&lt;/td&gt;
&lt;td&gt;~¥0.5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The per-user costs added up fast. With the Plus plan priced at ¥480/month, I was &lt;strong&gt;losing money on every paying user&lt;/strong&gt;. The math was simple: this product could never work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Realization: Not Every AI Call Needs a Genius
&lt;/h2&gt;

&lt;p&gt;Here's what I noticed looking at my AI usage patterns:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Metadata generation&lt;/strong&gt; — Extract title, tags, category, sentiment from a memo. Claude Sonnet is wildly overqualified for this. It's pattern matching, not reasoning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chat responses&lt;/strong&gt; — Most user questions are "What did I write about X last week?" The answer is in the RAG context. The model just needs to synthesize it, not think deeply.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Diary generation&lt;/strong&gt; — Take today's memos, write a narrative. This is structured content generation with clear inputs and outputs.&lt;/p&gt;

&lt;p&gt;None of these need the most powerful model. They need a &lt;strong&gt;fast, cheap, good-enough model&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;But one thing absolutely &lt;em&gt;does&lt;/em&gt; need the best: &lt;strong&gt;writing the code itself&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Split: Claude for Code, Gemini for Production
&lt;/h2&gt;

&lt;p&gt;I migrated every production AI feature to Google's Gemini Flash in a single day. Here's the new architecture:&lt;/p&gt;

&lt;h3&gt;
  
  
  Claude Code (Opus) — The Architect
&lt;/h3&gt;

&lt;p&gt;Claude Code writes all the application code — the API routes, the React components, the database migrations, the infrastructure config. This is where reasoning quality matters most. One subtle bug in RLS policy logic or Stripe webhook handling could be catastrophic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost:&lt;/strong&gt; High per-session, but I'm the only "user." Fixed cost, not per-customer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gemini Flash — The Production Workhorse
&lt;/h3&gt;

&lt;p&gt;Every AI feature that runs for actual users:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Cost per call&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Metadata generation&lt;/td&gt;
&lt;td&gt;Gemini Flash&lt;/td&gt;
&lt;td&gt;~¥0.02&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI Chat&lt;/td&gt;
&lt;td&gt;Gemini Flash&lt;/td&gt;
&lt;td&gt;~¥0.07&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Personal AI (RAG)&lt;/td&gt;
&lt;td&gt;Gemini Flash&lt;/td&gt;
&lt;td&gt;~¥0.15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Daily Diary&lt;/td&gt;
&lt;td&gt;Gemini Flash&lt;/td&gt;
&lt;td&gt;~¥1.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session summary&lt;/td&gt;
&lt;td&gt;Gemini Flash&lt;/td&gt;
&lt;td&gt;~¥0.15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Natural Language Search&lt;/td&gt;
&lt;td&gt;Gemini Flash&lt;/td&gt;
&lt;td&gt;~¥0.05&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Embedding&lt;/td&gt;
&lt;td&gt;gemini-embedding-001&lt;/td&gt;
&lt;td&gt;~¥0.01&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The Result
&lt;/h3&gt;

&lt;p&gt;The chat/RAG queries — the most expensive calls — dropped from ~¥4.5 to ~¥0.07-0.15. That's where the &lt;strong&gt;"30x cheaper"&lt;/strong&gt; comes from.&lt;/p&gt;

&lt;p&gt;Across all plans, per-user costs dropped by &lt;strong&gt;3x to 7x&lt;/strong&gt;. The Free plan became sustainable. The paid plans became profitable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Cheap AI Unlocked
&lt;/h2&gt;

&lt;p&gt;The cost reduction didn't just improve margins — it changed what the product &lt;em&gt;could be&lt;/em&gt;:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Free users get real AI features
&lt;/h3&gt;

&lt;p&gt;When per-user costs were high, giving Free users AI chat was financial suicide. After the migration, I can afford 50 chat turns/month as a taste of the product. Small cost, huge conversion driver.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. "Kimagure" Diary for Free
&lt;/h3&gt;

&lt;p&gt;Free users get a "whimsical diary" — Nokos (the AI) writes one when it feels like it (triggered on login, a few times per month). At ~¥1/diary, this is viable as a free feature. Magical enough to drive upgrades.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Stamina-based pricing instead of feature gating
&lt;/h3&gt;

&lt;p&gt;Instead of locking features behind plans, every plan gets access to everything — just with different quotas. This only works when the per-call cost is low enough that occasional use doesn't destroy your margins.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Session ingestion at scale
&lt;/h3&gt;

&lt;p&gt;Coding sessions from Claude Code, Codex, Cursor, and others get summarized by Gemini Flash at ~¥0.15/session. At the old Claude Haiku cost (~¥1.0), high-volume session ingestion would have been economically impossible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Break-Even Math
&lt;/h2&gt;

&lt;p&gt;With the migration complete and pricing adjusted (Plus ¥980/month, Pro ¥2,980/month), the break-even point landed at roughly &lt;strong&gt;150 users&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The assumption: the vast majority of users (~90%+) will be on the Free plan — that's standard for freemium SaaS. The cost reduction made this survivable. At the old per-user costs, I would have needed 700+ users just to break even.&lt;/p&gt;

&lt;p&gt;The biggest cost driver to watch? &lt;strong&gt;Session ingestion.&lt;/strong&gt; Claude Code fires "Compacting conversation" 3-5 times per coding session, each counting as a separate ingest. Heavy users can rack up hundreds of sessions per month. This is the line item I monitor most closely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quality: Did It Actually Get Worse?
&lt;/h2&gt;

&lt;p&gt;Honestly? For these use cases, I can't tell the difference.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Metadata extraction&lt;/strong&gt; — Gemini Flash correctly identifies category, sentiment, tags, people, locations from memo text. The structured JSON output is reliable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chat/RAG&lt;/strong&gt; — When the relevant context is already retrieved by vector search, the model just needs to synthesize an answer. Flash does this well.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diary generation&lt;/strong&gt; — The narrative quality is comparable. Users read their own memos reflected back as a story. The memos provide the substance; the model provides the structure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Where I &lt;em&gt;would&lt;/em&gt; notice a difference: complex multi-step reasoning, nuanced code generation, architectural decisions. That's why Claude Code still writes the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Uncomfortable Truth About Model Selection
&lt;/h2&gt;

&lt;p&gt;Most AI features in production apps are &lt;strong&gt;glorified text transformation&lt;/strong&gt;. Extract these fields. Summarize this text. Generate a response given this context.&lt;/p&gt;

&lt;p&gt;You don't need the most intelligent model for text transformation. You need:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reliable structured output (JSON mode)&lt;/li&gt;
&lt;li&gt;Good instruction following&lt;/li&gt;
&lt;li&gt;Low latency&lt;/li&gt;
&lt;li&gt;Low cost&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Gemini Flash delivers all four.&lt;/p&gt;

&lt;p&gt;The hard part — the part that actually requires intelligence — is designing the system, writing the prompts, building the data pipeline, and catching the edge cases. That's where Claude Code (Opus) earns its cost, once, during development.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five Lessons for Your AI Stack
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Audit your AI calls by complexity.&lt;/strong&gt; Most of them are simpler than you think.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model that builds your product and the model that runs it don't need to be the same.&lt;/strong&gt; Claude builds; Gemini runs. Each does what it's best at.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-call cost determines your product design.&lt;/strong&gt; At ¥4.5/query, you gate features. At ¥0.07/query, you give them away as samples.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session ingestion is a hidden cost bomb.&lt;/strong&gt; If your product processes AI coding sessions, one "session" can actually be 3-5 API calls due to conversation compacting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run the P&amp;amp;L before you pick a model.&lt;/strong&gt; I built a spreadsheet with per-feature costs × expected usage × plan distribution. The answer was obvious once I saw the numbers.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://nokos.ai" rel="noopener noreferrer"&gt;Nokos&lt;/a&gt; is live — free plan with AI chat, diary generation, and session capture from Claude Code, ChatGPT, Cursor, and more. The entire product is powered by the dual-AI architecture described above.&lt;/p&gt;

&lt;p&gt;What's your AI cost optimization story? I'd love to hear how others are handling the model selection tradeoff.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is article 2 in a series about building Nokos as a solo developer. &lt;a href="https://dev.to/tomokiikeda/zero-lines-of-code-how-claude-code-and-gemini-built-my-saas-35cn"&gt;Article 1: Zero Lines of Code&lt;/a&gt; covered how the product was built entirely by AI.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Follow me for more — I'm launching on &lt;a href="https://producthunt.com" rel="noopener noreferrer"&gt;Product Hunt&lt;/a&gt; on March 31st!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>saas</category>
      <category>webdev</category>
      <category>startup</category>
    </item>
    <item>
      <title>Zero Lines of Code: How Claude Code and Gemini Built My SaaS</title>
      <dc:creator>Tomoki Ikeda</dc:creator>
      <pubDate>Mon, 16 Mar 2026 09:02:00 +0000</pubDate>
      <link>https://dev.to/tomokiikeda/zero-lines-of-code-how-claude-code-and-gemini-built-my-saas-35cn</link>
      <guid>https://dev.to/tomokiikeda/zero-lines-of-code-how-claude-code-and-gemini-built-my-saas-35cn</guid>
      <description>&lt;p&gt;&lt;strong&gt;I didn't write a single line of code.&lt;/strong&gt; Not one.&lt;/p&gt;

&lt;p&gt;Claude Code (Anthropic) wrote every line — frontend, backend, database, infra, tests. Gemini (Google) powers all the AI features in production.&lt;/p&gt;

&lt;p&gt;Two competing AIs built one product. I just told them what to do.&lt;/p&gt;

&lt;p&gt;The product is &lt;a href="https://nokos.ai" rel="noopener noreferrer"&gt;Nokos&lt;/a&gt; — an AI note-taking app that auto-captures your conversations from Claude Code, ChatGPT, Cursor, Copilot, and 20+ other AI tools. It's live and it works. Free tier available.&lt;/p&gt;

&lt;p&gt;Here's exactly how it happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Role: The AI Orchestrator
&lt;/h2&gt;

&lt;p&gt;People ask: "If you didn't code, what did you do for 30 days?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Everything that isn't code:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Product vision &amp;amp; design docs&lt;/strong&gt;: I wrote the project plan and technical design with Claude.ai — describing the product concept, data model, and architecture in conversation. The AI drafted the documents; I made every decision&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architecture decisions&lt;/strong&gt;: Every technical choice — database schema, auth strategy, document format — was mine. I described them in plain language, and Claude Code implemented them&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI cost audit&lt;/strong&gt;: Manually reviewed every endpoint. Found 4 critical bugs Claude Code had written — including a storage limit that was tracked but &lt;strong&gt;never enforced&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing strategy&lt;/strong&gt;: Researched 10+ competitors, designed a soft-gate model where free users taste every premium feature&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Legal&lt;/strong&gt;: Terms of Service (16 articles, 10 languages), downgrade policy, inactive account policy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QA&lt;/strong&gt;: Ran every flow, filed bugs, described fixes for Claude Code to implement&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design review&lt;/strong&gt;: Had Gemini 2.5 Pro review screenshots and critique the UI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;This role isn't "non-technical." It's "technical without typing."&lt;/strong&gt; You need to understand databases, APIs, and auth flows to make good decisions. You just don't type the code.&lt;/p&gt;

&lt;p&gt;My workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Me (Product Manager)
  ↓ "Build a session ingest endpoint with rate limiting"
Claude Code (Developer)
  ↓ writes code, runs tests, commits
  ↓ calls Gemini API to test AI features
Gemini Flash (Production AI)
  ↓ generates metadata, writes diaries, powers RAG
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three roles. Two AIs from competing companies. One product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Two AIs?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Claude Code is the best AI developer I've found.&lt;/strong&gt; 1M token context window holds the entire project. It refactors across dozens of files in a single pass.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gemini Flash is the best production AI for the price.&lt;/strong&gt; ~30x cheaper than Claude Sonnet. It powers all of Nokos's features: auto-tagging, daily diaries, natural language search, and Personal AI (RAG).&lt;/p&gt;

&lt;p&gt;I didn't pick sides. I picked the best tool for each job.&lt;/p&gt;

&lt;p&gt;Here's the wild part: during development, &lt;strong&gt;Claude Code called the Gemini API directly&lt;/strong&gt; — testing prompts, evaluating outputs, iterating until the AI pipeline worked. An Anthropic AI invoking a Google AI, debugging its responses, and adjusting prompts to improve them.&lt;/p&gt;

&lt;p&gt;But they didn't just coexist silently. They &lt;em&gt;debated&lt;/em&gt;. When I asked Claude Code to consult Gemini on a design decision, Gemini would give its opinion. Claude would consider it, blend it with its own perspective, and then present me with a synthesized recommendation: "Here's what Gemini suggested, here's what I think, and here's my recommendation — what would you like to do?"&lt;/p&gt;

&lt;p&gt;Two AIs from competing companies, having a constructive discussion, with a human making the final call.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tech Stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Tech&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;Next.js 15&lt;/td&gt;
&lt;td&gt;App Router, RSC, single codebase for web + mobile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;Hono&lt;/td&gt;
&lt;td&gt;Lightweight, fast, perfect for Cloud Run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;PostgreSQL + pgvector + pg_bigm&lt;/td&gt;
&lt;td&gt;Vector search for RAG, bigram search for Japanese&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production AI&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Gemini Flash&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~30x cheaper than Claude. Fast metadata/diary/report generation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Embedding&lt;/td&gt;
&lt;td&gt;gemini-embedding-001&lt;/td&gt;
&lt;td&gt;768 dimensions, fire-and-forget on every save&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Development&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Claude Code (Opus)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1M context, wrote 100% of the codebase&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Design Docs&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Claude.ai (Sonnet)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Co-authored project plan and technical design&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Design Review&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Gemini 2.5 Pro&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Screenshot analysis, UI/UX feedback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;Firebase Auth&lt;/td&gt;
&lt;td&gt;Google + GitHub + Email&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Billing&lt;/td&gt;
&lt;td&gt;Stripe&lt;/td&gt;
&lt;td&gt;3 plans, multi-currency (JPY/USD)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infra&lt;/td&gt;
&lt;td&gt;GCP (Cloud Run, Cloud SQL, Cloud Storage)&lt;/td&gt;
&lt;td&gt;Managed, auto-scaling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i18n&lt;/td&gt;
&lt;td&gt;next-intl&lt;/td&gt;
&lt;td&gt;10 languages from day one&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Claude-Gemini Collaboration, In Practice
&lt;/h2&gt;

&lt;p&gt;Here's a real example. I asked Claude Code to build the AI metadata generation feature — when you save a memo, AI automatically generates a title, tags, category, sentiment, and importance.&lt;/p&gt;

&lt;p&gt;Claude Code:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Wrote the Gemini API client&lt;/li&gt;
&lt;li&gt;Designed the prompt (in both Japanese and English)&lt;/li&gt;
&lt;li&gt;Called Gemini Flash to test the prompt with sample memos&lt;/li&gt;
&lt;li&gt;Evaluated the JSON output quality&lt;/li&gt;
&lt;li&gt;Adjusted the prompt based on Gemini's responses&lt;/li&gt;
&lt;li&gt;Built the API endpoint with proper error handling&lt;/li&gt;
&lt;li&gt;Added fire-and-forget embedding generation&lt;/li&gt;
&lt;li&gt;Wrote tests&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;An Anthropic AI writing code that calls a Google AI, testing the Google AI's outputs, and iterating on prompts to improve them. This happened dozens of times throughout development.&lt;/p&gt;

&lt;p&gt;When it came to pricing strategy, I had Claude Code call Gemini to analyze competitor pricing and evaluate our positioning. Gemini came back with sharp criticism of our approach. Claude didn't just pass it along — it incorporated Gemini's feedback, added its own analysis, and proposed a revised strategy. Then it asked me: "What do you think?" I made the call, and Claude implemented the changes across the codebase and documentation in one session.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Went Wrong
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Claude Code doesn't understand your business
&lt;/h3&gt;

&lt;p&gt;It writes code. It doesn't understand &lt;em&gt;why&lt;/em&gt;. I had to constantly prevent it from adding features I didn't need or over-engineering simple functions.&lt;/p&gt;

&lt;p&gt;My fix: &lt;strong&gt;CLAUDE.md&lt;/strong&gt; — a 300+ line file at the repo root describing every architecture decision, convention, and constraint. Claude Code reads it at the start of every session. It's the most important file in the repo.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. AI-generated billing code is dangerous
&lt;/h3&gt;

&lt;p&gt;In a single audit session, I found 4 critical issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A storage limit constant that was defined but &lt;strong&gt;never enforced&lt;/strong&gt; in the upload handler&lt;/li&gt;
&lt;li&gt;A backward-compatible endpoint that &lt;strong&gt;bypassed all rate limits&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;A memo counter that &lt;strong&gt;never decremented&lt;/strong&gt; on delete (free users could get permanently locked out)&lt;/li&gt;
&lt;li&gt;A monthly reset that only triggered from one code path (users hitting another path first would be blocked with stale counters)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Claude Code wrote all of this. Each piece worked in isolation. None worked together correctly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson: Always manually audit security and billing logic. AI doesn't think about exploit paths.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. "It works locally" doesn't mean it deploys
&lt;/h3&gt;

&lt;p&gt;Claude Code can't test against real infrastructure. I lost days to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker build failing because pnpm strict isolation needed &lt;code&gt;node-linker=hoisted&lt;/code&gt; (one line)&lt;/li&gt;
&lt;li&gt;Cloud SQL Proxy TLS failing because the slim Docker image was missing &lt;code&gt;ca-certificates&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Prisma 7 breaking the &lt;code&gt;url = env()&lt;/code&gt; syntax that Prisma 6 required&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each fix was trivial once found. Finding them was the hard part.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Numbers
&lt;/h2&gt;

&lt;p&gt;After 30 days of solo development with Claude Code + Gemini:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;24 database tables&lt;/strong&gt; with Row-Level Security on every single one&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;19 API routes&lt;/strong&gt;, ~60 endpoints&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;15 AI tool integrations&lt;/strong&gt; (Claude Code, Codex, Cursor, Copilot Chat, Aider, and more)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;10 languages&lt;/strong&gt; (Japanese, English, Chinese, Korean, Hindi, Spanish, Portuguese, German, Turkish, French)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;473 tests&lt;/strong&gt; passing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3 deployed services&lt;/strong&gt; on Cloud Run&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;27 Playwright E2E tests&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~$70/month&lt;/strong&gt; infrastructure cost&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;0 lines of code written by a human&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;1 founder&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The PM role becomes more important, not less.&lt;/strong&gt; When AI writes all the code, the bottleneck shifts to decision-making. &lt;em&gt;What&lt;/em&gt; to build, &lt;em&gt;why&lt;/em&gt;, and &lt;em&gt;in what order&lt;/em&gt; — these questions don't go away. They become everything.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use competing AIs — and let them debate.&lt;/strong&gt; Claude Code is great at building. Gemini is great at evaluating. When they disagree, you get a richer perspective. The human's job is to be the tiebreaker.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Design docs matter more than ever.&lt;/strong&gt; I co-authored the project plan and technical design with Claude.ai before writing any code. These documents became the shared context that kept Claude Code on track. Without them, AI writes what &lt;em&gt;it&lt;/em&gt; thinks you want. With them, AI writes what you &lt;em&gt;actually&lt;/em&gt; want.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Audit everything that touches money or security.&lt;/strong&gt; AI generates plausible-looking code that can have subtle, critical bugs. Trust but verify.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ship before you're ready.&lt;/strong&gt; I spent too long on world-building and 10-language support when I should have been getting user feedback.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Try Nokos
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://nokos.ai" rel="noopener noreferrer"&gt;&lt;strong&gt;nokos.ai&lt;/strong&gt;&lt;/a&gt; — unlimited memos, AI chat, and auto-capture from 20+ AI tools. Free to start.&lt;/p&gt;

&lt;p&gt;If you use Claude Code, Cursor, or ChatGPT daily, try connecting them to Nokos. Your AI conversations are full of knowledge that vanishes after every session. Nokos catches it all.&lt;/p&gt;

&lt;p&gt;We're launching on Product Hunt soon — follow &lt;a href="https://x.com/tomoking1122" rel="noopener noreferrer"&gt;@tomoking1122&lt;/a&gt; to catch it.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;I want to hear from you:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Have you used AI to build an entire product? What was your experience?&lt;/li&gt;
&lt;li&gt;Would you trust an AI-built codebase in production?&lt;/li&gt;
&lt;li&gt;Is "Product Manager + AI" the future of solo SaaS?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Drop your thoughts in the comments. I read and reply to every one.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Building in public. Follow the journey on &lt;a href="https://x.com/tomoking1122" rel="noopener noreferrer"&gt;Twitter/X&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>buildinpublic</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
