For years, I've been messaging myself. Slack, Discord, whatever was convenient. Quick thoughts. Links to read later. Files to reference. Screenshots. Code snippets I wanted to remember.
I tried everything. Evernote felt like filing paperwork. Notion wanted me to build a second brain when I just needed a notepad with no friction. Obsidian, Apple Notes, Bear, Keep, Simplenote, Standard Notes—all fine tools, but I stuck with none of them.
What I actually wanted: a messaging interface. Send myself stuff. Search it later. Done.
No notebooks to organize. No folders or titles to think about. Just a chronological stream of everything I've sent myself, with the ability to group things into collections when I need structure.
A messaging app, but for one person.
Simple idea. But every time I pictured building it, I'd see the JS stack, the infra, and the headaches that came with them.
So I kept putting it off. Kept messaging myself on Discord.
Then I got a Framework laptop, put Linux on it, and it gave me the dopamine kick I needed to just build it. Not as a side project to abandon, but as something I'd actually use and share with others. Self-host it. Own the whole stack. No PaaS bills, no vendor lock-in, no BS.
I didn't want to build another JavaScript app. I'd done that. I wanted to try something different, something that would actually hold my attention. You know that feeling when you start a side project and lose interest in a week? I needed the opposite. I needed the spark.
I hadn't touched Rails in 15 years. But I had recently watched DHH's Rails 8 keynote — Kamal, SQLite in production, the whole "No PaaS Required" thing. It looked different enough to be interesting, simple enough to actually ship something. And besides, I had just started using Omarchy on my personal laptop. The stars had finally aligned.
Three weeks later, Monologue was live. On a Framework 13 mainboard under my desk.
Why Not JavaScript?
I use React, Vue, Nuxt, and Astro at work. I've also built many side projects with Vue. These are solid frameworks. The Vue composition API is elegant. Nuxt has good DX. But the fun just isn't there anymore, especially when it comes to building a backend with Node.
JavaScript is fine. It's everything around it that kills the fun. Vite configs that differ between dev and prod. Build errors that only surface in CI. Environment variables duplicated across `.env.local`, `.env.production`, and deployment configs. SSR hydration mismatches that work fine locally but explode in production. An afternoon lost debugging why the build fails because some transitive dependency updated.
JavaScript is fine. It's everything around it that kills the fun. Vite configs that differ between dev and prod. Build errors that only surface in CI. Environment variables duplicated across `.env.local`, `.env.production`, and deployment configs. SSR hydration mismatches that work fine locally but explode in production. An afternoon lost debugging why the build fails because some transitive dependency updated.
And TypeScript. Many swear by it. But god... types for everything, interfaces everywhere, fighting the compiler instead of building features. Where's the fun in that?
Then there's node_modules. In September 2025, two major npm supply chain attacks hit. First, a phishing attack compromised 18 widely-used packages—chalk, debug, ansi-styles—with over 2.6 billion weekly downloads combined. Malicious code intercepting crypto transactions went live for two hours.
Then there's node_modules. In September 2025, two major npm supply chain attacks hit. First, a phishing attack compromised 18 widely-used packages—chalk, debug, ansi-styles—with over 2.6 billion weekly downloads combined. Malicious code intercepting crypto transactions went live for two hours.
Then came "Shai-Hulud." A self-replicating worm that infected 500+ npm packages. When it found GitHub tokens in a compromised environment, it automatically published malicious versions of any packages it could access. It created repositories to dump stolen credentials. Self-propagating malware, spreading through the dependency chain.
That's not npm being insecure—that's the reality of a dependency chain where a typical React app pulls in 1,500+ transitive dependencies. npm audit screaming warnings. Patch version bumps breaking builds. Peer dependency conflicts. 700MB node_modules folders for a 50KB app.
For a personal messaging app? No thanks.
With Rails: no node_modules. No bundler. No build step. I write code, refresh the page, it works. That's it.
How Monologue Actually Works
Here's the entire stack: SQLite for the database, SQLite for the cache, SQLite for background jobs, SQLite for WebSockets. Four separate files. Zero external dependencies. No Redis, no Postgres, no Sidekiq, no Memcached.
I'm not building Twitter. I'm building a messaging app for myself. SQLite on a modern NVMe drive is more than fast enough. And it's just files on disk. `rsync` for backups. Done.
Authentication is built in:
class SessionsController < ApplicationController
def create
if user = User.authenticate_by(params.permit(:email_address, :password))
start_new_session_for user
redirect_to after_authentication_url
end
end
endThat's it. No Devise. No Auth0. No configuration files spread across twelve directories. It just works.
Real-time updates? Turbo Streams over WebSockets. Messages appear as you send them. Collections update live. Zero React, zero Vue, zero megabyte JavaScript bundles:
module Message::Broadcasts
after_create_commit -> { broadcast_append_to([user, :messages]) }
endThat's the entire real-time system. Message gets created, Turbo broadcasts it, DOM updates. No state management, no Redux, no useEffect hooks trying to keep the UI in sync.
Interactive bits—auto-resizing textareas, keyboard shortcuts, scroll management—are handled with Stimulus. Small controllers that enhance server-rendered HTML:
export default class extends Controller {
adjustHeight() {
this.inputTarget.style.height = 'auto'
this.inputTarget.style.height = this.inputTarget.scrollHeight + 'px'
}
}The Message model could have easily hit 500 lines. Instead, it's split into focused modules:
class Message < ApplicationRecord include Message::Broadcasts # Real-time updates include Message::Searchable # Full-text search with SQLite FTS5 include Message::LinkProcessing # Extract URLs, fetch previews include Message::Pinnable # Pin important messages belongs_to :user has_many_attached :attachments end
Each module does one thing. Open a file, see what it does, understand it completely. No archaeological digs through twelve files to figure out where a feature lives.
Self-Hosting on a Framework Mainboard
I'm running Monologue on a Framework 13 mainboard under my desk. Literally a homelab—a tiny server I can touch.
Deployment is as simple as:
kamal deploy
That's it. Docker containers, zero-downtime deploys, SSL certificates, health checks, rolling restarts. All handled automatically.
I can push an update and users don't see a blip. New containers start, health checks pass, traffic switches over, old containers die. In production. On a server under my desk.
That's what made this satisfying—not just building Monologue, but owning the whole stack.
Building with AI
I built Monologue with Claude Code as a pair programming partner (*gasp*).
Before you roll your eyes—I'm not talking about copy-pasted AI slop. I mean using it for the tedious stuff: scaffolding controllers, writing migrations, implementing patterns I already knew I wanted. It follows Rails conventions (when prompted right), suggests concern extraction when models get fat, catches bugs I miss. It's like having a junior developer who knows Rails 8 extremely well and never gets tired.
But here's what it doesn't do: decide what to build, how features should work, or what the app should become. Those decisions are mine. I review the code it produces. I refactor when something feels wrong. I own the architecture.
The result? Three weeks from first commit to production. Working on it mostly just nights and weekends.
The code quality is also higher than if I'd done it solo. Not because AI writes better code necessarily, but because having something to react to and think about, even if it's wrong, helps clarify what you actually want. Editing and improving is faster than creating from scratch.
If you're worried about AI code quality—don't be. Used right, it's a force multiplier for developers who know what they're building. You can just build things, make use of tools that can make you more productive, and ignore the haters.
Why This Matters
I finally built the thing I've wanted for years. A messaging interface for myself. Simple and focused, no overhead, no social features. Just send stuff, search it later, group it when needed.
And the whole stack is mine. Four SQLite files. A server under my desk. No external dependencies. No surprise bills. And I had fun along the way.
If Monologue grows beyond what a single server can handle, I'll deal with it then. But here's the thing: I'll cross that bridge when I get there. Not six months early while guessing what scale looks like.
For now? It works and it's plenty fast.
The web got complicated somewhere along the way. We convinced ourselves you need Postgres, Redis, Sidekiq, Memcached, ElasticSearch, JS frameworks for everything, and a PaaS to wire it together before you can build anything real.
You don't.
Monologue has real-time updates, file uploads, data encryption, background jobs, WebSockets, full-text search—everything you'd expect from a modern web app. Built with Rails, deployed to a Framework mainboard, running on SQLite.
Rails 8 delivered on its promise and gave me the satisfaction of building something I've wanted for so long. And that Framework laptop running Linux? It gave me the dopamine kick I desperately needed to finally stop putting it off.