This is a submission for the Hacktoberfest Weekend Challenge: Build for a Friend
This is a submission for the Hacktoberfest Weekend Challenge: Build for a Friend
What I Built
Popo wrote his journal in notebooks for years. Then he stopped.
It wasn't that he had nothing to say. Journaling felt one-sided. He logged his thoughts every night and never really sat with them. The page took everything in and never said a word back. And when he wanted to look back at old entries, the only option was flipping through months of his own handwriting.
I wanted to fix that without losing the one thing a notebook gets right: nobody else reads it.
Pepal (Your Penpal) is a journal that writes back. It looks like a physical notebook, and it does three things:
- It asks, so the page isn't silent. Each day you get three reflection questions tailored to your recent mood, so you sit with your thoughts instead of only dumping them.
- It stays on your own machine. No account, no cloud, no API key. The entries, the database, and the AI all run locally on Popo's system. Not even I can read it.
- You can talk to your past instead of scrolling it. Ask "when did I last feel like this?" and it answers from your own entries, and only your own entries.
When I showed it to Popo, I asked what he thought. He said he wouldn't tell me, he'd write it in the journal. LOL
I took that as the review.
Demo
Screenshot uses sample text. Popo's real journal stays on his machine.
Pepal is local-first, so there's no public link to click. With Docker, it runs in three commands:
git clone https://github.com/An16og/pepal-Your-Pen-Pal
cd pepal-Your-Pen-Pal
docker compose up -d --build
Open http://localhost:8080. The app, Postgres with pgvector, and Ollama all start automatically. On Windows, double-click start.bat.
Code
An16og
/
pepal-Your-Pen-Pal
Pepal: Your Penpal - 100% offline, local-first AI physical journal companion. Built with Spring Boot 3, Spring AI, PostgreSQL (pgvector), Ollama, and Docker.
Pepal: Your Penpal
A 100% local, offline, physical-style AI journal web app. Built with Spring Boot 3, Spring AI, PostgreSQL (pgvector), Flyway, and vanilla HTML/CSS/JS.
Features
-
2-Table Architecture: Normalized PostgreSQL storage separating
journal_entrymetadata from denseentry_embedding768-dimensional vectors. -
Personality System & Settings: Persona presets (
GENTLE,PLAYFUL,BLUNT,COACH,CUSTOM) and language options (EN,HINGLISH) composed dynamically with a strict, non-negotiable Safety Layer. - Dynamic Mood-Aware Daily Prompts: 3 AI-tailored reflection questions with deterministic static fallback and per-date reshuffling.
-
Full Entry Lifecycle: Create, edit (
PUT /api/entries/{id}), soft-delete (DELETE /api/entries/{id}), trash listing (GET /api/trash), conflict-aware restore (POST /api/entries/{id}/restore), permanent delete, and daily automated purge job. - Max 1 Daily Reflection per Day: Prompted with 3 questions. Enforced via partial unique index and Spring Boot service validation.
- Freeform Journaling: Add unlimitedβ¦
How I Built It
Browser (vanilla HTML/CSS/JS)
β
Spring Boot 3 βββΊ Spring AI βββΊ Ollama: gemma3:1b (chat) + nomic-embed-text (768-dim embeddings)
β
PostgreSQL 16 + pgvector (HNSW, cosine similarity)
ββ journal_entry (text, mood, energy, date, trash state)
ββ entry_embedding (768-dim vector)
The models. Chat runs on Gemma 3 1B through Ollama. Embeddings come from nomic-embed-text, which turns each entry into a 768-dimensional vector. Both run locally.
The database. Entry metadata lives in journal_entry, and the vectors live in a separate entry_embedding table. Listing your history never drags vectors around, and similarity search runs against an HNSW cosine index (<=>) without touching the rest of the row.
Mood is part of the memory. The Energy (High/Low) and Mood (Good/Bad) you pick are embedded along with the text, not just shown as badges. So when you ask the penpal about a feeling, it can find entries by how the day felt, not only by what you wrote.
The frontend. Plain HTML, CSS, and JavaScript with the Fetch API. No framework, because a journal should open fast and have nothing to learn.
Decisions I made for Popo
One reflection a day, enforced twice. Popo wanted to sit with his thoughts, not drown in a to-do list. So there's a single daily reflection (three questions), and every other entry is freeform and unlimited. The rule is checked in the service layer and again by a partial unique index in Postgres, so no double-click or future bug can slip in a second one.
The app has to work when the model doesn't. Daily prompts are AI-generated and mood-aware, but there's a deterministic fallback with per-date reshuffling. If Ollama is still loading or busy, Popo still gets three good questions. A journal that opens empty at night is worse than a plain one.
A personality, under a fixed safety layer. The penpal can be GENTLE, PLAYFUL, BLUNT, COACH, or CUSTOM, in English or Hinglish. Whatever is chosen is composed on top of a non-negotiable Safety Layer that no preset can override. A journal is where people write their worst nights, and a persona should change the tone, never the care.
Deleting is reversible. Entries go to a trash with a "days remaining" counter and a daily purge job. Because you can backfill past dates, restoring an old reflection can clash with one you've since written for that day. Restore is conflict-aware and returns a 409 instead of silently breaking the one-a-day rule.
Swapping to Gemma
The app started on llama3.2. I switched the chat model to gemma3:1b, and because the model is configuration and not architecture, the app didn't need to be redesigned around it. [ADD ONE TRUE SENTENCE: what the swap involved, e.g. "It was a one-line config change" or "I had to adjust X".]
Why Does Open Innovation Matter?
Because Popo's journal never leaves his system. A hosted model would need every entry sent to someone else's server to be embedded and answered. Here the entry goes from the browser to Postgres on his own machine, and the vector is computed on his own CPU. For something he writes at his most honest, that was the whole point. He was never going to tell me what he'd write. Now there is genuinely no server I could read it from, even if I wanted to.
Because it costs nothing, forever. No API key, no per-token bill, no subscription. A journal is a daily habit for years, and metered pricing punishes exactly that.
Because I could swap the model. Moving from Llama to Gemma changed one piece of the stack, not the product. If Popo gets a stronger laptop, a bigger open model is a config change away.
Because I can see where the model ends and my rules begin. The persona text, the safety layer, and the retrieval query are code and config I can read and edit, and GET /api/settings/preview returns the exact system prompt before the penpal ever speaks.
Because his entries can't be taken away. The models, the vectors, and the entries all live in Docker volumes (pepal_data, pepal_ollama) on his machine. If a company deprecates a model or changes its pricing, years of his writing keep working.
Where it lost. A 1B model is small, and a hosted frontier model will write more eloquent replies. [VERIFY: if you saw it give a clunky or off-topic answer, add one example here.] For a journal opened once a day, I'll take a plainer reply and total privacy.
Handing It Over
Popo uses Pepal on his own system, every day. When I first showed it to him, he said he wouldn't tell me what he'd write. He'd write it in the journal.
That sentence is the reason it runs entirely on his machine. He trusts a journal more than a friend, and a hosted app could never have earned that trust. [ADD ONE TRUE LINE IF YOU CAN: what he did after, e.g. which question he answered first.]
My Agent Session
[ADD 2 TO 3 TRUE SENTENCES: which AI tool you built with (if any), and one decision you made, e.g. keeping everything local, the one-reflection-per-day rule, or putting the safety layer above CUSTOM prompts. If you didn't use an agent, say how you built it.]
Prize Categories
- Best Use of Gemma. Pepal runs Gemma 3 1B locally through Ollama as its chat model, powering the daily reflection questions, the persona-driven replies, and the retrieval-grounded penpal.
Built for Popo. If you've stopped journaling because the page never answers back, or because you don't trust where the words go, the code is yours. Clone it, run it, and let it write back.

Top comments (0)