Building Dream Journal: a local-first journaling app built for privacy and low friction
Why I built Dream Journal, why IndexedDB and Dexie made sense, and the product and architecture tradeoffs that came from keeping the app private, local, and intentionally small.
A dream journal is one of those products that gets worse the moment it feels too heavy. If recording a dream takes too many steps, asks you to sign in, or feels like maintaining a life dashboard before you are fully awake, the habit probably dies there.
Dream Journal came from wanting the opposite experience: something calm, private, and simple enough to use half-awake. I wanted to capture dreams quickly, mark the days I did not remember one, and notice patterns over time without turning the whole thing into a productivity tool.
The product boundary came first
The most important decision was what not to build.
I did not want Dream Journal to become:
- a generic journaling platform
- a wellness dashboard
- an AI dream interpretation tool
- a cloud service with a note-taking feature attached
Those are all valid products. They were just not the one I wanted to make.
The core promise was much smaller: understand your dreams and rest patterns without overthinking them. That led to a different tone and a different architecture. The app needed to feel calm, private, and low-pressure, which meant the product boundary had to stay sharp.
Why I chose a local-first architecture
The main architecture decision was also the clearest product decision: no backend.
That immediately ruled out a lot of weight:
- no auth flow
- no sync engine
- no analytics pipeline
- no server-side persistence
- no question about whether a private journal entry is being sent somewhere else
For this product, local-first was not just an implementation shortcut. It was part of the UX. Dream entries are personal, often fragmented, and sometimes a little strange. The app feels better when it opens instantly, works offline, and keeps that data on the device by default.
There are real tradeoffs to that decision, and I will get to those later. But the first version of this product became much clearer once I stopped treating a backend as the default shape of an app.
Why IndexedDB and Dexie made sense
This could have been built with localStorage, but I do not think that would have held up well.
I needed structured client-side data, the ability to evolve the model over time, and a persistence layer that felt more like application storage than a key-value scratchpad. IndexedDB was the right fit for that, and Dexie made it practical to use without spending too much energy on browser storage ceremony.
Dexie was especially useful because the app already had more than one kind of record:
- dreams
- categories
- tags
- settings
Once the product moved beyond “one textarea saved locally,” a real data layer made the rest of the implementation calmer. I could model entries cleanly, query by date, join category metadata at the UI layer, and keep the app flexible without inventing a server just to make the data feel organized.
Modeling dreams, categories, tags, and settings
I tried to keep the model small enough to reason about without making it too thin to grow.
The main record is the dream entry itself. Each entry is tied to a local calendar day and stores the content, whether the day was marked as No dream, and optional metadata like a primary category or tags.
Categories are first-class because they support the main review pattern in the app. If someone wants to mark entries as anxiety, flying, lucid, or whatever structure makes sense to them, the calendar becomes more meaningful. The category colors are not just decoration. They make the review surface legible at a glance.
Tags are a little lighter weight. Categories create broad structure. Tags help capture themes that cut across that structure without forcing everything into a single taxonomy. Settings sit separately because theme preferences and behavior toggles should not leak into the main journaling model.
That separation helped the code and the product. The Today flow stays focused on capture. The calendar and history routes take on the heavier review work.
Designing around the Today flow
The Today screen mattered more than anything else because that is where the habit either survives or collapses.
I deliberately scoped it to the current local date only. That may sound restrictive, but it removes a lot of friction. The point of the screen is not to manage your entire archive. It is to make one decision easy right now:
- write down the dream
- mark
No dream - move on
Past entries are still there, but they live in the Calendar and Dreams flows instead. That separation was important. If the main screen starts trying to do capture, organization, review, editing, and analytics all at once, it stops feeling calm.
The overall visual direction followed the same rule. I did not want a dashboard. I wanted something softer and more editorial, where the interface feels human instead of managerial.
Why export and import matter in a local-first app
One of the easiest mistakes in local-first products is to treat local persistence as the whole story.
It is not.
If user data stays on-device, then portability and backup matter even more. Export and import are how the app acknowledges that browser storage is useful but not magic. If someone changes devices, wants a backup, or just wants more control over their own data, they should not have to reverse-engineer the storage layer to get it.
That is why I think export and import belong in the core product, not as an afterthought in settings. A local-first app still needs a clear ownership model.
The small technical problems that matter more than they look
Some of the hardest parts of this project were not flashy.
Date handling
Dates are tricky because the app is built around human days, not abstract timestamps. The Today flow has to mean the user's current local date, and the calendar has to stay stable across reloads and edits. That meant being deliberate about normalization and avoiding the kind of timezone drift that makes an entry appear on the wrong day.
For a dream journal, that kind of bug is not cosmetic. It breaks trust.
Browser dictation
Dictation felt like a natural fit for the product because dream capture often happens in a low-energy state where speaking is easier than typing. But browser support is inconsistent, so I treated it as progressive enhancement rather than a core dependency.
When it works, it lowers friction. When it does not, the app still needs to feel complete.
Theme support
Theme support sounds straightforward until you remember that visual tone is part of the product. I wanted the app to feel calm without becoming vague or low-contrast. That meant thinking about readability, category color behavior, and how much visual weight the interface should carry before it starts feeling like software for analysis instead of reflection.
Keeping it simple without underbuilding it
This was probably the most important ongoing judgment call. It is easy to keep an app simple by leaving useful things out. It is harder to decide which features actually reinforce the product promise.
For Dream Journal, the answer was usually: build the things that reduce friction or improve trust, and be suspicious of the things that only make the app broader.
The tradeoffs I accepted
Not adding backend or cloud features makes the app lighter, but it also means accepting real limitations.
There is no cross-device sync. There is no account recovery flow. If someone clears browser storage without exporting first, there is no server copy waiting for them.
I think those are honest tradeoffs for this version. Adding auth and sync would not just add technical work. It would change the posture of the product. More screens, more state transitions, more failure modes, and a different relationship to privacy.
I also chose not to add AI interpretation, analytics, or “insight” features. I did not want the app to tell users what their dreams mean. I wanted it to help them record and review them cleanly.
What I learned from building it
The strongest lesson was that privacy has to shape the product, not just the storage layer.
It affected the architecture, but it also affected the copy, the tone, the feature set, and the flow design. Once I treated privacy as part of the experience instead of a technical detail, a lot of decisions got easier.
I also learned that local-first apps still need strong recovery and portability stories. Keeping data on-device is useful, but users still need clear ways to move it, protect it, and trust it.
More broadly, the project reinforced something I keep coming back to: smaller products often get better when their boundaries are explicit. Dream Journal works because it does not try to become a universal journaling system.
What I would improve next
The first thing I would improve is the ownership story around data. Export and import exist, but I would keep refining that part of the experience so backup and portability feel more visible and deliberate.
I would also keep working on the review surfaces. The calendar already reveals useful patterns, but there is room to make that reflection loop stronger without drifting into quantified-self dashboards.
Finally, I would keep tightening the capture flow, especially around dictation and mobile ergonomics. That is the center of the product. If capture feels smooth, the rest of the app earns the right to exist.
Dream Journal is a small project, but I like what it clarified. A lot of apps become bigger by default. This one got better by getting narrower on purpose: private by default, local by design, and focused on making one personal habit feel easier to keep.