<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Full-Stack Developer is learning AI]]></title><description><![CDATA[Full-Stack Developer is learning AI]]></description><link>https://joung.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Fri, 16 Oct 2026 09:46:28 GMT</lastBuildDate><atom:link href="https://joung.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Find Similar Sentences Using Embeddings and a Vector Database]]></title><description><![CDATA[Why I Started Exploring Semantic Search
I'm building Second Memory, a personal memory application that allows users to save their memories and retrieve them later through natural-language questions.
O]]></description><link>https://joung.hashnode.dev/find-similar-sentences-using-embeddings-and-a-vector-database</link><guid isPermaLink="true">https://joung.hashnode.dev/find-similar-sentences-using-embeddings-and-a-vector-database</guid><dc:creator><![CDATA[Joung Park]]></dc:creator><pubDate>Thu, 15 Oct 2026 12:02:32 GMT</pubDate><content:encoded><![CDATA[<h2>Why I Started Exploring Semantic Search</h2>
<p>I'm building Second Memory, a personal memory application that allows users to save their memories and retrieve them later through natural-language questions.</p>
<p>One challenge is that users don't necessarily remember the exact words they used when saving a memory. They might remember its context or meaning instead.</p>
<p>For example, a user might have saved these memories:</p>
<ul>
<li><p>"I forgot my umbrella and got soaked in the rain."</p>
</li>
<li><p>"I lifted weights at the gym after work."</p>
</li>
<li><p>"I caught up with John over coffee yesterday."</p>
</li>
<li><p>"I stayed up late debugging my application."</p>
</li>
</ul>
<p>Later, the user asks:</p>
<p><strong>"Who did I meet recently?"</strong></p>
<p>The most relevant memory would be:</p>
<p>"I caught up with John over coffee yesterday."</p>
<p>A traditional keyword search might miss this because "meet" and "caught up" are different expressions.</p>
<p>This is where <strong>semantic search</strong> comes in. Instead of relying on exact keyword matches, we can search for text with similar meanings using embeddings.</p>
<p>In this post, I'll explore how to generate embeddings, calculate cosine distance, find relevant sentences, and perform vector searches using PostgreSQL with pgvector.</p>
<p>The examples use a locally runnable embedding model, so no API key or paid embedding service is required.</p>
<h2>Generate Embeddings</h2>
<p>An embedding is a numerical representation of text. An embedding model converts text into a vector, allowing us to compare sentences mathematically.</p>
<p>For this demonstration, I'll use <code>sentence-transformers/all-MiniLM-L6-v2</code>, a small, open-source embedding model that runs locally and produces 384-dimensional vectors.</p>
<pre><code class="language-python">from sentence_transformers import SentenceTransformer

model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")

sentences = [
    "I forgot my umbrella and got soaked in the rain.",
    "I lifted weights at the gym after work.",
    "I caught up with John over coffee yesterday.",
    "I stayed up late debugging my application.",
]

embeddings = model.encode(sentences)

print(embeddings.shape)
# (4, 384)

print(embeddings)
# [[ 0.04035296  0.10288464  0.09306896 ... -0.00502765 -0.13130508  0.05253862]
#  [-0.07009698  0.04319851  0.00160278 ... -0.06155823 -0.09415536 -0.01941661]
#  [-0.04802262 -0.01395144  0.12706237 ... -0.0639021  -0.03755342 -0.06052431]
#  [ 0.00379441  0.01192298 -0.02876158 ... -0.02021407 -0.0892337   0.05982817]]
</code></pre>
<p>The model converts each sentence into a vector containing 384 floating-point values.</p>
<p>The shape <code>(4, 384)</code> represents four sentences, each with a 384-dimensional embedding.</p>
<p>Each number is a coordinate in a high-dimensional vector space. Individual dimensions don't necessarily correspond to specific concepts such as people, places, or activities. Instead, semantic information is distributed across the vector.</p>
<p>The output is abbreviated with <code>...</code>, but each embedding contains all 384 values.</p>
<p>Looking at these numbers alone doesn't tell us which sentences are semantically related. To find that out, we need to compare their vectors.</p>
<h3>Using an Embedding API</h3>
<p>Embeddings can also be generated through an API rather than running a model locally.</p>
<p>For example, OpenAI provides an embeddings endpoint:</p>
<pre><code class="language-bash">curl https://api.openai.com/v1/embeddings \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "text-embedding-3-small",
    "input": "I caught up with John over coffee yesterday."
  }'
</code></pre>
<p>The response contains an embedding vector, just like the local model.</p>
<p>The concept is the same, although different models produce different vector dimensions and values. Embeddings generated by different models generally shouldn't be mixed in the same similarity search.</p>
<p>For the rest of this post, I'll continue using the local model so the examples remain reproducible without an API key.</p>
<h2>Calculate Cosine Distance</h2>
<p>Now that we have embeddings, we need a way to measure how closely two sentences are related.</p>
<p>One commonly used metric is <strong>cosine distance</strong>, which compares the directions of two vectors rather than their magnitudes.</p>
<p>Let's calculate the distance between two sentences.</p>
<pre><code class="language-python">import numpy as np
from sentence_transformers import SentenceTransformer

model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")

embeddings = model.encode([
    "I forgot my umbrella and got soaked in the rain.",
    "Who did I meet recently?"
], normalize_embeddings=True)

# Cosine distance using normalized vectors
distance = 1 - np.dot(embeddings[0], embeddings[1])

print(f"Cosine distance: {distance:.4f}")
# Cosine distance: 0.7981
</code></pre>
<p>The <code>normalize_embeddings=True</code> option scales each vector to unit length.</p>
<p>For normalized vectors, the dot product (<code>np.dot()</code>) is equivalent to cosine similarity. Subtracting it from 1 gives us cosine distance:</p>
<p>$$\text{cosine distance} = 1 - \text{cosine similarity}$$</p>
<p>Sentence Transformers also provides <code>model.similarity()</code>, which calculates cosine similarity by default. The equivalent calculation would be:</p>
<pre><code class="language-python">distance = 1 - model.similarity(
    embeddings[0:1], embeddings[1:2]
).item()
</code></pre>
<p>However, using NumPy keeps the distance calculation independent of Sentence Transformers. The same approach works with embeddings returned by OpenAI or other embedding APIs.</p>
<p><strong>A smaller cosine distance indicates greater similarity.</strong> A distance of 0 means the vectors point in the same direction.</p>
<h3>Why Cosine Distance?</h3>
<p>There are several ways to compare vectors:</p>
<table>
<thead>
<tr>
<th>Metric</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td>Cosine distance</td>
<td>Compares vector directions, ignoring magnitude</td>
</tr>
<tr>
<td>Euclidean distance</td>
<td>Measures straight-line distance between vectors</td>
</tr>
<tr>
<td>Inner product</td>
<td>Measures vector alignment and is affected by magnitude</td>
</tr>
</tbody></table>
<p>For text embeddings, we're generally interested in semantic relationships rather than differences in vector magnitude, making cosine distance a practical default.</p>
<p>When embeddings are L2-normalized, cosine distance, Euclidean distance, and inner product produce equivalent rankings. The choice of metric matters more when embeddings aren't normalized or when the model recommends a particular metric.</p>
<p>We'll use cosine distance throughout this post because pgvector supports it directly through the <code>&lt;=&gt;</code> operator.</p>
<h2>Find the Most Relevant Memory</h2>
<p>Now let's apply cosine distance to our original example and find the memory most relevant to the question:</p>
<p><strong>"Who did I meet recently?"</strong></p>
<pre><code class="language-python">import numpy as np
from sentence_transformers import SentenceTransformer

model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")

texts = [
    "I forgot my umbrella and got soaked in the rain.",
    "I lifted weights at the gym after work.",
    "I caught up with John over coffee yesterday.",
    "I stayed up late debugging my application.",
]

query = "Who did I meet recently?"

memory_embeddings = model.encode(texts, normalize_embeddings=True)
query_embedding = model.encode(query, normalize_embeddings=True)

# Cosine distance using normalized vectors
distances = 1 - np.dot(memory_embeddings, query_embedding)

for i in np.argsort(distances):
    print(f"{distances[i]:.4f} - {texts[i]}")
# 0.6097 - I caught up with John over coffee yesterday.
# 0.7981 - I forgot my umbrella and got soaked in the rain.
# 0.8151 - I lifted weights at the gym after work.
# 0.8454 - I stayed up late debugging my application.
</code></pre>
<p>This time, we're comparing a single query against multiple candidate memories.</p>
<p>The results are sorted in ascending order because <strong>smaller cosine distance indicates greater relevance</strong>.</p>
<p>As expected, the memory about meeting John ranks first, even though the question uses "meet" while the memory uses "caught up".</p>
<p>This demonstrates semantic retrieval: finding related meanings rather than matching exact words.</p>
<p>However, the closest result isn't guaranteed to be relevant in every case. The ranking depends on the embedding model and the available memories.</p>
<p>In a production system, we'd need to evaluate retrieval quality using representative queries and data.</p>
<h2>Introducing pgvector</h2>
<p>So far, we've generated embeddings and calculated distances in Python. But generating embeddings for every stored memory on each query would be inefficient.</p>
<p>A better approach is to generate embeddings when memories are created or updated, persist them, and search against the stored vectors.</p>
<p>For this, I'll use <strong>pgvector</strong>, a PostgreSQL extension that adds vector storage and similarity search capabilities.</p>
<p>Before using it with text embeddings, let's look at a simple example with two-dimensional vectors.</p>
<h3>Store Vectors</h3>
<p>First, enable the extension and create a table:</p>
<pre><code class="language-sql">CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE examples (
    id SERIAL PRIMARY KEY,
    embedding VECTOR(2)
);
</code></pre>
<p>Insert three vectors:</p>
<pre><code class="language-sql">INSERT INTO examples (embedding)
VALUES
    ('[1,0]'),
    ('[0.8,0.6]'),
    ('[0,1]');
</code></pre>
<p>Using two-dimensional vectors makes it easier to see how pgvector calculates distances.</p>
<h3>Search by Cosine Distance</h3>
<p>Let's find the vectors closest to <code>[1,0]</code>.</p>
<pre><code class="language-sql">SELECT
    embedding,
    embedding &lt;=&gt; '[1,0]' AS distance
FROM examples
ORDER BY distance;
</code></pre>
<p>The <code>&lt;=&gt;</code> operator calculates cosine distance.</p>
<p>The results are:</p>
<pre><code class="language-text"> embedding | distance
-----------+----------
 [1,0]     | 0
 [0.8,0.6] | 0.2
 [0,1]     | 1
</code></pre>
<p>The first vector has a distance of 0 because it points in the same direction as the query.</p>
<p>The second vector has a distance of 0.2, while the third is orthogonal to the query and has a distance of 1.</p>
<p>The important point is that <strong>pgvector doesn't understand the meaning of text</strong>. It stores vectors and performs mathematical operations on them.</p>
<p>The embedding model is responsible for producing vectors that represent semantic relationships.</p>
<h3>Vector Indexes</h3>
<p>Without an index, PostgreSQL performs an exact nearest-neighbor search.</p>
<p>For larger datasets, pgvector supports approximate nearest-neighbor indexes such as HNSW and IVFFlat.</p>
<p>For example, we can create an HNSW index:</p>
<pre><code class="language-sql">CREATE INDEX examples_embedding_hnsw_idx
ON examples
USING hnsw (embedding vector_cosine_ops);
</code></pre>
<p>The search query itself doesn't need to change.</p>
<p>PostgreSQL can use the index to accelerate searches, although the query planner may choose a sequential scan for small datasets.</p>
<p>Approximate indexes can improve search performance by trading off some retrieval accuracy.</p>
<h3>Embedding Dimensions and Index Limitations</h3>
<p>One practical consideration when using pgvector is the number of dimensions in an embedding.</p>
<p>pgvector supports storing vectors with up to 16,000 dimensions, but standard HNSW and IVFFlat indexes using the <code>vector</code> type support up to 2,000 dimensions.</p>
<p>This becomes relevant when using models such as OpenAI's <code>text-embedding-3-large</code>, which produces 3,072-dimensional embeddings by default.</p>
<p>One option is to use a shorter embedding supported by the model.</p>
<p>OpenAI's <code>text-embedding-3</code> models use <strong>Matryoshka Representation Learning (MRL)</strong>, which allows shorter embeddings to retain useful semantic information.</p>
<p>For example, we can request a 1,536-dimensional embedding from <code>text-embedding-3-large</code>:</p>
<pre><code class="language-bash">curl https://api.openai.com/v1/embeddings \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "text-embedding-3-large",
    "input": "I caught up with John over coffee yesterday.",
    "dimensions": 1536
  }'
</code></pre>
<p>This reduces storage requirements and allows the embedding to fit within pgvector's standard vector index dimension limit.</p>
<p>Another option is pgvector's <code>halfvec</code> type, which supports indexing up to 4,000 dimensions using half-precision values.</p>
<p>The right approach depends on retrieval quality, storage, precision, and performance requirements.</p>
<p>For our example using 384-dimensional embeddings, the standard <code>vector</code> type is sufficient.</p>
<h2>Store and Search Embeddings with pgvector</h2>
<p>Now that we've covered embedding generation, cosine distance, and pgvector, let's bring everything together.</p>
<p>We'll store the four memories with their embeddings in PostgreSQL and retrieve them using cosine distance.</p>
<pre><code class="language-python">import psycopg
from pgvector.psycopg import register_vector
from sentence_transformers import SentenceTransformer

DB_URL = "postgresql://user:password@localhost:5432/mydatabase"

model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")

texts = [
    "I forgot my umbrella and got soaked in the rain.",
    "I lifted weights at the gym after work.",
    "I caught up with John over coffee yesterday.",
    "I stayed up late debugging my application.",
]

query = "Who did I meet recently?"

DIMENSIONS = 384

with psycopg.connect(DB_URL) as conn:
    conn.execute("CREATE EXTENSION IF NOT EXISTS vector")
    register_vector(conn)

    conn.execute(f"""
        CREATE TABLE IF NOT EXISTS journal (
            id BIGSERIAL PRIMARY KEY,
            content TEXT NOT NULL,
            embedding VECTOR({DIMENSIONS}) NOT NULL
        )
    """)

    embeddings = model.encode(texts)

    # Reset the demo data
    conn.execute("TRUNCATE TABLE journal RESTART IDENTITY")

    for content, embedding in zip(texts, embeddings):
        conn.execute(
            """
            INSERT INTO journal (content, embedding)
            VALUES (%s, %s)
            """,
            (content, embedding)
        )

    conn.commit()

    query_embedding = model.encode(query)

    results = conn.execute(
        """
        SELECT
            content,
            embedding &lt;=&gt; %s AS distance
        FROM journal
        ORDER BY embedding &lt;=&gt; %s ASC
        LIMIT 4
        """,
        (query_embedding, query_embedding)
    ).fetchall()

    print(f"Query: {query}")

    for content, distance in results:
        print(f"{distance:.4f} - {content}")
# Query: Who did I meet recently?
# 0.6097 - I caught up with John over coffee yesterday.
# 0.7981 - I forgot my umbrella and got soaked in the rain.
# 0.8151 - I lifted weights at the gym after work.
# 0.8454 - I stayed up late debugging my application.
</code></pre>
<p>There are two key differences from the in-memory implementation.</p>
<p><strong>1. Embeddings are persisted</strong></p>
<p>pgvector provides the <code>VECTOR(384)</code> data type, allowing embeddings to be stored alongside their original text.</p>
<p><code>register_vector(conn)</code> integrates pgvector with psycopg so NumPy arrays can be passed directly as parameters.</p>
<p>In a real application, embeddings would be generated when memories are created or updated, rather than on every query.</p>
<p><strong>2. Similarity search moves into PostgreSQL</strong></p>
<p>Instead of loading every embedding into Python to calculate distances, we let PostgreSQL perform the search.</p>
<p>The <code>&lt;=&gt;</code> operator calculates cosine distance, and the query returns memories ordered by their distance from the query embedding.</p>
<p>As shown in the output, the ranking matches our earlier Python example, with the memory about meeting John appearing first.</p>
<p>For larger datasets, vector indexes can improve search performance without changing the core retrieval logic.</p>
<h2>Conclusion</h2>
<p>We've explored how embeddings represent sentence meanings numerically, how cosine distance helps identify related text, and how to retrieve relevant sentences even when their wording differs.</p>
<p>We started with a locally runnable embedding model, calculated distances in Python, and then introduced pgvector using simple two-dimensional vectors.</p>
<p>Finally, we combined everything into a PostgreSQL-backed semantic search implementation.</p>
<p>The same retrieval approach can be used in a Retrieval-Augmented Generation (RAG) system, where retrieved text is provided to an LLM as context for generating an answer.</p>
<p>There are further considerations for production systems, including vector indexing, hybrid search, relevance thresholds, and retrieval evaluation.</p>
<p>But the core mechanism is straightforward: <strong>generate embeddings, calculate distances, and retrieve the closest matches.</strong></p>
]]></content:encoded></item><item><title><![CDATA[Why I Chose Third-Party (Firebase) Authentication]]></title><description><![CDATA[Authentication is essential to Second-Memory, but it isn't its core business.
The goal of Second-Memory is to help users capture their thoughts and retrieve meaningful information from them. Building ]]></description><link>https://joung.hashnode.dev/why-i-chose-third-party-firebase-authentication</link><guid isPermaLink="true">https://joung.hashnode.dev/why-i-chose-third-party-firebase-authentication</guid><dc:creator><![CDATA[Joung Park]]></dc:creator><pubDate>Wed, 05 Aug 2026 00:30:00 GMT</pubDate><content:encoded><![CDATA[<p>Authentication is essential to Second-Memory, but it isn't its core business.</p>
<p>The goal of Second-Memory is to help users capture their thoughts and retrieve meaningful information from them. Building an authentication system doesn't directly advance that goal.</p>
<p>There's also a user-experience consideration. People already manage many online accounts. Asking them to create and remember another password adds friction, while social login lets them sign in with an account they already have.</p>
<h2>Build vs. Buy</h2>
<p>I considered two approaches: building authentication in-house or using an existing service.</p>
<table>
<thead>
<tr>
<th>Consideration</th>
<th>In-house authentication</th>
<th>Firebase Authentication</th>
</tr>
</thead>
<tbody><tr>
<td>Initial development</td>
<td>Build authentication flows and supporting infrastructure</td>
<td>Integrate an existing service</td>
</tr>
<tr>
<td>Security</td>
<td>Implement and maintain security controls</td>
<td>Firebase manages core authentication infrastructure; the application still has security responsibilities</td>
</tr>
<tr>
<td>Maintenance</td>
<td>Handle security updates and operational issues</td>
<td>Google maintains the service</td>
</tr>
<tr>
<td>User experience</td>
<td>Users may need another account and password</td>
<td>Users can sign in with a supported existing identity</td>
</tr>
<tr>
<td>Engineering cost</td>
<td>More development and ongoing maintenance</td>
<td>Less implementation and maintenance work</td>
</tr>
</tbody></table>
<p>For a project like Second-Memory, building authentication from scratch would mean taking on additional complexity without providing much differentiation.</p>
<h2>What Does In-House Authentication Involve?</h2>
<p>Building authentication is more than verifying a username and password. It means taking responsibility for several areas.</p>
<p><strong>1. Security</strong></p>
<ul>
<li>Store passwords using an appropriate password-hashing algorithm.</li>
<li>Protect against brute-force attacks and credential stuffing.</li>
<li>Implement secure session management, including expiration and revocation.</li>
<li>Protect account recovery and verification flows against account takeover.</li>
<li>Manage secrets, security logging, monitoring, and incident response.</li>
</ul>
<p><strong>2. Account lifecycle</strong></p>
<ul>
<li>Registration and email verification</li>
<li>Login and logout</li>
<li>Account recovery and password resets</li>
<li>Account deletion and related data cleanup</li>
<li>Session revocation and suspicious-login handling</li>
</ul>
<p><strong>3. Social login integration</strong></p>
<p>Supporting social login involves integrating with external identity providers and handling their authentication flows. Standards such as OpenID Connect (OIDC) and OAuth 2.0 are commonly involved, depending on the provider and flow.</p>
<p>The application must also handle identity mapping, account linking where appropriate, and the relationship between an external identity and its own user records.</p>
<p><strong>4. Ongoing operations</strong></p>
<p>Authentication requires continued attention after launch. Security vulnerabilities emerge, dependencies need updates, and authentication flows must be tested as the application evolves.</p>
<p>The effort doesn't end when login starts working.</p>
<h2>The Decision</h2>
<p>I don't have specific authentication requirements that justify building and maintaining my own system.</p>
<p>I'll use Firebase Authentication and make social login the preferred way to sign in. This avoids asking users to create another password and lets me rely on an established authentication service.</p>
<p>There may be usage costs and some provider dependency, but building authentication in-house also has costs in development time, maintenance, and security responsibilities.</p>
<p>For Second-Memory, Firebase Authentication is a practical choice because it meets my needs without requiring me to build and operate an authentication system myself.</p>
<p>However, using a third-party authentication service doesn't eliminate application security responsibilities. Second-Memory still needs to validate identities correctly, enforce authorization in the backend, and ensure users can access only their own data.</p>
<h2>How Authentication Works in Second-Memory</h2>
<p>Second-Memory has web and mobile clients, plus backend services. Both clients use Firebase Authentication, while the backend verifies each request and maps the Firebase identity to an internal user record.</p>
<h3>The authentication flow</h3>
<p><a href="https://mermaid.live/edit#pako:eNplkk1v4yAQhv_KiGvj1An5cDhUcm1F6qFS1Wq30soXYiYpqg3pGKSkUf97oY43uwonmHnmfWeAE6utQiZYhx8eTY2lljuSbWUgrL0kp2u9l8ZBAbKDV9zALTzajW4QikajcdfkOpJrTbiRHULu3VvAdC2dtuaaziN9L-t3NAryp4droozEC9bWqOQRW0tHKKWTUb06CxbJ3d1awIveGdDn2DoJwUJcOnkowdlgcynJRXQEirN3Dm7gHiUh_YvlPfYbSW-PfQJk6BQPjmTtwGt1AcvoFpK-CyKbI2zP1r8GqEx6uWd0nmKrDsnIpi-gMCKp_2zj5VnSnzj0OGT70frmu701HbIR25FWTDjyOGItUivjkZ1iTcXCK7RYMRG2hMofEiXpPaltY6lilfkK9eG2_1jbDhJk_e6Nia1sunDyeyXd8D3-Rim8GlJhvXFMrH4kmDixAxPTGR9ns8l0usjS5Woyn2cjdozhdMxns3S-nPCU88Vi-TVinz-u6XiRhjif89WS8ywLqW_bo9Py"><img src="https://mermaid.ink/img/pako:eNplUstu2zAQ_JUFr7Vc2fRD4SGAIsFADgGKBm2AQpe1tHaISKS7IgE7Rv69ZGTVLawTOTM7s7vUWdS2IaFET789mZpKjXvGrjIQvgOy07U-oHFQAPbwQlv4Ck92q1uCotVk3K1yE5UbzbTFniD37jXIdI1OW3OrzqP6Aes3Mg3k3x5vFWVUPFNtTZM8UWf5BCU6jO7VxbBI7u83Cp713oC-YJskgIW6dvJYgrMh5lqSq5gIHGfvHXyBB0Im_leWD7KfxHp3GgjA0CkdHWPtwOvmKixjWiB9H0y2J9hdon-MojIZ7L6T8xxbdcQG26GAw4jc_Bcbl2dZv9PY48gOow3N9wdr4ioiIyZiz7oRyrGnieiIO4xXcY58JcJbdFQJFY5MjT8mDfJbUtvWciUq8xHqw85_WduNFmz9_lWoHbZ9uPlDg278Sf6iHN6OuLDeOKFm8tNDqLM4CjVfyGm2mM3nqyxd382Wy2wiThFOp3KxSJfrmUylXK3WHxPx_hmbTldpwOVS3q2lzLJA_QE9ONVn?type=png" alt="" /></a></p>
<p>The flow has six steps:</p>
<ol>
<li><strong>Sign in:</strong> The client signs in with Firebase Authentication and obtains a Firebase ID token.</li>
<li><strong>Call the API:</strong> The client sends the token in the <code>Authorization: Bearer &lt;token&gt;</code> header.</li>
<li><strong>Verify the token:</strong> The backend verifies the token using the Firebase Admin SDK. It must not trust a token simply because it can decode it.</li>
<li><strong>Extract the Firebase UID:</strong> After verification, the backend reads the user's Firebase UID from the token.</li>
<li><strong>Resolve the internal user:</strong> The backend looks up the user record using <code>firebaseUid</code>.</li>
<li><strong>Authorize the request:</strong> The backend uses the internal user ID to enforce access controls and scope data operations to that user.</li>
</ol>
<h3>Why maintain our own user table?</h3>
<p>Firebase provides authentication, but Second-Memory still needs its own application-level user record.</p>
<p>For example:</p>
<table>
<thead>
<tr>
<th>id</th>
<th>firebaseUid</th>
</tr>
</thead>
<tbody><tr>
<td><code>usr_123</code></td>
<td><code>firebase-uid-abc</code></td>
</tr>
<tr>
<td><code>usr_456</code></td>
<td><code>firebase-uid-xyz</code></td>
</tr>
</tbody></table>
<p>The <code>id</code> is Second-Memory's internal user ID, while <code>firebaseUid</code> links the record to Firebase Authentication. The <code>firebaseUid</code> column should have a unique constraint.</p>
<p>This separation has a few advantages:</p>
<ul>
<li><strong>Domain independence:</strong> Memories and other application entities can reference Second-Memory's internal user ID.</li>
<li><strong>Consistent authorization:</strong> Backend services can use the same internal identity when checking data access.</li>
<li><strong>Flexibility:</strong> If the authentication provider changes in the future, the application's domain model doesn't need to use provider-specific identifiers everywhere.</li>
</ul>
<p>The backend must still enforce authorization on every relevant request. A valid Firebase token proves the caller's authenticated identity; it does not automatically grant access to a particular memory or resource. The backend should derive identity from the verified token, not trust a user ID supplied by the client.</p>
<h3>How this fits the client and backend stack</h3>
<p>The same authentication principle applies across the different parts of Second-Memory, although their implementations differ.</p>
<ul>
<li><strong>Next.js web app:</strong> Configure Firebase for the web client, implement sign-in, and attach the ID token to API requests.</li>
<li><strong>Expo mobile app:</strong> Configure Firebase Authentication for React Native, handle session persistence, and attach the ID token to API requests.</li>
<li><strong>Backend services:</strong> Use the Firebase Admin SDK to verify ID tokens, resolve the internal user, and enforce authorization.</li>
</ul>
<p>The important distinction is that the clients obtain tokens, but the backend is responsible for verifying them and deciding what the authenticated user is allowed to access.</p>
<h2>Conclusion</h2>
<p>For Second-Memory, authentication is necessary infrastructure, not a feature I need to reinvent.</p>
<p>Firebase Authentication reduces the amount of authentication code I need to build and maintain. Keeping an internal user record also lets Second-Memory separate external identity verification from application-level identity and authorization.</p>
<p>The goal isn't to eliminate security responsibilities. It's to use an established service for authentication while keeping control of the application's own data and access rules.</p>
]]></content:encoded></item><item><title><![CDATA[From Monorepo to Turborepo: Managing Multiple Projects]]></title><description><![CDATA[Why a Monorepo?

A monorepo is a single repository containing multiple distinct projects, with well-defined relationships. — Monorepo Explained

A monorepo makes sense when multiple projects have mean]]></description><link>https://joung.hashnode.dev/from-monorepo-to-turborepo-managing-multiple-projects</link><guid isPermaLink="true">https://joung.hashnode.dev/from-monorepo-to-turborepo-managing-multiple-projects</guid><dc:creator><![CDATA[Joung Park]]></dc:creator><pubDate>Mon, 03 Aug 2026 23:00:00 GMT</pubDate><content:encoded><![CDATA[<h2>Why a Monorepo?</h2>
<blockquote>
<p>A monorepo is a single repository containing multiple distinct projects, with well-defined relationships. — <a href="https://monorepo.tools">Monorepo Explained</a></p>
</blockquote>
<p>A monorepo makes sense when multiple projects have meaningful relationships:</p>
<ul>
<li><p><strong>Shared libraries and utilities</strong> — reuse common code across projects.</p>
</li>
<li><p><strong>Shared contracts</strong> — keep API types consistent between producers and consumers.</p>
</li>
<li><p><strong>Cross-project changes</strong> — update related projects in a single change.</p>
</li>
<li><p><strong>Shared tooling and rules</strong> — maintain consistent development practices.</p>
</li>
</ul>
<p>If projects have no meaningful relationships, separate repositories may be simpler.</p>
<p>For Second-Memory, Web, Mobile, and Backend need to agree on API contracts. Sharing TypeScript API types helps keep those contracts consistent.</p>
<p>Web uses React/Next.js, while Mobile uses React Native/Expo, so their UI implementations remain separate. Code is shared only where it genuinely works across both platforms.</p>
<p>Keeping related projects together makes coordinating changes and managing shared contracts easier.</p>
<p>How do we manage their packages and dependencies?</p>
<h2>Why Workspaces?</h2>
<blockquote>
<p>Workspaces are a set of features in the npm CLI that provide support for managing multiple packages from a local file system within a single top-level root package. — <a href="https://docs.npmjs.com/cli/v8/using-npm/workspaces">npm Workspaces</a></p>
</blockquote>
<p>A monorepo can work without workspaces. Each project can have its own <code>package.json</code> and manage dependencies independently.</p>
<p>For example:</p>
<pre><code class="language-text">second-memory/
├── apps/
│   ├── web/
│   └── mobile/
├── services/
│   ├── memory-service/
│   ├── ask-service/
│   └── embedding-worker/
└── packages/
    ├── api-types/
    └── ui-component/
</code></pre>
<p>Suppose both Web and Memory Service depend on <code>api-types</code>.</p>
<p>Without workspaces, each project needs to manage its dependencies and local references separately. A project might reference the shared package using a local file dependency:</p>
<pre><code class="language-json">{
  "dependencies": {
    "@second-memory/api-types": "file:../../packages/api-types"
  }
}
</code></pre>
<p>The relative path depends on the consuming project's location. Dependencies must be installed and managed in each project, and keeping local references and updates consistent requires additional care.</p>
<p>With workspaces, the root configuration defines which directories contain packages.</p>
<p>For example, with pnpm:</p>
<p><code>pnpm-workspace.yaml</code></p>
<pre><code class="language-yaml">packages:
  - "apps/*"
  - "services/*"
  - "packages/*"
</code></pre>
<p>Each package declares its own dependencies in its <code>package.json</code>. For example, Web can depend on the shared API types package:</p>
<p><code>apps/web/package.json</code></p>
<pre><code class="language-json">{
  "dependencies": {
    "@second-memory/api-types": "workspace:*"
  }
}
</code></pre>
<p>The shared package identifies itself in its own <code>package.json</code>:</p>
<p><code>packages/api-types/package.json</code></p>
<pre><code class="language-json">{
  "name": "@second-memory/api-types",
  "version": "1.0.0"
}
</code></pre>
<p>The <code>workspace:*</code> protocol explicitly tells pnpm to use the local workspace package.</p>
<p>Dependencies can then be installed from the repository root:</p>
<pre><code class="language-bash">pnpm install
</code></pre>
<p>pnpm recognises the workspace packages and links local dependencies, making it easier to manage relationships between projects.</p>
<h3>Without Workspaces vs With Workspaces</h3>
<table>
<thead>
<tr>
<th>Area</th>
<th>Without workspaces</th>
<th>With workspaces</th>
</tr>
</thead>
<tbody><tr>
<td>Install dependencies</td>
<td>Install separately in each project</td>
<td>Install from the root</td>
</tr>
<tr>
<td>Reference shared packages</td>
<td>Configure local file dependencies or links</td>
<td>Declare dependencies between workspace packages</td>
</tr>
<tr>
<td>Manage package relationships</td>
<td>Maintain references across projects</td>
<td>Define participating packages in the root configuration</td>
</tr>
<tr>
<td>Develop shared packages</td>
<td>Manage local references and updates carefully</td>
<td>Work with linked local packages</td>
</tr>
<tr>
<td>Run builds and tests</td>
<td>Run scripts manually or maintain root scripts</td>
<td>Still requires scripts or task orchestration</td>
</tr>
</tbody></table>
<p>A workspace reduces manual package and dependency management. It provides a way to define packages and their local dependencies within the monorepo.</p>
<p>However, managing packages is only part of the problem. As the number of projects and dependencies grows, coordinating builds, tests, and other tasks becomes another challenge.</p>
<h2>Why Task Orchestration?</h2>
<p>With only a few projects, running build and test commands manually may be enough. As the repository grows, two problems become more noticeable.</p>
<h3>Problem 1: Running Only the Necessary Tasks</h3>
<p>The dependencies in Second-Memory look like this:</p>
<p><a href="https://mermaid.live/edit#pako:eNpdjU1vgkAQhv8KmTNYcPmSQxNTe-ihJ21MCh5WdsSN7C4ZoGqN_70rlqZxTzvPM_O-FyiNQMhgV5tjuefUOatFoR375qucN9Lrzg22G8fznp21BU37dMTtZlwZ-PudK7OVNT6oZa5QGTp7LdKXLB_0fJnz9vDP3e3HW95LrzSqMRp199s-unvwuHsaxtd1jmqLQkhdeUdDB6QNuFCRFJB11KMLCknx2wiX22UB3R4VFpDZL6HoT57gdLCttaECCn219w3Xn8aoMYJMX-0h2_G6tVPfCN7hQvKKuPqjhFogvZhed5AFLBxCILvACbJpyCZpGEynceonsyCKUhfON-xPWBj6URIwn7E4Tq4ufA-9_iT2LWcRmyWMpalVPy9ZhHs"><img src="https://mermaid.ink/img/pako:eNpdjctuwjAQRX8lmjWmCc4LLyqh0kUXXUGF1ISFiYcQEdvRJClQxL_XQFNVeOU5Z-beMxRWIQjY1vZQ7CR13nKeG8-92TKTTcW6U4Pt2mPs2Vs50LRPB9ysh5Ubf79zbTdVjQ9qkWnUlk6sRfqqigc9W2Sy3f9zd_vxlvUVK6xurEHT_bYP7h487L6uMtQbVKoyJTtY2iOtYQQlVQpERz2OQCNpeR3hfL3JoduhxhyE-xKq_siUpL3rqy3lkJuLu2-k-bRWDxFk-3IHYivr1k19o2SH80qWJPUfJTQK6cX2pgMR8PQWAuIMRxCTkI_TMJhM4tRPpkEUOXu6Yn_Mw9CPkoD7nMdxchnB963XH8e-4zzi04TzNHXqB5MHgy8?type=png" alt="" style="display:block;margin:0 auto" /></a></p>
<p>Suppose I change <code>packages/ui-component</code>.</p>
<p>Web and Mobile depend on it, but Memory Service, Ask Service, and Embedding Worker do not.</p>
<p>Ideally, I want to run checks for the affected projects without wasting time checking unrelated services.</p>
<p>With workspaces alone, I can run individual commands using package filters:</p>
<pre><code class="language-bash">pnpm --filter web test
pnpm --filter mobile test
</code></pre>
<p>But I still need to know which projects depend on the changed package and decide which commands to run.</p>
<p>As the dependency graph grows, maintaining this selection manually becomes more difficult.</p>
<p>Task orchestration can automate the selection of relevant tasks based on package relationships, configured task dependencies, and changes.</p>
<h3>Problem 2: Managing Dependencies Between Tasks</h3>
<p>Some tasks need to run before others.</p>
<p>For example, if a service consumes a shared package's compiled output, that package needs to be built before the service.</p>
<p>I could maintain a script to enforce the order:</p>
<pre><code class="language-json">{
  "scripts": {
    "build:memory": "pnpm --filter api-types build &amp;&amp; pnpm --filter memory-service build"
  }
}
</code></pre>
<p>This works for a simple case, but the scripts become harder to maintain as more packages and task dependencies are added.</p>
<p>Ideally, I want to define task relationships once and let a tool determine the correct execution order.</p>
<h2>How Task Orchestration Helps</h2>
<p>Task orchestration adds a layer above workspace package management. It coordinates tasks such as builds, tests, and linting across packages.</p>
<h3>Task Graph</h3>
<p>A task graph describes dependencies between tasks and helps determine their execution order.</p>
<p>For example, if <code>api-types</code> must be built before its consumers:</p>
<p><a href="https://mermaid.live/edit#pako:eNpdjctuwjAQRX8lmjWmCc4LLyqh0kUXXUGF1ISFiYcQEdvRJClQxL_XQFNVeOU5Z-beMxRWIQjY1vZQ7CR13nKeG8-92TKTTcW6U4Pt2mPs2Vs50LRPB9ysh5Ubf79zbTdVjQ9qkWnUlk6sRfqqigc9W2Sy3f9zd_vxlvUVK6xurEHT_bYP7h487L6uMtQbVKoyJTtY2iOtYQQlVQpERz2OQCNpeR3hfL3JoduhxhyE-xKq_siUpL3rqy3lkJuLu2-k-bRWDxFk-3IHYivr1k19o2SH80qWJPUfJTQK6cX2pgMR8PQWAuIMRxCTkI_TMJhM4tRPpkEUOXu6Yn_Mw9CPkoD7nMdxchnB963XH8e-4zzi04TzNHXqB5MHgy8"><img src="https://mermaid.ink/img/pako:eNptjc1ug0AMhF8F-QwIWP6yh0pN6Bv01MJhwzqAwrLILE0oyrsXEKlUqT55vvGMZyi1ROBwafWtrAUZ6z3LO2uZ18_j2LTSEn3jmKnHobAc58U67lih0jQ5A9JXU2KxZ7aT0zM5XP_1s92_4fkPf3tW63PTYgE2VNRI4IZGtEEhKbFKmNdQDqZGhTnwZSWU492Rgq5OqVtNOeTdY8n3ovvQWj0rSI9VDfwi2mFRYy-FwawRFQn1Swk7iXTSY2eA-2GwlQCf4Q48CJmbhn4QxKmXHPwoSm2YVuy5LAy9KPGZx1gcJw8bvre_nht7C2cROySMpeli_QDCfHFI?type=png" alt="" style="display:block;margin:0 auto" /></a></p>
<p>The graph should reflect the actual build requirements. If consumers compile the shared package's source directly, a separate build step may not be necessary.</p>
<p>Turborepo uses the workspace package dependency graph together with configured task dependencies to determine execution order. The package relationships provide the foundation; task configuration defines how tasks depend on one another.</p>
<h3>Affected Tasks</h3>
<p>When a package changes, Turborepo can use Git change information and the dependency graph to select affected packages and their relevant tasks.</p>
<ul>
<li><p><strong>Change</strong> <code>api-types</code><strong>:</strong> Web, Mobile, Memory Service, and Ask Service may be affected.</p>
</li>
<li><p><strong>Change</strong> <code>ui-component</code><strong>:</strong> Web and Mobile may be affected.</p>
</li>
<li><p><strong>Change</strong> <code>embedding-worker</code><strong>:</strong> Its checks can run independently of the other projects.</p>
</li>
</ul>
<p>This reduces the need to maintain manual build and test sequences as the repository grows. The result depends on the configured task graph, available Git change information, and the tasks defined by each package.</p>
<h3>Additional Benefits</h3>
<table>
<thead>
<tr>
<th>Technique</th>
<th>Benefit</th>
</tr>
</thead>
<tbody><tr>
<td>Caching</td>
<td>Reuse valid task results instead of repeating unchanged work</td>
</tr>
<tr>
<td>Parallel execution</td>
<td>Run independent tasks concurrently while respecting dependencies</td>
</tr>
<tr>
<td>Remote caching</td>
<td>Share cached results across developers and CI when configured</td>
</tr>
</tbody></table>
<p>These capabilities can improve development and CI performance, although the primary goal is to manage task relationships and execution more reliably.</p>
<h2>Comparing the Approaches</h2>
<table>
<thead>
<tr>
<th>Capability</th>
<th>Workspaces alone</th>
<th>Turborepo</th>
<th>Nx</th>
</tr>
</thead>
<tbody><tr>
<td>Manage packages and local dependencies</td>
<td>Yes</td>
<td>Uses a workspace package manager</td>
<td>Yes</td>
</tr>
<tr>
<td>Run scripts across packages</td>
<td>Yes, using scripts and filters</td>
<td>Yes</td>
<td>Yes</td>
</tr>
<tr>
<td>Coordinate task dependencies</td>
<td>Manual scripts or additional tooling</td>
<td>Built-in task orchestration</td>
<td>Built-in task orchestration</td>
</tr>
<tr>
<td>Identify affected tasks</td>
<td>Manual selection or custom tooling</td>
<td>Supported</td>
<td>Supported</td>
</tr>
<tr>
<td>Cache task results</td>
<td>Requires additional tooling</td>
<td>Built-in</td>
<td>Built-in</td>
</tr>
<tr>
<td>Run independent tasks in parallel</td>
<td>Manual or script-based</td>
<td>Built-in</td>
<td>Built-in</td>
</tr>
<tr>
<td>Broader project tooling and plugins</td>
<td>Limited to package management</td>
<td>Task-focused</td>
<td>More extensive options</td>
</tr>
</tbody></table>
<p>Workspaces alone can be sufficient for a small repository with simple build and test requirements. Turborepo and Nx become useful when coordinating tasks across project dependencies requires more automation.</p>
<p>Both provide task orchestration, affected-task capabilities, caching, and parallel execution. Nx offers a broader set of project tooling and plugins, while Turborepo focuses more directly on task orchestration.</p>
<p>For Second-Memory, I wanted the task orchestration capabilities without needing a broader project-management system.</p>
]]></content:encoded></item><item><title><![CDATA[Monorepo or Polyrepo?]]></title><description><![CDATA[After deciding to use microservices, the next question was:
How should I organise the codebase?
I chose a monorepo.
Second-Memory has several applications and services:
apps/
  web/
  mobile/

service]]></description><link>https://joung.hashnode.dev/monorepo-or-polyrepo</link><guid isPermaLink="true">https://joung.hashnode.dev/monorepo-or-polyrepo</guid><dc:creator><![CDATA[Joung Park]]></dc:creator><pubDate>Sun, 02 Aug 2026 23:00:00 GMT</pubDate><content:encoded><![CDATA[<p>After deciding to use microservices, the next question was:</p>
<p><strong>How should I organise the codebase?</strong></p>
<p>I chose a <strong>monorepo</strong>.</p>
<p>Second-Memory has several applications and services:</p>
<pre><code class="language-text">apps/
  web/
  mobile/

services/
  memory-service/
  ask-service/
  embedding-worker/

packages/
  api-types/
  ui-components/
</code></pre>
<p>They are independently deployable, but they are all part of the same product.</p>
<h2>Monorepo vs Polyrepo</h2>
<p>There is no universally better choice.</p>
<p>The trade-offs become clearer when looking at common development tasks:</p>
<table>
<thead>
<tr>
<th>Area</th>
<th>Monorepo</th>
<th>Polyrepo</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Share code</strong></td>
<td><strong>Easy</strong> — reusable code can live in shared packages</td>
<td>Harder — publish packages or duplicate code</td>
</tr>
<tr>
<td><strong>Share types</strong></td>
<td><strong>Easy</strong> — TypeScript types can be shared directly</td>
<td>Harder — types need separate versioning</td>
</tr>
<tr>
<td><strong>Cross-project changes</strong></td>
<td><strong>Easy</strong> — related changes can be made in one PR</td>
<td>Harder — changes may require multiple PRs</td>
</tr>
<tr>
<td><strong>Dependency management</strong></td>
<td><strong>Easy</strong> — dependencies can be managed consistently</td>
<td>More complex — each repo manages its own dependencies</td>
</tr>
<tr>
<td><strong>Local development</strong></td>
<td><strong>Easy</strong> — one repository contains the whole system</td>
<td>More setup — multiple repositories need to be configured</td>
</tr>
<tr>
<td><strong>CI/CD</strong></td>
<td>More complex — need to detect affected projects</td>
<td><strong>Easy</strong> — each repository naturally has its own pipeline</td>
</tr>
<tr>
<td><strong>Service isolation</strong></td>
<td>Harder — boundaries need to be enforced within the repository</td>
<td><strong>Easy</strong> — repositories provide natural isolation</td>
</tr>
<tr>
<td><strong>Access control</strong></td>
<td>Harder — repository access generally covers the whole codebase</td>
<td><strong>Easy</strong> — permissions can be managed per repository</td>
</tr>
<tr>
<td><strong>Independent releases</strong></td>
<td>Possible — CI/CD can deploy projects independently</td>
<td><strong>Easy</strong> — each repository has its own release lifecycle</td>
</tr>
</tbody></table>
<p>Neither approach is inherently better.</p>
<p>For Second-Memory, I valued <strong>easy development across the whole system</strong> more than repository-level isolation.</p>
<h2>When to Choose Which</h2>
<p>A monorepo can be a good fit when:</p>
<ul>
<li><p><strong>Small team or solo developer</strong> — one repository is easier to manage than several.</p>
</li>
<li><p><strong>Projects share contracts or code</strong> — changes can be made and tested together.</p>
</li>
<li><p><strong>Cross-project changes are common</strong> — one PR can update multiple applications or services.</p>
</li>
<li><p><strong>The whole system is closely related</strong> — one repository makes it easier to understand and work across the product.</p>
</li>
</ul>
<p>Polyrepo can be a better fit when:</p>
<ul>
<li><p><strong>Large or independent teams</strong> need clear ownership boundaries.</p>
</li>
<li><p><strong>Strong repository isolation</strong> is important.</p>
</li>
<li><p><strong>Projects have different release cycles</strong>.</p>
</li>
<li><p><strong>Access control</strong> needs to differ between projects.</p>
</li>
</ul>
<p>For Second-Memory, being a <strong>sole developer</strong> was one of the main reasons I chose a monorepo.</p>
<p>Managing several repositories would add overhead without giving me much benefit. I didn't need repository-level ownership boundaries between teams.</p>
<p>Instead, I wanted to make it easy to work across the web app, mobile app, backend services, and shared contracts.</p>
<h2>What Should Be Shared?</h2>
<p>A monorepo makes sharing easy.</p>
<p>But <strong>easy to share doesn't mean everything should be shared</strong>.</p>
<p>I wanted to keep the shared surface area small.</p>
<table>
<thead>
<tr>
<th>Shared Part</th>
<th>Used By</th>
<th>Why Share It?</th>
</tr>
</thead>
<tbody><tr>
<td><strong>API types</strong></td>
<td>Web, Mobile, Backend</td>
<td>Keep API contracts consistent</td>
</tr>
<tr>
<td><strong>Web-specific code</strong></td>
<td>Web</td>
<td>Keep it inside <code>apps/web</code></td>
</tr>
<tr>
<td><strong>Mobile-specific code</strong></td>
<td>Mobile</td>
<td>Keep it inside <code>apps/mobile</code></td>
</tr>
<tr>
<td><strong>Service domain logic</strong></td>
<td>Individual service</td>
<td>Keep ownership within the service</td>
</tr>
<tr>
<td><strong>Service implementation</strong></td>
<td>Individual service</td>
<td>Preserve service boundaries</td>
</tr>
</tbody></table>
<p>So instead of creating shared packages for everything, I only create a package when there is a genuine reason to share something.</p>
<p>For example:</p>
<pre><code class="language-text">apps/
  web/
  mobile/
services/
  memory-service/
  ask-service/
  embedding-worker/
packages/
  api-types/
  ui-components/
</code></pre>
<p>The API types are shared because they represent a contract between different parts of the system.</p>
<p>But a Web-only component stays in <code>apps/web</code>.</p>
<p>A Mobile-only utility stays in <code>apps/mobile</code>.</p>
<p>And Memory Service domain logic stays inside <code>memory-service</code>.</p>
<h3>Share the contract, not the ownership</h3>
<p>The fact that services live in the same repository doesn't mean they should share their internal implementation.</p>
<p>The API contract can be shared:</p>
<p><a href="https://mermaid.live/edit#pako:eNo9jU9rhDAUxL9KeGeVaPyTzWHBtpeChdJj1z0Ek1WpSeQ1odvKfvfGhfad3vyGmdlgcEqDgMvivoZJoifdW29JvPbUrusyD9LPzp5Jmh7Jw6nrXkj7-nyGBEacFQiPQSdgNBq5S9j2cA9-0kb3IOKLWoVrqiR-pINbHPbQ21vMr9K-O2f-KtCFcQJxkctnVGFV0uunWY4ozT9FbZXGRxesB5HzeweIDa4gipJlvMyLoua0OeRVFd3vHdOMlSWtmpxRxuq6uSXwc5-lWU0jZxU7NIxxHq1fsThRoA"><img src="https://mermaid.ink/img/pako:eNpVjs1ugzAQhF8F7RmowfzFhxxorzkVKVJxDg7eAArYyECbFOXda4JaqXva-UYzuwtUWiIwuHT6q2qEmZwi58qxU5SDqK6ixvFFDK033QccT1xt5rE84vnkeN7eKTZyKA_63Hb4D-ZlbjtQSecdzWdb2YbNBhdq00pgk5nRhR5NL1YJyxrkMDXYIwdmV4NyvnlSmKtX6U4bDlw9bH4Q6kPr_rfC6LlugF1EN1o1D1JM-NaK2oj-jxr7CZpXPasJWBg-O4AtcLMqon4WBWGYZCTdBXGcuXBfMfFpFJE4DSihNEnShwvfz7PET4jlNKa7lNIss9YPMCdlRw?type=png" alt="" style="display:block;margin:0 auto" /></a></p>
<p>while the services remain independently owned:</p>
<p><a href="https://mermaid.live/edit#pako:eNpVjs1ugzAQhF8F7RmowfzFhxxorzkVKVJxDg7eAArYyECbFOXda4JaqXva-UYzuwtUWiIwuHT6q2qEmZwi58qxU5SDqK6ixvFFDK033QccT1xt5rE84vnkeN7eKTZyKA_63Hb4D-ZlbjtQSecdzWdb2YbNBhdq00pgk5nRhR5NL1YJyxrkMDXYIwdmV4NyvnlSmKtX6U4bDlw9bH4Q6kPr_rfC6LlugF1EN1o1D1JM-NaK2oj-jxr7CZpXPasJWBg-O4AtcLMqon4WBWGYZCTdBXGcuXBfMfFpFJE4DSihNEnShwvfz7PET4jlNKa7lNIss9YPMCdlRw"><img src="https://mermaid.ink/img/pako:eNpNjU9vgkAUxL8KeSdNwADLPzk0ge7FAyd6KnjYwFOJsGueS6s1fveuNGj3tDPzfjM3aFSLkMKuV9_NQZC2PvJaWuYVZVXgoOhqlUhfXYPbPz8rq-x8_GfO55bjvFkFnymuBtHJJzSlGa-yjbWRGkk0ulPyhfPpgOdetZgL8uVMz6FfLR7bXGix3IINe-paSDWNaMOAZAaNhNuDqkEfcMAaUvMlbMeL0wo6Oo3qFdVQy7vhT0J-KjXMFaTG_QHSnejPRo2nVmjkndiTGJ4uoWyR3tUoNaR-NHVAeoOLUQFbJYHn-1HixmsvDBMbrg_bXbEgcMPYYy5jURTfbfiZZt1V5BqfhWwdM5YkJvoFodR4QQ?type=png" alt="" style="display:block;margin:0 auto" /></a></p>
<blockquote>
<p><strong>Share contracts where it makes sense, but don't share service ownership.</strong></p>
</blockquote>
<h2>Keeping the Shared Surface Small</h2>
<p>There is another practical reason for limiting shared code.</p>
<p>The more projects depend on a shared package, the larger the potential impact of a change.</p>
<p>For example:</p>
<p><a href="https://mermaid.live/edit#pako:eNpNjU9vgkAQxb8KmZMmYIDln3toAt2LB0_2VPCwgVGJsGvGpdUav3tXGrRzmnlvfu_doNYNAoddp7_rgyTjfBSVcuysN-Uae01XZ4P01da4_dPzTZmfj__E6d3xvDdnLSZK6F626gmNbi7KfOWslEGStWm1euFifBBFUM6mgGI-0ZMZlrNHt5BGzrfgwp7aBrihAV3okWyhPeH2oCowB-yxAm5Xwma4eI2ko1frTlMFlbpb_iTVp9b9FEF62B-A72R3ttdwaqRB0co9yf6pEqoG6V0PygBnwZgB_AYX4GHEFlkUhGGS-ekyiOPMhetD9hcsivw4DZjPWJKkdxd-xlp_kfhWZzFbpoxlmbV-AaCNeD0"><img src="https://mermaid.ink/img/pako:eNpVjUtvgzAQhP8K2jNQg3nFh0jp48iplSIVcnDwBhDYRgbUpCj_vSZqK7GnnW92ZxaotEBgcOn1V9VwMzkfz6Vy7LwXA686XuP4NFoDxemXO563d47FEc8bkhe5Prc9buFbkaPU5rahh-IwdidwoTatADaZGV2QaCRfJSzrbQlTgxJLYHa17fPVE9x0XqV7bUoo1d3-D1x9ai3_Ioye6wbYhfejVfMg-ISvLa8Nl__UoBJoXvSsJmA0fmQAW-AKLIyon0VBGCYZSXdBHGcu3FZMfBpFJE4DSihNkvTuwvejlvgJsZzGdJdSmmXW-gHi2GSi?type=png" alt="" style="display:block;margin:0 auto" /></a></p>
<p>A change to <code>shared</code> may require all projects to be tested.</p>
<p>But a change here:</p>
<p><a href="https://mermaid.live/edit#pako:eNpVjUtvgzAQhP8K2jNQg3nFh0jp48iplSIVcnDwBhDYRgbUpCj_vSZqK7GnnW92ZxaotEBgcOn1V9VwMzkfz6Vy7LwXA686XuP4NFoDxemXO563d47FEc8bkhe5Prc9buFbkaPU5rahh-IwdidwoTatADaZGV2QaCRfJSzrbQlTgxJLYHa17fPVE9x0XqV7bUoo1d3-D1x9ai3_Ioye6wbYhfejVfMg-ISvLa8Nl__UoBJoXvSsJmA0fmQAW-AKLIyon0VBGCYZSXdBHGcu3FZMfBpFJE4DSihNkvTuwvejlvgJsZzGdJdSmmXW-gHi2GSi"><img src="https://mermaid.ink/img/pako:eNo9jU9vgzAMxb9K5DPQQPibQy_0MmmnXZBWesiIC2iQIBfUbqjffYFp88Xv_Ww_r9BYjSDhOth70yma2etbbZir8lx2yrTIesPUNN0Od_y4MN8_supcOfm7Ve2kfNmQa4fydAEPWuo1yJkW9GBEGtVmYd1Oapg7HLEG6SShXh6-VvTpN3awVENtnu5-Uubd2vEvguzSdiCvarg5t0xazXjqVUtq_KeERiOVdjEzSFHsGSBXeICMYhHkcRhFac6zIkyS3IOvDfNAxDFPslBwIdI0e3rwvb_lQcodF4koMiHy3I1-AHC-WUA?type=png" alt="" style="display:block;margin:0 auto" /></a></p>
<p>only affects Web.</p>
<p>This has implications for both <strong>CI/CD</strong> and <strong>application dependencies</strong>.</p>
<p>I don't want a large <code>shared</code> package that contains unrelated functionality used by almost every project.</p>
<p>Instead, I prefer small, focused shared packages with clear reasons for existing.</p>
<h2>The CI/CD Trade-off</h2>
<p>CI/CD is one area where polyrepo can be simpler.</p>
<p>With polyrepo, a change to <code>memory-service</code> naturally belongs to the <code>memory-service</code> repository.</p>
<p><a href="https://mermaid.live/edit#pako:eNo9jU9vgzAMxb9K5DPQQPibQy_0MmmnXZBWesiIC2iQIBfUbqjffYFp88Xv_Ww_r9BYjSDhOth70yma2etbbZir8lx2yrTIesPUNN0Od_y4MN8_supcOfm7Ve2kfNmQa4fydAEPWuo1yJkW9GBEGtVmYd1Oapg7HLEG6SShXh6-VvTpN3awVENtnu5-Uubd2vEvguzSdiCvarg5t0xazXjqVUtq_KeERiOVdjEzSFHsGSBXeICMYhHkcRhFac6zIkyS3IOvDfNAxDFPslBwIdI0e3rwvb_lQcodF4koMiHy3I1-AHC-WUA"><img src="https://mermaid.ink/img/pako:eNpFTUFugzAQ_Iq1ZyAGYyA-5EIulXLKBalxDi7eACrYyAElLcrfa6iq7mVnZmdmF6itRhBw6-2jbpWbyOksDfFTXspWmQZJZ4gax_vugR9XEoYHUl0qD39d1aaUb6vk1648XiGAxnUaxORmDGBAN6iVwrJGJEwtDihBeOhQz89QK_cZ1ra3ToI0L58flXm3dvircHZuWhA31d89m0etJjx2qnHq34JGoyvtbCYQnG4VIBZ4gkhSFhVpnCRZQfN9zHkRwNcq04ilKeV5zChjWZa_AvjevtIoo15nnO1zxorCn34AEW9Y7g?type=png" alt="" style="display:block;margin:0 auto" /></a></p>
<p>In a monorepo, multiple deployable units live in the same repository.</p>
<p>A change to:</p>
<pre><code class="language-text">services/memory-service/
</code></pre>
<p>shouldn't necessarily rebuild and deploy everything.</p>
<p>The pipeline needs to understand:</p>
<ul>
<li><p>what changed</p>
</li>
<li><p>what depends on it</p>
</li>
<li><p>what needs to be tested</p>
</li>
<li><p>what needs to be built</p>
</li>
<li><p>what needs to be deployed</p>
</li>
</ul>
<p>This becomes particularly important when shared packages are involved.</p>
<p>A change to <code>ui-components</code> may affect only web and mobile projects, while a change isolated to <code>services/embedding-worker/</code> should ideally affect only the relevant pipeline.</p>
<p>The repository structure itself is simple, but the build and deployment system needs to become more intelligent as the project grows.</p>
<h2>Why Monorepo for Second-Memory?</h2>
<p>The main benefits for me were:</p>
<ul>
<li><p><strong>Easy</strong> sharing of API types</p>
</li>
<li><p><strong>Easy</strong> cross-project changes</p>
</li>
<li><p><strong>Easy</strong> local development</p>
</li>
<li><p><strong>Easy</strong> dependency management</p>
</li>
<li><p><strong>Easy</strong> visibility of the whole product</p>
</li>
<li><p>Less overhead for a <strong>solo developer</strong></p>
</li>
</ul>
<p>The trade-offs were:</p>
<ul>
<li><p><strong>More complex</strong> CI/CD</p>
</li>
<li><p>More tooling needed as the repository grows</p>
</li>
<li><p>Less repository-level isolation</p>
</li>
<li><p>The need to carefully control shared dependencies</p>
</li>
</ul>
<p>For Second-Memory, I was willing to accept these trade-offs.</p>
<p>The goal wasn't to share as much code as possible.</p>
<p>The goal was to get the convenience of a monorepo while keeping <strong>ownership and dependencies clear</strong>.</p>
<p>That led to the next question:</p>
<p><strong>How do I manage builds, dependencies, task execution, and caching across all these projects?</strong></p>
]]></content:encoded></item><item><title><![CDATA[Microservices or Monolith?]]></title><description><![CDATA[The initial architecture had three main backend responsibilities:

Memory Service

Ask Service

Embedding Worker


But there was another architectural decision behind this:
Why make them separate serv]]></description><link>https://joung.hashnode.dev/microservices-or-monolith</link><guid isPermaLink="true">https://joung.hashnode.dev/microservices-or-monolith</guid><dc:creator><![CDATA[Joung Park]]></dc:creator><pubDate>Sat, 01 Aug 2026 23:00:00 GMT</pubDate><content:encoded><![CDATA[<p>The initial architecture had three main backend responsibilities:</p>
<ul>
<li><p>Memory Service</p>
</li>
<li><p>Ask Service</p>
</li>
<li><p>Embedding Worker</p>
</li>
</ul>
<p>But there was another architectural decision behind this:</p>
<p><strong>Why make them separate services instead of building one backend?</strong></p>
<p>A monolith would have been simpler.</p>
<p>For a small application, it would probably have been enough.</p>
<p>I still chose a microservice architecture for Second-Memory for three main reasons.</p>
<h2>1. Clear boundaries</h2>
<p>Memory and Ask have different responsibilities.</p>
<p>The <strong>Memory Service</strong> owns the memory domain:</p>
<ul>
<li><p>create and manage memories</p>
</li>
<li><p>persist memory data</p>
</li>
<li><p>generate/retrieve relevant memories</p>
</li>
<li><p>manage embeddings</p>
</li>
</ul>
<p>The <strong>Ask Service</strong> owns the AI interaction:</p>
<ul>
<li><p>receive questions</p>
</li>
<li><p>retrieve relevant context</p>
</li>
<li><p>construct prompts</p>
</li>
<li><p>call the LLM</p>
</li>
<li><p>return the answer</p>
</li>
</ul>
<p>They work together, but they represent different responsibilities.</p>
<p>I wanted those boundaries to exist at the service level rather than only as modules inside one application.</p>
<p><a href="https://mermaid.live/edit#pako:eNpdj09vgkAQxb_KZq4Fgi7_3EMTxKZpgknTJj0UPGxgRCPLmhGqlPjdu1DtoXOa937zZjIDFLpEELCt9bnYSWpZ-pY3zFScxa8v7Fm2eJb9htn2I1tma1SaevaO9LUvcHObnGCSxafDP7JktmPQ6p57YB9YtJpuOPnFT1maro0FFlS0L0G01KEFCknJUcIwjufQ7lBhDsK0hGV3sUtJB7vQtaYc8uZq8kfZfGqt7itId9UOxFbWJ6O6Y2m-We1lRVL9uYRNiZTormlB-NMKEANcQMw97kTebD4PIjdczHw_sqAfbdfhnuf64Yy7nAdBeLXge7rqOoFrfO7zRch5FBn0A75oaeE"><img src="https://mermaid.ink/img/pako:eNpdj09rg0AQxb_KMteqrK7_soeCSUopGCgt9FDNYdGJCYlumGgTK_nuXW1CoXOaeb83b5gBCl0iSNgc9LnYKmpZ-pY3zFSSJa8v7Fm1eFb9mtn2I5tnK6w19ewd6WtX4PrmnOAiS077f2TObMeg5X3vgX1g0Wq64cUvfsrSdGUksKCiXQmypQ4tqJFqNY4wjPYc2i3WmIM0LWHZXexS0d4u9EFTDnlzNftH1XxqXd8jSHfVFuRGHU5m6o6l-Wa5UxWpPws2JdJCd00L0uVTBMgBLiA9Xzix73peGPNo5gZBbEE_ytwRvs-DyBVciDCMrhZ8T1e5E3Kji0DMIiHi2KAftZ1pwg?type=png" alt="" style="display:block;margin:0 auto" /></a></p>
<p>The Embedding Worker is separated for a different reason.</p>
<p>Embedding generation is asynchronous work and doesn't need to be part of the request that creates a memory.</p>
<p><a href="https://mermaid.live/edit#pako:eNpdj09rg0AQxb_KMteqrK7_soeCSUopGCgt9FDNYdGJCYlumGgTK_nuXW1CoXOaeb83b5gBCl0iSNgc9LnYKmpZ-pY3zFSSJa8v7Fm1eFb9mtn2I5tnK6w19ewd6WtX4PrmnOAiS077f2TObMeg5X3vgX1g0Wq64cUvfsrSdGUksKCiXQmypQ4tqJFqNY4wjPYc2i3WmIM0LWHZXexS0d4u9EFTDnlzNftH1XxqXd8jSHfVFuRGHU5m6o6l-Wa5UxWpPws2JdJCd00L0uVTBMgBLiA9Xzix73peGPNo5gZBbEE_ytwRvs-DyBVciDCMrhZ8T1e5E3Kji0DMIiHi2KAftZ1pwg"><img src="https://mermaid.ink/img/pako:eNo9jUtvgzAQhP8K2jMgg3nFh0oN6a1V1fZQqcDBwRtAwTja4pYU5b-X0Meedr7dmZmhNgpBwKE3n3UraXTun8vBWea2eEBt6Oy8IH10NVaO59042-LRjnszVT9P2xXmxZNFi78sX9muuNN7VKobGufV0BGpAhca6hSIkSy6oJG0vEqYr8YSxhY1liCWlVDZyVOSjl5tekMllMNl8Z_k8GaM_osgY5sWxEH274uyJyVH3HWyIan_KeGgkHJjhxFEEK0ZIGaYQIQR97MoCMMkY-kmiOPMhfMVM59HEYvTgDPOkyS9uPC11jI_YQvnMd-knGfZcvoGzytiEg?type=png" alt="" style="display:block;margin:0 auto" /></a></p>
<p>The boundaries therefore follow the responsibilities and execution models of the system.</p>
<h2>2. A system for learning AI engineering</h2>
<p>Second-Memory is also a project for exploring how AI systems are built.</p>
<p>I didn't want the architecture to stop at:</p>
<p><a href="https://mermaid.live/edit#pako:eNo9jUtvgzAQhP8K2jMgg3nFh0oN6a1V1fZQqcDBwRtAwTja4pYU5b-X0Meedr7dmZmhNgpBwKE3n3UraXTun8vBWea2eEBt6Oy8IH10NVaO59042-LRjnszVT9P2xXmxZNFi78sX9muuNN7VKobGufV0BGpAhca6hSIkSy6oJG0vEqYr8YSxhY1liCWlVDZyVOSjl5tekMllMNl8Z_k8GaM_osgY5sWxEH274uyJyVH3HWyIan_KeGgkHJjhxFEEK0ZIGaYQIQR97MoCMMkY-kmiOPMhfMVM59HEYvTgDPOkyS9uPC11jI_YQvnMd-knGfZcvoGzytiEg"><img src="https://mermaid.ink/img/pako:eNo9jU9rhDAUxL9KeGeVaPyTzWHBtpeChdJj1z0Ek1WpSeQ1odvKfvfGhfad3vyGmdlgcEqDgMvivoZJoifdW29JvPbUrusyD9LPzp5Jmh7Jw6nrXkj7-nyGBEacFQiPQSdgNBq5S9j2cA9-0kb3IOKLWoVrqiR-pINbHPbQ21vMr9K-O2f-KtCFcQJxkctnVGFV0uunWY4ozT9FbZXGRxesB5HzeweIDa4gipJlvMyLoua0OeRVFd3vHdOMlSWtmpxRxuq6uSXwc5-lWU0jZxU7NIxxHq1fsThRoA?type=png" alt="" style="display:block;margin:0 auto" /></a></p>
<p>The system gave me an opportunity to work with several patterns that are common in production AI systems:</p>
<ul>
<li><p>asynchronous processing</p>
</li>
<li><p>queues</p>
</li>
<li><p>workers</p>
</li>
<li><p>embeddings</p>
</li>
<li><p>vector search</p>
</li>
<li><p>retrieval</p>
</li>
<li><p>service-to-service communication</p>
</li>
<li><p>LLM integration</p>
</li>
</ul>
<p>Using separate services makes these boundaries explicit and gives me a way to experiment with each part independently.</p>
<p>The goal isn't to use microservices simply because they are popular.</p>
<p>The architecture should help me learn and validate how these pieces work together as a real system.</p>
<h2>3. Production readiness and scaling</h2>
<p>I also wanted to design Second-Memory as something that could eventually grow into a production system.</p>
<p>The different components have different scaling characteristics.</p>
<p>If memory creation increases, I may need more Memory Service instances.</p>
<p>If embedding jobs increase, I can scale the workers independently.</p>
<p>If AI requests become the bottleneck, the Ask Service can scale independently.</p>
<p>This doesn't mean I need to scale everything independently today.</p>
<p>It means the architecture doesn't force unrelated workloads into the same deployment unit.</p>
<h2>What about a monolith?</h2>
<p>A monolith would still be a reasonable choice for Second-Memory.</p>
<p>It would have:</p>
<ul>
<li><p>less infrastructure</p>
</li>
<li><p>simpler deployment</p>
</li>
<li><p>simpler local development</p>
</li>
<li><p>fewer service-to-service calls</p>
</li>
<li><p>less operational overhead</p>
</li>
</ul>
<p>So why accept the additional complexity?</p>
<p>Because my goal wasn't only to build the simplest V1.</p>
<p>I wanted to build a system with <strong>clear boundaries that could evolve toward production scale</strong>, while giving me a practical environment to explore AI system architecture.</p>
<p>The trade-off was intentional.</p>
<blockquote>
<p><strong>I chose microservices not because a monolith couldn't work, but because the boundaries, asynchronous workloads, learning goals, and potential scaling characteristics made the additional complexity worthwhile.</strong></p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[Why the Outbox Pattern, Queue, and Embedding Worker?]]></title><description><![CDATA[Why the Outbox Pattern, Queue, and Embedding Worker?
The previous post covered why I chose pgvector for semantic search.
But there was another question:
When should a memory be embedded?
At first, it ]]></description><link>https://joung.hashnode.dev/why-the-outbox-pattern-queue-and-embedding-worker</link><guid isPermaLink="true">https://joung.hashnode.dev/why-the-outbox-pattern-queue-and-embedding-worker</guid><dc:creator><![CDATA[Joung Park]]></dc:creator><pubDate>Fri, 31 Jul 2026 23:00:00 GMT</pubDate><content:encoded><![CDATA[<h1>Why the Outbox Pattern, Queue, and Embedding Worker?</h1>
<p>The previous post covered why I chose pgvector for semantic search.</p>
<p>But there was another question:</p>
<p><strong>When should a memory be embedded?</strong></p>
<p>At first, it might seem simple:</p>
<pre><code class="language-text">Create memory
     ▼
Generate embedding
     ▼
Save embedding
</code></pre>
<p>But this makes memory creation dependent on the embedding process.</p>
<p>If the embedding provider is slow or unavailable, creating a memory could also fail or become slow.</p>
<p>I wanted to separate these two operations.</p>
<p>The architecture became:</p>
<pre><code class="language-text">Memory Service
      ▼
 PostgreSQL
      ▼
 Outbox Table
      ▼
 Queue / Job Runner
      ▼
 Embedding Worker
      ▼
 pgvector
</code></pre>
<h2>1. The Outbox Pattern</h2>
<p>When a user creates a memory, the Memory Service writes the memory and an outbox event in the same database transaction.</p>
<p>For example:</p>
<pre><code class="language-text">Transaction
┌─────────────────────────────┐
│ INSERT memory               │
│ INSERT outbox event         │
└─────────────────────────────┘
</code></pre>
<p>The important part is that they succeed or fail together.</p>
<p>This avoids a common problem with distributed systems:</p>
<pre><code class="language-text">Memory saved
     ▼
Embedding event lost
</code></pre>
<p>The outbox gives me a durable record of the work that needs to happen.</p>
<p>The outbox is not the queue itself.</p>
<p>It is a reliable bridge between the database transaction and asynchronous processing.</p>
<h2>2. The Queue / Job Runner</h2>
<p>A separate process reads pending outbox events and puts jobs onto a queue.</p>
<pre><code class="language-text">Outbox
   ▼
Queue
   ▼
Embedding Worker
</code></pre>
<p>This gives the embedding process some useful properties:</p>
<ul>
<li><p>asynchronous processing</p>
</li>
<li><p>retries</p>
</li>
<li><p>independent scaling</p>
</li>
<li><p>failure isolation</p>
</li>
<li><p>no need to block the memory API</p>
</li>
</ul>
<p>The user can save a memory without waiting for the embedding provider.</p>
<h2>3. The Embedding Worker</h2>
<p>The embedding worker has one main responsibility:</p>
<p><strong>turn memory text into an embedding and store it in pgvector.</strong></p>
<p><a href="https://mermaid.live/edit#pako:eNo9jkFPwzAMhf9K5HM7tUvS0hyQ2CZx4gTiwNpDaLy2okkmk4qNqv-drAh8sZ_1vWfP0HqDoOA0-q-21xTYy6F2LNbD8REDe0Lr6dqwNL1nu7hxSDogQ_uOxgyua37h3Qrsj8_BE7JXbGNvIIGOBgMq0IQJWCSrbxLmm6mG0KPFGlQcCc10SY2mj7T1o6caardE_1m7N-_tXwT5qetBnfT4GdV0NvGXw6A70vZ_S-gM0t5PLoDaFvkaAmqGC6g8yzaFyKqiFJwLkVdVAteV2uRlJUVZ8ay649slge_1bMRlzqXkvJRlIUWRLz9qtl3u"><img src="https://mermaid.ink/img/pako:eNo9TkFOwzAQ_Iq156RyYsdNfUCircSJE4gDTQ5uvE0rartaHNES5e-4QTCX3RnNzO4IXbAIGg7n8NUdDUX2um08S3jcPWFkz-gC3VqW5w9snRSPZCIydHu09uT79te8ng2b3UsMhOwNuzRbyKCnkwUdacAMHJIzdwrjPdRAPKLDBnRaCe1wza2hj7wL50ANNH5K-Yvx7yG4vwoKQ38EfTDnz8SGi02_bE-mJ-P-VUJvkTZh8BF0qaq5BPQIV9BFrRZiVQlVSlWIqpAZ3ECLqlyoFReiLlbFki_FlMH3fJUvZCVVLRM4l4oXXE0_bK1d2g?type=png" alt="" style="display:block;margin:0 auto" /></a></p>
<p>This keeps embedding-specific logic out of the Memory Service's synchronous request path.</p>
<p>It also gives me a place to evolve the embedding pipeline later.</p>
<p>For example, I could change:</p>
<ul>
<li><p>embedding models</p>
</li>
<li><p>chunking strategy</p>
</li>
<li><p>retry behaviour</p>
</li>
<li><p>batch processing</p>
</li>
<li><p>embedding dimensions</p>
</li>
</ul>
<p>without changing the API used to create a memory.</p>
<h2>4. Why asynchronous?</h2>
<p>There is an important consequence of this design:</p>
<p><strong>A newly created memory may not be immediately searchable.</strong></p>
<p>There can be a small delay between:</p>
<pre><code class="language-text">Memory created
     │ asynchronous processing
     ▼
Embedding created
     ▼
Memory available for semantic search
</code></pre>
<p>I accepted this trade-off.</p>
<p>For Second-Memory, immediate persistence is more important than making the embedding operation part of the user's request.</p>
<p>This is essentially <strong>eventual consistency</strong> between the memory record and its vector representation.</p>
<h2>5. Failure becomes easier to handle</h2>
<p>The asynchronous design also changes how failures work.</p>
<p>If the embedding provider temporarily fails:</p>
<pre><code class="language-text">Memory
  ▼
Outbox
  ▼
Queue
  ▼
Embedding Worker - X -&gt; Retry
</code></pre>
<p>The memory itself has already been safely stored.</p>
<p>The embedding job can be retried without asking the user to submit the memory again.</p>
<p>That separation was important to me.</p>
<h2>The decision</h2>
<p>The final flow became:</p>
<p><a href="https://mermaid.live/edit#pako:eNpVUFFrwjAQ_ivhQHDQim3aqh3sRYsvc7ZsKKz1IdpbLdpE0kTcxP--aLvN3cOR77vvvrvcGTYiRwihkOywJW-TjBMTnQ55MXzdoPEsHUtkCskMKyE_V8S2n0g8TbuxqFUh8TV5flg12ibXet0YxlNyr2pF15ilrdkfNY_SuVZrcSLREblqK8jze-t5dJuepIlGja0muXHRMo2qNeZ5yQuyFHKHsq1Hy2blRdo9FEfcKCFJycn_9RvpZs_q2uxtxQuSM8XWrMbHjINlTlTmECqp0YIKZcWuEM7XtgzUFivMIDRPibk-2TmTO3sj9kJmkPGL6T8w_i5E9WMhhS62EH6wfW2QPphhOCmZuVv1y0rzeZRjobmCkI7ozQTCM5wgdIZBj458Grhe4FDf8Sz4NCLf7QWjPqVDZ-QM-gN6seDrNrXf83wvGHoeDajruP7Q9y7fZ--ZrQ"><img src="https://mermaid.ink/img/pako:eNpVUFFrwjAQ_ivhQHBQxTZtrR3sRYsvc7ZsKKz1IdpbLdpE0kTcxP--aLu53cOR77vvvrvcGTYiRwihkOywJW-TjBMTnQ55MXzdoPEsHUtkCskMKyE_V6TXeyLxNO3GolaFxNfk-WHVaJtc63VjGE9Jehe1mmvM0tbrTs2jdK7VWpxIdESu2gry_K_zPLoNT9JEo8ZWk9y4aJlG1RrzvOQFWQq5Q9nWo2Wz8SLtHoojbpSQpOTk__aNdLNndW3WtuIFyZlia1bjY8bBMhcqcwiV1GhBhbJiVwjna1sGaosVZhCap8Rcn3o5k7veRuyFzCDjF9N_YPxdiOrHQgpdbCH8YPvaIH0ww3BSMnO26peV5vMox0JzBSEd0ZsJhGc4QWgHfp-OPOo7rm9Tz3Yt-DQiz-n7owGlgT2yh4MhvVjwdZs66Lue6weuS33q2I4XeO7lGwmcmYU?type=png" alt="" style="display:block;margin:0 auto" /></a></p>
<p>Each part has a different responsibility:</p>
<table>
<thead>
<tr>
<th>Component</th>
<th>Responsibility</th>
</tr>
</thead>
<tbody><tr>
<td>Memory Service</td>
<td>Store the memory</td>
</tr>
<tr>
<td>Outbox</td>
<td>Reliably record the event</td>
</tr>
<tr>
<td>Queue</td>
<td>Deliver asynchronous work</td>
</tr>
<tr>
<td>Embedding Worker</td>
<td>Generate embeddings</td>
</tr>
<tr>
<td>pgvector</td>
<td>Store and search vectors</td>
</tr>
</tbody></table>
<p>This added some complexity compared with simply generating the embedding inside the API request.</p>
<p>But it gave me something more important:</p>
<p><strong>memory creation is no longer tightly coupled to the availability of the embedding pipeline.</strong></p>
<p>That was the trade-off I wanted.</p>
]]></content:encoded></item><item><title><![CDATA[Why pgvector?]]></title><description><![CDATA[Why pgvector?
Second-Memory stores things a user writes over time.
If a user later asks:

“What did I write about performance problems?”

A normal keyword search isn't always enough.
The memory might ]]></description><link>https://joung.hashnode.dev/why-pgvector</link><guid isPermaLink="true">https://joung.hashnode.dev/why-pgvector</guid><dc:creator><![CDATA[Joung Park]]></dc:creator><pubDate>Thu, 30 Jul 2026 23:00:00 GMT</pubDate><content:encoded><![CDATA[<h1>Why pgvector?</h1>
<p>Second-Memory stores things a user writes over time.</p>
<p>If a user later asks:</p>
<blockquote>
<p>“What did I write about performance problems?”</p>
</blockquote>
<p>A normal keyword search isn't always enough.</p>
<p>The memory might say:</p>
<blockquote>
<p>“The database becomes slow when thousands of users query it at the same time.”</p>
</blockquote>
<p>There may be no exact keyword match between the question and the memory.</p>
<p>This is where <strong>semantic search</strong> becomes useful.</p>
<h2>1. From text to vectors</h2>
<p>I can convert a piece of text into an <strong>embedding</strong> — a vector representation of its meaning.</p>
<p>For example:</p>
<pre><code class="language-text">"The database becomes slow when thousands of users query it at the same time."
         ↓
[Embedding model]
         ↓
[0.021, -0.183, ...]
</code></pre>
<p>The actual vector contains many dimensions, so it isn't meaningful to look at the individual numbers.</p>
<p>What matters is the relationship between vectors.</p>
<p>Texts with similar meanings tend to have vectors that are closer together.</p>
<p>So when a user asks a question, I can:</p>
<ol>
<li><p>Question</p>
</li>
<li><p>Embedding</p>
</li>
<li><p>Vector similarity search</p>
</li>
<li><p>Relevant memories</p>
</li>
</ol>
<p>This gives Second-Memory a way to retrieve memories based on <strong>meaning</strong>, rather than just matching words.</p>
<h2>2. Why pgvector?</h2>
<p>Once I decided to use embeddings, I needed somewhere to store and search them.</p>
<p>I considered:</p>
<table>
<thead>
<tr>
<th></th>
<th>pgvector</th>
<th>Pinecone</th>
</tr>
</thead>
<tbody><tr>
<td>Vector search</td>
<td>Yes</td>
<td>Yes</td>
</tr>
<tr>
<td>Relational data</td>
<td>Yes</td>
<td>No</td>
</tr>
<tr>
<td>Existing PostgreSQL</td>
<td>Yes</td>
<td>No</td>
</tr>
<tr>
<td>Separate infrastructure</td>
<td>No</td>
<td>Yes</td>
</tr>
<tr>
<td>Operational complexity</td>
<td>Lower</td>
<td>Higher</td>
</tr>
<tr>
<td>Good fit for V1</td>
<td>Yes</td>
<td>Yes</td>
</tr>
</tbody></table>
<p>I chose <strong>pgvector</strong>.</p>
<p>The main reason wasn't that pgvector is necessarily better than Pinecone.</p>
<p>It was that Second-Memory already had a natural place for the vectors:</p>
<p><strong>the Memory Service's database.</strong></p>
<p>With pgvector, I could keep the memory and its embedding together.</p>
<p><a href="https://mermaid.live/edit#pako:eNp1UEFugzAQ_Iq110IUIBTwtT22UqvcKqTKxVtiFdvRYkdJgL_XgaQ5dU-emZ0Z2wM0ViJwQHpWoiWha8PC1PCK2tKJbZEOqsEa2DjG8TgG5c32riXcvr-wB7ZvD9g4S2GBM-9Up87Y3zL-25yjBnat4Kyxxgllgm9xXoVhQZfxXknme6RPJe-sw6ObzWjcnV1qGOovlFKZdlGm2kAELSkJ3JHHCDSSFhcIc1ENboc6vJSHI6H0x1gK-okb210uXZsp-PfCfFirbxFkfbsD_i26PiC_l8Lh9R__WEIjkZ6sNw74Ji_nEOADHIGnabUq1kmR5Jv1JkvKKo3gBDypVllZ5o95laVFVlTpFMF5rl2vyiKffgFsu4pS"><img src="https://mermaid.ink/img/pako:eNp1kdFvgjAQxv8Vcq8DIyACjfFlPm7JFt8WElPpDclsa67F6Az_-wq4BY27p953_X3fNb1AqQUCg3LPjVnVvCIuC-W56hXvFaWm8xrpWJfoXYZRV4vFVVwuB7Et1Bh808ZWhOv3l82hOmJpNd3iK275lpv_-CF4jDw1BmlTi5FSamVR2ZGCcotC1Kq6M719RxAsHy7IPJdhBuLRvOOuizGvC-e1ctfBh4pqAcxSgz5IJMm7FvrtC7A7lFgAc0dC0ZwCwekrKPVeUwGFah1_4OpDa_lrQbqpdsA--d64rjkIbvH6O38qoRJIz7pRFthsHvUmwC5wAhZF-SSdhmmYzKazOMxyNz0DC_NJnGXJPMnjKI3TPGp9-O5jp5MsTdofu2OlLQ?type=png" alt="" style="display:block;margin:0 auto" /></a></p>
<p>That kept the architecture simple.</p>
<h2>3. Why not introduce a vector database?</h2>
<p>A dedicated vector database could make sense at larger scale.</p>
<p>But introducing one also creates another system to operate and another boundary to manage.</p>
<p>For V1, I didn't see enough benefit to justify that complexity.</p>
<p>The Memory Service could own:</p>
<ul>
<li><p>memory data</p>
</li>
<li><p>embeddings</p>
</li>
<li><p>vector search</p>
</li>
</ul>
<p>all within the same data boundary.</p>
<p>That also reinforced one of the architectural principles from the previous post:</p>
<blockquote>
<p><strong>The service that owns the data should own access to it.</strong></p>
</blockquote>
<p>The Ask Service doesn't need to know whether semantic search is implemented with pgvector, Pinecone, or something else.</p>
<p>It simply asks the Memory Service for relevant memories.</p>
<h2>4. What this gives me</h2>
<p>With pgvector, the basic retrieval flow becomes:</p>
<pre><code class="language-text">User question
      ↓
Generate query embedding
      ↓
Memory Service
      ↓
pgvector similarity search
      ↓
Relevant memories
      ↓
Ask Service
      ↓
     LLM
</code></pre>
<p>This is the first important piece of the AI architecture.</p>
<p>The LLM doesn't need to know everything the user has ever written.</p>
<p>Instead, the system retrieves the memories that are most relevant to the current question and uses those as context.</p>
<h2>The decision</h2>
<p>For Second-Memory V1, I chose:</p>
<p><strong>PostgreSQL + pgvector</strong></p>
<p>because it gave me semantic search without introducing another database.</p>
<p>It was a pragmatic choice:</p>
<ul>
<li><p>vector search</p>
</li>
<li><p>relational data</p>
</li>
<li><p>one data boundary</p>
</li>
<li><p>less infrastructure</p>
</li>
<li><p>simpler development</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[The First Architecture Draft]]></title><description><![CDATA[I had the product definition, user stories, and backlog.
Now it was time to design the system.
I started with the main responsibilities of Second-Memory and came up with my first architecture draft:

]]></description><link>https://joung.hashnode.dev/the-first-architecture-draft</link><guid isPermaLink="true">https://joung.hashnode.dev/the-first-architecture-draft</guid><dc:creator><![CDATA[Joung Park]]></dc:creator><pubDate>Wed, 29 Jul 2026 23:00:00 GMT</pubDate><content:encoded><![CDATA[<p>I had the product definition, user stories, and backlog.</p>
<p>Now it was time to design the system.</p>
<p>I started with the main responsibilities of Second-Memory and came up with my first architecture draft:</p>
<p><a href="https://mermaid.live/edit#pako:eNpdkGuLglAQhv_KYT4VaJua6-XDgiXFQkIgFOxR6KRTSuqJ07HLRv99zXajdj7NO887F-YCCU8RXFgX_JhkTEgSVaSJBV3gioyKHCsZE1X9IBPqzT7JhEk8sjN5I8PxOL57AxrwVV7gqz2q7nTSSo96tczi51IQ0gBLLs4kRHHIE3yhXki9_fYJ_e4KW-rTjs8kW7E9duMXMqedOSaSC-IPu4827w6ndDoNyEzwQ56i-E-DMKqWyyUosBF5Cq4UNSpQoijZTcLl5o5AZlhiBG6TCkzrk5oysVUTXnARQVRdm_4dq744L_9GCF5vMnDXrNg3qt6lzRP9nG0EKx9VgVVz0ojXlQTXtNsZ4F7gBK6uOz2rr1maOegPDM12dAXO4GpOz7Bt8910DN0yLEe_KvDdbu33bMu8_gCKM42k"><img src="https://mermaid.ink/img/pako:eNpdkF2LglAQhv_KYa42yDY1U8_FgiXFQkIgFOzRi5NOKaknTsc-Nvrva7YbtXM17zzvfDAXSESKQGFdiGOScalIVJEmlmyJKzIucqxUTDTtg0yZN_8kU67wyM_knYwmk_juDVggVnmBr_aoutNpKz3m1SqLn0tByAIshTyTEOUhT_CFeiHz9tsn9LsrbKnP3nyu-IrvsRO_kAV7W2CihCT-qPNo8-5wxmazgMylOOQpyv80CKELG5mnQJWssQslypLfJFxuxghUhiVGQJtUYlqftJTLrZaIQsgIoura9O949SVE-TdCinqTAV3zYt-oepc2__NzvpG8fFQlVs01Y1FXCujQaGcAvcAJqGG4Pbuv27o16A9M3XEbegaquz3Tcayh5ZqGbdquce3Cd7u133Ns6_oDe4GLtQ?type=png" alt="" style="display:block;margin:0 auto" /></a></p>
<p>The diagram itself is fairly simple, but there were several design principles behind it.</p>
<h2>1. BFF for the clients</h2>
<p>Both the web and mobile clients communicate through the API Gateway / BFF.</p>
<p>I chose the <strong>Backend for Frontend (BFF)</strong> pattern so that the clients don't need to communicate directly with the internal services.</p>
<p>The BFF provides a client-facing API while hiding the internal architecture.</p>
<p>It also gives me a place to handle things such as authentication, routing, and client-specific requirements.</p>
<p>This means the clients don't need to know whether a request is handled by the Memory Service, Ask Service, or something else.</p>
<p>The internal architecture can evolve without making the clients aware of every change.</p>
<h2>2. Each service owns its data</h2>
<p>One principle I wanted to follow was:</p>
<p><strong>Don't share databases between services.</strong></p>
<p>A service owns the data that belongs to its responsibility.</p>
<p>In this architecture, the Memory Service owns both the database and vector database.</p>
<p>If another service needs memory data, it doesn't connect directly to either database.</p>
<p>Instead, it calls the Memory Service through an internal API.</p>
<p>For example:</p>
<p><a href="https://mermaid.live/edit#pako:eNo9jc1ug0AMhF9l5VMqAeInFNhDJVouOUSqhJRDIYctOIACu5FZ2rSEdy8QpT555vOMRyhUicDh1KrvohakWS7ZPHGaxf2ZpUhfTYFHZpovt53USFK0LH7f3dg-vV_u05WyJNskQotP0ePT8YFWcsg2Byy0Ipa8LggMqKgpgWsa0IAOqROLhHGJ5aBr7DAHPq-E5XA1S0Fns1CtohxyOc35i5AfSnWPClJDVQM_ibaf1XAphcakERWJ7t8llCXSmxqkBu769loCfITrLN3ICmwncPytvfWcMHIN-AHuRJYXhv6zH3lu4AWROxnwu761rTDwpz_-WGDR"><img src="https://mermaid.ink/img/pako:eNpVjU9rg0AQxb_KMqcUVPxbdQ8FWy85BApCDtUctjpRie6GcW3TGr97NSWFzmne-817M0GpKgQOx059lo0gzQrJlkmyPBlOLEP6aEs8MNN8um6lRpKiY8nr9sp22e_lLlshS_NNKrR4FwM-HP6Rfb7ZY6kVsfR5RWBATW0FXNOIBvRIvVglTGusAN1gjwXwZSWsxotZCTqZpeoUFVDIecmfhXxTqr9XkBrrBvhRdMOixnMlNKatqEn0fy6hrJBe1Cg1cDfwbyXAJ7gs0o2t0HZCJ_Bt33Oi2DXgC7gTW14UBY9B7LmhF8bubMD37a1tRWEw_wCPG2B4?type=png" alt="" style="display:block;margin:0 auto" /></a></p>
<p>This keeps the data ownership clear and prevents other services from becoming coupled to the Memory Service's storage implementation.</p>
<p>If the storage implementation changes later, the other service shouldn't need to know.</p>
<h2>3. Separation of concerns</h2>
<p>Each component has a specific responsibility.</p>
<p>The BFF deals with client-facing communication.</p>
<p>The Memory Service deals with memory.</p>
<p>The Ask Service deals with the AI interaction.</p>
<p>The LLM generates the response.</p>
<p>The databases provide persistence.</p>
<p>This separation makes the architecture easier to reason about.</p>
<p>It also means that AI-specific logic doesn't have to spread throughout the rest of the application.</p>
<h2>4. DDD and bounded contexts</h2>
<p>The separation between Memory Service and Ask Service wasn't just about splitting the code into smaller services.</p>
<p>I was thinking about the different <strong>domains and responsibilities</strong> involved.</p>
<p>The Memory Service represents the memory domain:</p>
<ul>
<li><p>creating memories</p>
</li>
<li><p>retrieving memories</p>
</li>
<li><p>searching memories</p>
</li>
<li><p>managing memory data</p>
</li>
</ul>
<p>The Ask Service represents the AI interaction:</p>
<ul>
<li><p>receiving a question</p>
</li>
<li><p>deciding what information is needed</p>
</li>
<li><p>requesting relevant memories</p>
</li>
<li><p>constructing context</p>
</li>
<li><p>calling the LLM</p>
</li>
<li><p>generating the answer</p>
</li>
</ul>
<p>They are closely related, but they are not the same responsibility.</p>
<p>This is where the idea of <strong>bounded contexts</strong> from Domain-Driven Design helped me think about the boundaries.</p>
<p>The Ask Service uses the Memory Service, but it doesn't become responsible for managing memory itself.</p>
<h2>5. Encapsulation and API contracts</h2>
<p>The service boundary also creates an important form of encapsulation.</p>
<p>The Ask Service doesn't need to know how the Memory Service stores or searches memories.</p>
<p>It only needs to know what the Memory Service provides through its API.</p>
<p>For example, the Ask Service might ask for relevant memories, without knowing whether the Memory Service uses PostgreSQL, pgvector, a different vector store, or some other implementation in the future.</p>
<p>The API becomes the contract between the services.</p>
<p>This keeps implementation details inside the service that owns them.</p>
<hr />
<p>These principles gave me the initial structure of Second-Memory.</p>
<p>It wasn't a complicated architecture, and that was intentional.</p>
<p>I wanted clear boundaries without creating unnecessary complexity.</p>
<p>With the boundaries in place, I could start looking at the individual technical decisions.</p>
]]></content:encoded></item><item><title><![CDATA[Putting on the Product Owner Hat]]></title><description><![CDATA[Putting on the Product Owner Hat
I had the idea for Second-Memory.
But at that point, it was still just an idea. It was too vague to start thinking about architecture or implementation.
I wanted to de]]></description><link>https://joung.hashnode.dev/putting-on-the-product-owner-hat</link><guid isPermaLink="true">https://joung.hashnode.dev/putting-on-the-product-owner-hat</guid><dc:creator><![CDATA[Joung Park]]></dc:creator><pubDate>Tue, 28 Jul 2026 23:00:00 GMT</pubDate><content:encoded><![CDATA[<h1>Putting on the Product Owner Hat</h1>
<p>I had the idea for Second-Memory.</p>
<p>But at that point, it was still just an idea. It was too vague to start thinking about architecture or implementation.</p>
<p>I wanted to develop the idea further before I started building it.</p>
<p>So I put on the <strong>Product Owner hat</strong>.</p>
<p>I followed the kind of process I had experienced in my professional work.</p>
<p>I started by defining the initiative. I wrote down what I was trying to achieve and what I wanted the product to be.</p>
<p>Then I wrote a PRD.</p>
<p>Writing it made me think through the product in more detail: the problem, the user, the main use cases, the scope, and what I wanted to achieve with the first version.</p>
<p>From there, I created user stories.</p>
<p>This helped me look at the product from the user's perspective rather than thinking about how I was going to implement it.</p>
<p>I then organised the stories into an initial backlog and broke the work down into tasks that I could eventually implement.</p>
<p>There was another reason I wanted to document all of this.</p>
<p><strong>I knew I would be using AI tools to help me build the project.</strong></p>
<p>Instead of explaining the project from scratch every time I asked an AI coding tool for help, I could give it the product documents as context.</p>
<p>The PRD and user stories became a shared reference point — for me as the developer, and for the AI tools I was working with.</p>
<p>The idea started in my head as something fairly vague.</p>
<p>By the end of this process, I had something much more concrete:</p>
<p><strong>an idea → a product definition → user stories → work I could build.</strong></p>
]]></content:encoded></item><item><title><![CDATA[AI Project Idea]]></title><description><![CDATA[I Wanted an AI That Would Just Listen
I wanted to learn how to build AI systems.
So I started looking for a project.
At the time, I was using ChatGPT like everyone else. I would type something, and it]]></description><link>https://joung.hashnode.dev/ai-project-idea</link><guid isPermaLink="true">https://joung.hashnode.dev/ai-project-idea</guid><dc:creator><![CDATA[Joung Park]]></dc:creator><pubDate>Tue, 28 Jul 2026 08:00:00 GMT</pubDate><content:encoded><![CDATA[<h1>I Wanted an AI That Would Just Listen</h1>
<p>I wanted to learn how to build AI systems.</p>
<p>So I started looking for a project.</p>
<p>At the time, I was using ChatGPT like everyone else. I would type something, and it would answer.</p>
<p>That's what makes a chatbot useful.</p>
<p>But I started thinking about something different.</p>
<p>Sometimes I don't want an answer.</p>
<p>Sometimes I don't want advice.</p>
<p>I don't want another question.</p>
<p>I just want to write down what I'm thinking.</p>
<p>A thought.</p>
<p>An idea.</p>
<p>Something that happened today.</p>
<p>A feeling.</p>
<p>Something I'm not even sure is important yet.</p>
<p>Just like talking to myself.</p>
<h2>What if the AI didn't respond?</h2>
<p>Imagine opening an AI application and writing:</p>
<blockquote>
<p>"I've been thinking about learning AI more seriously. I don't know where this will take me, but I feel like I should start building something."</p>
</blockquote>
<p>And then...</p>
<p>Nothing.</p>
<p>No advice.</p>
<p>No suggestions.</p>
<p>No follow-up question.</p>
<p>It just keeps it.</p>
<p>That's it.</p>
<p>Then a few weeks later, I could ask:</p>
<blockquote>
<p>"What have I been thinking about learning AI?"</p>
</blockquote>
<p>And now the system responds.</p>
<p>Not based on a generic conversation.</p>
<p>Not based on what I happened to tell it five minutes ago.</p>
<p>But based on the things <strong>I had previously written down</strong>.</p>
<p>That was the idea behind Second-Memory.</p>
<h2>A different kind of AI interaction</h2>
<p>Most chatbots are built around a simple interaction:</p>
<p><strong>You → AI → You → AI → You</strong></p>
<p>Every message expects a response.</p>
<p>I wanted something different:</p>
<p><strong>Me → Second-Memory</strong></p>
<p><strong>Me → Second-Memory</strong></p>
<p><strong>Me → Second-Memory</strong></p>
<p>...</p>
<p>And only when I have a question:</p>
<p><strong>Me → Second-Memory → Answer based on what I've written</strong></p>
<p>The system isn't there to constantly talk to me.</p>
<p>It's there to remember.</p>
<p>That's where the name came from:</p>
<p><strong>Second-Memory.</strong></p>
<p>A place where I can put the things I might otherwise forget.</p>
<h2>Why build this?</h2>
<p>This started as an AI learning project.</p>
<p>I wanted to understand what it actually takes to build an AI-powered system rather than simply calling an LLM API.</p>
<p>And this idea seemed like a good problem to explore.</p>
<p>How do you store someone's thoughts?</p>
<p>How do you find the relevant ones later?</p>
<p>How do you understand that two thoughts written months apart might be related?</p>
<p>How do you give an AI enough context to answer a question without giving it everything?</p>
<p>How do you know whether the answer is actually grounded in what the person wrote?</p>
<p>I didn't know the answers yet.</p>
<p>I just had the idea.</p>
<p>So I created a repository.</p>
<p>The first commit was:</p>
<blockquote>
<p><strong>second-memory</strong><em>Your second memory - capture today, remember forever</em></p>
</blockquote>
<p>And that's where the project began.</p>
]]></content:encoded></item></channel></rss>