DEV Community

Venkat Ambati
Venkat Ambati

Posted on

I Built a RAG-Powered “Second Brain” and Accidentally Created My Personal Research Assistant

The Problem: Information Overload Is Real

You know that feeling when you're in a meeting and someone asks, "Didn't we read something about that last month?" And you're like... "Yeah... somewhere... maybe... it was in a PDF... or a URL... or perhaps I dreamed it?"

Or when you have a brilliant insight in the shower, promise yourself you'll remember it, and by the time you're dressed, it's gone forever?

That's been my life for years. I have bookmarks dating back to 2015 that I'll "definitely read later," and random thoughts scattered across 17 different note apps.

So I built Memory Palace — a personal knowledge management system that handles BOTH your external research (PDFs, articles, documents) AND your internal thoughts (notes, ideas, memories). Think of it as NotebookLM meets Obsidian, but open source, self-hostable, and powered by whatever LLM you want (local or cloud).


What Is Memory Palace?

Memory Palace is a RAG (Retrieval-Augmented Generation) powered knowledge management system with a dual brain:

📚 Pockets (External Knowledge):

  1. Save external sources — URLs, PDFs, Word docs, text files
  2. Auto-processing — Extracts text, chunks it, creates embeddings
  3. Ask research questions — "What did that paper say about momentum investing?"
  4. Get cited answers — No hallucinations, just facts from YOUR sources

🧠 Memories (Internal Knowledge):

  1. Save personal thoughts — Notes, ideas, insights, reflections
  2. Rich formatting — Markdown support with colors and tags
  3. Ask about your thinking — "What was my take on that marketing strategy?"
  4. Connect the dots — AI helps link your thoughts across time

It's like having both a research assistant who has actually read all your documents AND a personal memory that never forgets what you were thinking.

Real-World Use Cases

For Research (Pockets):

  • Researchers: Save papers, ask "Compare the methodologies across my sources"
  • Students: Upload course materials, ask "Explain quantum entanglement using my lecture notes"
  • Professionals: Save industry reports, ask "What are the key trends from Q3 reports?"
  • Content Creators: Save research, ask "Give me statistics about AI adoption"

For Personal Knowledge (Memories):

  • Entrepreneurs: Save business ideas, ask "What were my thoughts on the subscription model?"
  • Writers: Save story concepts, ask "Find all my character development notes"
  • Learners: Save insights, ask "What did I learn from that productivity experiment?"
  • Decision Makers: Save pros/cons lists, ask "Why did I decide against that opportunity?"
  • Everyone: Finally remember those shower thoughts and 3am insights!

Combined Power:

  • Ask "How does this research paper relate to my previous thoughts on AI ethics?"
  • Connect external knowledge with personal insights seamlessly

The Tech Stack (For Nerds)

Here's what powers this beast:

  • Frontend: Next.js 14 + Tailwind CSS — Fast, pretty, server components
  • API: Fastify + TypeScript — Speed demon API framework
  • Worker: BullMQ + Playwright — Background processing with headless browser
  • Database: Supabase (PostgreSQL + pgvector) — Vector search + SQL = ❤️
  • LLM: OpenRouter (or local via Ollama) — Model flexibility
  • Hosting: Railway + Vercel — Deploy in minutes

Architecture Overview

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│                 │     │                 │     │                 │
│   Next.js Web   │────▶│   Fastify API   │────▶│   BullMQ Worker │
│   (Vercel)      │     │   (Railway)     │     │   (Railway)     │
│                 │     │                 │     │                 │
└─────────────────┘     └────────┬────────┘     └────────┬────────┘
                                 │                       │
                                 ▼                       ▼
                        ┌─────────────────┐     ┌─────────────────┐
                        │                 │     │                 │
                        │    Supabase     │     │    OpenRouter   │
                        │  (PostgreSQL +  │     │   (LLM + Embed) │
                        │    pgvector)    │     │                 │
                        └─────────────────┘     └─────────────────┘
Enter fullscreen mode Exit fullscreen mode

GitHub Repositories

Here are the three repos that make up Memory Palace:

vedha-pocket-web — Next.js frontend with dual chat UI (pockets + memories)
👉 https://github.com/venki0552/vedha-pocket-web

vedha-pocket-api — Fastify API with RAG pipeline for both document and memory search
👉 https://github.com/venki0552/vedha-pocket-api

vedha-pocket-worker — Background worker with Playwright for document processing
👉 https://github.com/venki0552/vedha-pocket-worker


Key Features

🔍 Hybrid Search (Vector + Full-Text)

Not just semantic search. We combine vector similarity with PostgreSQL full-text search for the best of both worlds. Works across both documents and memories.

🌊 Streaming Responses

Answers stream in real-time, token by token. No waiting 10 seconds staring at a spinner. Real-time status updates during search.

📚 Multi-Query RAG

The system generates multiple search queries from your question for better retrieval. Ask "What are the risks?" and it also searches for "dangers," "downsides," and "limitations."

🧠 Dual Knowledge Base

Pockets for external sources (PDFs, URLs) and Memories for personal notes. Ask questions across both or search each separately.

🎨 Rich Memory Management

  • Markdown rendering with bold, italics, lists, links
  • Color coding for visual organization
  • Tag system for categorization
  • Resizable interface - adjust memory/chat panel sizes

🤖 Bot Detection

The worker detects when websites block scrapers (CloudFlare, access denied, etc.) and fails gracefully instead of saving junk.

📄 PDF Page-Level Chunking

PDFs are chunked by page with page numbers preserved. Citations tell you exactly which page to check.

🔐 Multi-Tenant Security

Full Row Level Security (RLS) with organizations and memberships. Share pockets with your team. Complete privacy with encrypted API keys.

🏠 Local LLM Support

Set LLM_BASE_URL to your Ollama/LM Studio/vLLM instance and use local models. Full control over your AI.

⚡ Performance & UX

  • Fallback model support - automatic retry with different models
  • Free model defaults - uses Google Gemma for cost-effective operation
  • Vercel Analytics - privacy-focused performance tracking
  • Responsive design - works great on all devices

How to Run Locally

Prerequisites

  • Node.js 20+
  • Docker (for Redis)
  • Supabase account
  • OpenRouter API key (or local LLM)

1. Clone the Repos

git clone https://github.com/venki0552/vedha-pocket-web.git
git clone https://github.com/venki0552/vedha-pocket-api.git
git clone https://github.com/venki0552/vedha-pocket-worker.git
Enter fullscreen mode Exit fullscreen mode

2. Start Redis

docker run -d --name redis -p 6379:6379 redis:alpine
Enter fullscreen mode Exit fullscreen mode

3. Set Up Environment Variables

vedha-pocket-api/.env

PORT=3001
NODE_ENV=development

# Supabase
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key

# Redis
REDIS_URL=redis://localhost:6379

# OpenRouter
OPENROUTER_API_KEY=your-openrouter-key
OPENROUTER_BASE_URL=https://openrouter.ai/api/v1
OPENROUTER_EMBED_MODEL=openai/text-embedding-3-large
OPENROUTER_CHAT_MODEL=anthropic/claude-sonnet-4

# For local LLM (optional)
# LLM_BASE_URL=http://localhost:11434/v1
Enter fullscreen mode Exit fullscreen mode

vedha-pocket-worker/.env

SUPABASE_URL=https://your-project.supabase.co
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
REDIS_URL=redis://localhost:6379
OPENROUTER_API_KEY=your-openrouter-key
OPENROUTER_BASE_URL=https://openrouter.ai/api/v1
OPENROUTER_EMBED_MODEL=openai/text-embedding-3-large
PLAYWRIGHT_ENABLED=true
WORKER_CONCURRENCY=5
Enter fullscreen mode Exit fullscreen mode

vedha-pocket-web/.env.local

NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
NEXT_PUBLIC_API_URL=http://localhost:3001
Enter fullscreen mode Exit fullscreen mode

4. Install Dependencies & Run

# Terminal 1 - API
cd vedha-pocket-api
npm install
npm run dev

# Terminal 2 - Worker
cd vedha-pocket-worker
npm install
npx playwright install chromium
npm run dev

# Terminal 3 - Web
cd vedha-pocket-web
npm install
npm run dev
Enter fullscreen mode Exit fullscreen mode

5. Open the App

Visit http://localhost:3000 — Sign up, create a pocket, add some sources!


Deployment Guide

Step 1: Supabase Setup

  1. Create a new project at supabase.com
  2. Enable the Vector extension:
CREATE EXTENSION IF NOT EXISTS vector;
Enter fullscreen mode Exit fullscreen mode
  1. Run the database migrations. Here's the core schema:
-- Organizations
CREATE TABLE orgs (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name TEXT NOT NULL,
    slug TEXT UNIQUE,
    owner_id UUID REFERENCES auth.users(id) NOT NULL,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Memberships
CREATE TABLE memberships (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    org_id UUID REFERENCES orgs(id) ON DELETE CASCADE NOT NULL,
    user_id UUID REFERENCES auth.users(id) NOT NULL,
    role TEXT NOT NULL DEFAULT 'member',
    created_at TIMESTAMPTZ DEFAULT NOW(),
    UNIQUE(org_id, user_id)
);

-- Pockets (Knowledge Collections)
CREATE TABLE pockets (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    org_id UUID REFERENCES orgs(id) ON DELETE CASCADE NOT NULL,
    name TEXT NOT NULL,
    description TEXT,
    created_by UUID REFERENCES auth.users(id) NOT NULL,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Pocket Members
CREATE TABLE pocket_members (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    pocket_id UUID REFERENCES pockets(id) ON DELETE CASCADE NOT NULL,
    org_id UUID REFERENCES orgs(id) ON DELETE CASCADE NOT NULL,
    user_id UUID REFERENCES auth.users(id) NOT NULL,
    role TEXT NOT NULL DEFAULT 'viewer',
    created_at TIMESTAMPTZ DEFAULT NOW(),
    UNIQUE(pocket_id, user_id)
);

-- Sources (URLs, PDFs, etc.)
CREATE TABLE sources (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    org_id UUID REFERENCES orgs(id) ON DELETE CASCADE NOT NULL,
    pocket_id UUID REFERENCES pockets(id) ON DELETE CASCADE NOT NULL,
    type TEXT NOT NULL,
    title TEXT NOT NULL,
    url TEXT,
    storage_path TEXT,
    mime_type TEXT NOT NULL,
    size_bytes BIGINT,
    status TEXT NOT NULL DEFAULT 'queued',
    error_message TEXT,
    created_by UUID REFERENCES auth.users(id) NOT NULL,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Chunks (Text segments with embeddings)
CREATE TABLE chunks (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    org_id UUID REFERENCES orgs(id) ON DELETE CASCADE NOT NULL,
    pocket_id UUID REFERENCES pockets(id) ON DELETE CASCADE NOT NULL,
    source_id UUID REFERENCES sources(id) ON DELETE CASCADE NOT NULL,
    idx INTEGER NOT NULL,
    page INTEGER,
    text TEXT NOT NULL,
    content_hash TEXT NOT NULL,
    embedding VECTOR(3072),
    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Conversations
CREATE TABLE conversations (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    org_id UUID REFERENCES orgs(id) ON DELETE CASCADE NOT NULL,
    pocket_id UUID REFERENCES pockets(id) ON DELETE CASCADE NOT NULL,
    created_by UUID REFERENCES auth.users(id) NOT NULL,
    title TEXT,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Messages
CREATE TABLE messages (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    org_id UUID REFERENCES orgs(id) ON DELETE CASCADE NOT NULL,
    pocket_id UUID REFERENCES pockets(id) ON DELETE CASCADE NOT NULL,
    conversation_id UUID REFERENCES conversations(id) ON DELETE CASCADE NOT NULL,
    role TEXT NOT NULL,
    content TEXT NOT NULL,
    citations JSONB,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Memories (Personal Notes/Thoughts)
CREATE TABLE memories (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    org_id UUID REFERENCES orgs(id) ON DELETE CASCADE NOT NULL,
    user_id UUID REFERENCES auth.users(id) NOT NULL,
    title TEXT NOT NULL,
    content TEXT NOT NULL,
    content_html TEXT,
    color TEXT DEFAULT 'default',
    tags TEXT[] DEFAULT '{}',
    status TEXT NOT NULL DEFAULT 'active',
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Memory Chunks (For AI search)
CREATE TABLE memory_chunks (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    org_id UUID REFERENCES orgs(id) ON DELETE CASCADE NOT NULL,
    memory_id UUID REFERENCES memories(id) ON DELETE CASCADE NOT NULL,
    user_id UUID REFERENCES auth.users(id) NOT NULL,
    idx INTEGER NOT NULL,
    text TEXT NOT NULL,
    content_hash TEXT NOT NULL,
    embedding VECTOR(1536),
    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- General Conversations (For Memory Chat)
CREATE TABLE general_conversations (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    org_id UUID REFERENCES orgs(id) ON DELETE CASCADE NOT NULL,
    user_id UUID REFERENCES auth.users(id) NOT NULL,
    title TEXT,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- General Messages (For Memory Chat)
CREATE TABLE general_messages (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    org_id UUID REFERENCES orgs(id) ON DELETE CASCADE NOT NULL,
    user_id UUID REFERENCES auth.users(id) NOT NULL,
    conversation_id UUID REFERENCES general_conversations(id) ON DELETE CASCADE NOT NULL,
    role TEXT NOT NULL,
    content TEXT NOT NULL,
    citations JSONB,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- User Settings
CREATE TABLE user_settings (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID REFERENCES auth.users(id) UNIQUE NOT NULL,
    theme TEXT DEFAULT 'system',
    llm_preference TEXT DEFAULT 'openrouter',
    openrouter_api_key_encrypted TEXT,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Indexes for performance
CREATE INDEX idx_chunks_pocket_id ON chunks(pocket_id);
CREATE INDEX idx_chunks_source_id ON chunks(source_id);
CREATE INDEX idx_chunks_embedding ON chunks USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);
CREATE INDEX idx_chunks_text_fts ON chunks USING gin(to_tsvector('english', text));

-- Memory indexes
CREATE INDEX idx_memory_chunks_memory_id ON memory_chunks(memory_id);
CREATE INDEX idx_memory_chunks_user_id ON memory_chunks(user_id);
CREATE INDEX idx_memory_chunks_embedding ON memory_chunks USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);
CREATE INDEX idx_memory_chunks_text_fts ON memory_chunks USING gin(to_tsvector('english', text));
CREATE INDEX idx_memories_user_id ON memories(user_id);
CREATE INDEX idx_memories_status ON memories(status) WHERE status = 'active';

-- Hybrid Search Function
CREATE OR REPLACE FUNCTION hybrid_search(
    query_embedding VECTOR(3072),
    query_text TEXT,
    target_pocket_id UUID,
    match_count INT DEFAULT 10,
    vector_weight FLOAT DEFAULT 0.7,
    fts_weight FLOAT DEFAULT 0.3
)
RETURNS TABLE (
    id UUID,
    chunk_id UUID,
    source_id UUID,
    source_title TEXT,
    source_type TEXT,
    page INT,
    text TEXT,
    similarity FLOAT,
    combined_score FLOAT
)
LANGUAGE plpgsql
AS $$
BEGIN
    RETURN QUERY
    WITH vector_results AS (
        SELECT
            c.id,
            c.source_id,
            s.title as source_title,
            s.type as source_type,
            c.page,
            c.text,
            1 - (c.embedding <=> query_embedding) as similarity
        FROM chunks c
        JOIN sources s ON s.id = c.source_id
        WHERE c.pocket_id = target_pocket_id
        ORDER BY c.embedding <=> query_embedding
        LIMIT match_count * 2
    ),
    fts_results AS (
        SELECT
            c.id,
            c.source_id,
            s.title as source_title,
            s.type as source_type,
            c.page,
            c.text,
            ts_rank(to_tsvector('english', c.text), plainto_tsquery('english', query_text)) as rank
        FROM chunks c
        JOIN sources s ON s.id = c.source_id
        WHERE c.pocket_id = target_pocket_id
        AND to_tsvector('english', c.text) @@ plainto_tsquery('english', query_text)
        ORDER BY rank DESC
        LIMIT match_count * 2
    ),
    combined AS (
        SELECT
            COALESCE(v.id, f.id) as id,
            COALESCE(v.source_id, f.source_id) as source_id,
            COALESCE(v.source_title, f.source_title) as source_title,
            COALESCE(v.source_type, f.source_type) as source_type,
            COALESCE(v.page, f.page) as page,
            COALESCE(v.text, f.text) as text,
            COALESCE(v.similarity, 0) as similarity,
            (COALESCE(v.similarity, 0) * vector_weight) +
            (COALESCE(f.rank, 0) * fts_weight) as combined_score
        FROM vector_results v
        FULL OUTER JOIN fts_results f ON v.id = f.id
    )
    SELECT
        combined.id,
        combined.id as chunk_id,
        combined.source_id,
        combined.source_title,
        combined.source_type,
        combined.page,
        combined.text,
        combined.similarity,
        combined.combined_score
    FROM combined
    ORDER BY combined_score DESC
    LIMIT match_count;
END;
$$;

-- Memory Hybrid Search Function
CREATE OR REPLACE FUNCTION memory_hybrid_search(
    query_embedding VECTOR(1536),
    query_text TEXT,
    target_user_id UUID,
    match_count INT DEFAULT 10,
    vector_weight FLOAT DEFAULT 0.7,
    fts_weight FLOAT DEFAULT 0.3
)
RETURNS TABLE (
    id UUID,
    memory_id UUID,
    memory_title TEXT,
    text TEXT,
    similarity FLOAT,
    combined_score FLOAT
)
LANGUAGE plpgsql
AS $$
BEGIN
    RETURN QUERY
    WITH vector_results AS (
        SELECT
            mc.id,
            mc.memory_id,
            m.title as memory_title,
            mc.text,
            1 - (mc.embedding <=> query_embedding) as similarity
        FROM memory_chunks mc
        JOIN memories m ON m.id = mc.memory_id
        WHERE mc.user_id = target_user_id
        AND m.status = 'active'
        ORDER BY mc.embedding <=> query_embedding
        LIMIT match_count * 2
    ),
    fts_results AS (
        SELECT
            mc.id,
            mc.memory_id,
            m.title as memory_title,
            mc.text,
            ts_rank(to_tsvector('english', mc.text), plainto_tsquery('english', query_text)) as rank
        FROM memory_chunks mc
        JOIN memories m ON m.id = mc.memory_id
        WHERE mc.user_id = target_user_id
        AND m.status = 'active'
        AND to_tsvector('english', mc.text) @@ plainto_tsquery('english', query_text)
        ORDER BY rank DESC
        LIMIT match_count * 2
    ),
    combined AS (
        SELECT
            COALESCE(v.id, f.id) as id,
            COALESCE(v.memory_id, f.memory_id) as memory_id,
            COALESCE(v.memory_title, f.memory_title) as memory_title,
            COALESCE(v.text, f.text) as text,
            COALESCE(v.similarity, 0) as similarity,
            (COALESCE(v.similarity, 0) * vector_weight) +
            (COALESCE(f.rank, 0) * fts_weight) as combined_score
        FROM vector_results v
        FULL OUTER JOIN fts_results f ON v.id = f.id
    )
    SELECT
        combined.id,
        combined.memory_id,
        combined.memory_title,
        combined.text,
        combined.similarity,
        combined.combined_score
    FROM combined
    ORDER BY combined_score DESC
    LIMIT match_count;
END;
$$;

-- Auto-create org on signup
CREATE OR REPLACE FUNCTION handle_new_user()
RETURNS TRIGGER AS $$
DECLARE
    v_org_id UUID;
BEGIN
    INSERT INTO public.orgs (name, slug, owner_id)
    VALUES ('Personal', 'personal-' || substring(NEW.id::text from 1 for 8), NEW.id)
    RETURNING id INTO v_org_id;

    INSERT INTO public.memberships (org_id, user_id, role)
    VALUES (v_org_id, NEW.id, 'owner');

    RETURN NEW;
EXCEPTION
    WHEN OTHERS THEN
        RAISE WARNING 'Failed to create org for user %: %', NEW.id, SQLERRM;
        RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER on_auth_user_created
    AFTER INSERT ON auth.users
    FOR EACH ROW
    EXECUTE FUNCTION handle_new_user();
Enter fullscreen mode Exit fullscreen mode
  1. Enable Row Level Security (RLS) on all tables
  2. Create a storage bucket called sources for file uploads

Step 2: OpenRouter Setup

  1. Create account at openrouter.ai
  2. Add credits ($5 gets you pretty far)
  3. Create an API key
  4. Note: Embedding costs ~$0.0001 per 1K tokens, Chat varies by model

Step 3: Railway Deployment

Railway makes deploying Docker containers dead simple.

  1. Create account at railway.app
  2. Create a new project
  3. Add a Redis service (click "+ New" → "Database" → "Redis")

Deploy the API:

  1. Click "+ New" → "GitHub Repo"
  2. Select vedha-pocket-api
  3. Add environment variables (from .env above)
  4. Railway auto-detects Dockerfile and deploys

Deploy the Worker:

  1. Click "+ New" → "GitHub Repo"
  2. Select vedha-pocket-worker
  3. Add environment variables
  4. Deploy!

Step 4: Vercel Deployment

  1. Connect your GitHub to Vercel
  2. Import vedha-pocket-web
  3. Add environment variables:
    • NEXT_PUBLIC_SUPABASE_URL
    • NEXT_PUBLIC_SUPABASE_ANON_KEY
    • NEXT_PUBLIC_API_URL (your Railway API URL)
  4. Deploy!

Docker Compose (One-Click Local Setup)

Want to run everything locally with one command? Here's a docker-compose file:

# docker-compose.yml
version: "3.8"

services:
  redis:
    image: redis:alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data

  api:
    build: ./vedha-pocket-api
    ports:
      - "3001:3001"
    environment:
      - PORT=3001
      - NODE_ENV=production
      - SUPABASE_URL=${SUPABASE_URL}
      - SUPABASE_ANON_KEY=${SUPABASE_ANON_KEY}
      - SUPABASE_SERVICE_ROLE_KEY=${SUPABASE_SERVICE_ROLE_KEY}
      - REDIS_URL=redis://redis:6379
      - OPENROUTER_API_KEY=${OPENROUTER_API_KEY}
      - OPENROUTER_BASE_URL=https://openrouter.ai/api/v1
      - OPENROUTER_EMBED_MODEL=openai/text-embedding-3-large
      - OPENROUTER_CHAT_MODEL=anthropic/claude-sonnet-4
    depends_on:
      - redis

  worker:
    build: ./vedha-pocket-worker
    environment:
      - NODE_ENV=production
      - SUPABASE_URL=${SUPABASE_URL}
      - SUPABASE_SERVICE_ROLE_KEY=${SUPABASE_SERVICE_ROLE_KEY}
      - REDIS_URL=redis://redis:6379
      - OPENROUTER_API_KEY=${OPENROUTER_API_KEY}
      - OPENROUTER_BASE_URL=https://openrouter.ai/api/v1
      - OPENROUTER_EMBED_MODEL=openai/text-embedding-3-large
      - PLAYWRIGHT_ENABLED=true
      - WORKER_CONCURRENCY=5
    depends_on:
      - redis

volumes:
  redis_data:
Enter fullscreen mode Exit fullscreen mode

Run it:

# Create .env file with your secrets
cp .env.example .env
# Edit .env with your values

# Start everything
docker-compose up -d

# Check logs
docker-compose logs -f
Enter fullscreen mode Exit fullscreen mode

The Funny Bits (Lessons Learned)

1. The Bot Detection Saga

First version of the scraper successfully saved... CloudFlare challenge pages. Very useful. "According to your sources, JavaScript is required to view this content."

Now we check for phrases like "Please enable JavaScript" and "Access Denied" before declaring victory.

2. The Infinite Recursion Incident

Row Level Security is great until your policies check table A which checks table B which checks table A. PostgreSQL was NOT amused. Spent 3 hours debugging "infinite recursion detected in policy."

Moral: Don't let your security policies play ping-pong.

3. The UUID Citation Disaster

LLM was citing sources like: "According to [0d1ca55c-8278-44b3-aedf-a5fada07bb97]..."

Very helpful for robots. Less so for humans. Fixed it to use [Source 1] instead.

4. The "Why Won't It Scroll" Mystery

Chat messages wouldn't scroll. Tried everything. Turns out flex and overflow have a complicated relationship. CSS: keeping developers humble since 1996.


What's Next?

Some ideas brewing:

For Memories:

  • [ ] Daily/Weekly Reviews — AI-powered reflection prompts
  • [ ] Memory Connections — Auto-suggest related thoughts
  • [ ] Voice Memos — Speak your thoughts, auto-transcribed
  • [ ] Memory Maps — Visual connections between ideas

For Pockets:

  • [ ] Chrome Extension — Save pages with one click
  • [ ] Smart Summaries — Auto-generate document summaries
  • [ ] Multi-modal — Images, audio, video support

For Both:

  • [ ] Mobile App — Capture thoughts and research on the go
  • [ ] Slack Integration — Ask questions in Slack
  • [ ] Team Collaboration — Share memories and pockets
  • [ ] Automated Insights — Weekly "what you learned" reports

Try It Out!

🌐 Live Demo: https://vedha-pocket-web.vercel.app/

⚠️ Note: This is a BYOK (Bring Your Own Key) app. After signing up, you'll need to add your OpenRouter API key in Settings to use AI features. No subscription required — you only pay for what you use!

🆓 Free tier available: The app uses free models by default (Google Gemma), so you can try it out with minimal costs.

The project is fully open source. Clone it, deploy it, break it, fix it, make it yours.

GitHub Repos:

Tech Stack:

  • Frontend: Next.js 14, Tailwind CSS, shadcn/ui
  • Backend: Fastify, TypeScript, BullMQ
  • Database: Supabase (PostgreSQL + pgvector)
  • LLM: OpenRouter (Claude, GPT-4, etc.) or local (Ollama)

Final Thoughts

Building Memory Palace taught me that RAG isn't magic — it's chunking, embedding, and good prompts. But adding personal memory management revealed new challenges:

Technical challenges:

  1. Extracting clean text — PDFs are still nightmares
  2. Chunking intelligently — Too small = no context, too big = irrelevant
  3. Not hallucinating — LLMs love to make stuff up
  4. Making it fast — Streaming and multi-query search save UX

UX challenges:

  1. Dual context switching — Users need to think "documents" vs "memories"
  2. Information architecture — How do you organize both research and thoughts?
  3. Privacy concerns — Personal memories are more sensitive than research papers
  4. Cross-pollination — Connecting external knowledge with internal insights

The most surprising insight? People don't just want to remember what they read — they want to remember what they thought about what they read. That's where the real value lies.

If you build something cool with this, let me know! And if you find bugs, well... that's what issues are for. 😄


Built with ❤️, ☕, and way too many hours debugging RLS policies.

— Venkat

Top comments (0)