A RAG Q&A tool for personal journals. Ask a question and get an answer grounded in your actual journal entries.
- A journal export (plain text, dated entries) is parsed into individual dated entries
- Each entry is embedded (OpenAI
text-embedding-3-small) and stored in Postgres with thepgvectorextension - A question is embedded the same way, and Postgres finds the most similar entries by cosine distance, optionally filtered to a date range
- The retrieved entries are passed to Claude (Anthropic API) along with the question, which generates an answer
- Everything is served through a FastAPI
/askendpoint, with auth, input validation, rate limiting, and generic error handling in front of it
- API key auth on
/ask, via a dedicated key (not the OpenAI/Anthropic provider keys) - Pydantic request validation (question length cap, real date types — rejects malformed input before it reaches the database or either API)
- Rate limiting (10 requests/minute per IP) to cap runaway API cost
- Generic error responses to callers; full error detail logged server-side only, with entry/question content never logged
- Parameterized SQL throughout (no string-built queries)
- source .venv/bin/activate
- uvicorn main:app --reload
- Create the journal file
- Create a separate database for the journal in postgres
createdb bellie_demo
psql bellie_demo
CREATE EXTENSION vector;
CREATE TABLE entries (
id SERIAL PRIMARY KEY,
entry_date DATE NOT NULL,
content TEXT NOT NULL,
embedding vector(1536)
);
\q
- Point ingest.py at the new file and new database.
DB_NAME=bellie_demo python3 -c "
from parser import parse_journal
import ingest
with open('synthetic_journal.txt') as f:
raw = f.read()
entries = parse_journal(raw, year=2026)
print(f'Parsed {len(entries)} synthetic entries')
"
- Verify count psql bellie_demo SELECT COUNT(*) FROM entries;
https://bellie-production.up.railway.app/health https://bellie-production.up.railway.app/docs#
brew install railwayrailway login- Code pushed to GitHub
Procfilepresent in the repo root: web: uvicorn main:app --host 0.0.0.0 --port $PORT
- Go to railway.app → New Project → Deploy from GitHub repo → select the repo
railway service # select "Postgres"
railway variables # copy the DATABASE_URL value shown — you'll need it in step 4
railway connect PostgresFirst time connecting may require an SSH key:
ssh-keygen -t ed25519(press Enter to accept the default path~/.ssh/id_ed25519— don't rename it)
Inside the psql session that opens:
CREATE EXTENSION vector;
CREATE TABLE entries (
id SERIAL PRIMARY KEY,
entry_date DATE NOT NULL,
content TEXT NOT NULL,
embedding vector(1536)
);
\qrailway service # select "Bellie" (not Postgres) — all remaining commands target this
⚠️ Railway's UI suggests referencing${{Postgres.DATABASE_PRIVATE_URL}}— this does not work, that variable doesn't exist on the Postgres service. Use the literalDATABASE_URLstring copied in step 2 instead.
railway variables --set "DATABASE_URL=<paste the connection string from step 2>"
railway variables --set "OPENAI_API_KEY=..."
railway variables --set "ANTHROPIC_API_KEY=..."
railway variables --set "EMBEDDING_MODEL=text-embedding-3-small"
railway variables --set "CLAUDE_MODEL=claude-sonnet-4-5"
railway variables --set "MAX_TOKENS=500"
railway variables --set "BELLIE_API_KEY=..."Verify with railway variables — values should show as real strings, not empty.
railway up
⚠️ ingest.pycannot be run from your laptop against Railway's database —postgres.railway.internalonly resolves inside Railway's private network. Userailway sshto run it from inside that network instead:
railway sshThen, inside that shell (now running on Railway):
JOURNAL_FILE=synthetic_journal.txt python3 ingest.py- Get the public URL from the app service's Settings → Networking (generate a domain if none exists)
curl https://YOUR-URL.up.railway.app/health
# expect: {"status":"ok"}- Test
/askviahttps://YOUR-URL.up.railway.app/docs→ click Authorize, pasteBELLIE_API_KEY→ try a question
Project Settings → Danger Zone → Delete Project. This removes the app + database together. Code stays safe in GitHub — redeploy anytime by repeating this runbook.
