Back

How Snippiz is built

Snippiz is a small, deliberately boring web app: one Rails application, one SQLite database, one container. This page lists everything it is made of and why, partly because that is the kind of page we like reading ourselves, and partly because you should be able to see what you are trusting with your reading.

Ruby on Rails Hotwire SQLite Tailwind CSS daisyUI Kamal

Backend

A single Rails application. It fetches your Feeds on a schedule, parses them, and renders every page server-side.

  • Ruby

    The language everything server-side is written in.

  • Ruby on Rails 8

    The framework behind the whole app: routing, models, views, background jobs, and the session-based sign-in.

  • Feedjira

    Parses the RSS and Atom XML behind every Feed you subscribe to.

  • curl

    Does the actual fetching of Feeds and websites, rather than Ruby's built-in HTTP client.

  • libvips

    Generates the cover thumbnails and Feed icons you see on cards, from the original images.

  • Solid Queue

    Runs the background work: checking Feeds, resolving images, importing an OPML file.

Frontend

No single-page app, no JavaScript framework, and no build step. Pages are plain HTML sent by the server, with just enough JavaScript on top to make them feel instant.

  • Hotwire (Turbo)

    Swaps in new HTML instead of reloading the page, and updates things like unread counts live as you read.

  • Stimulus

    Small, targeted JavaScript controllers for the interactive bits: theme switching, dialogs, the media players, drag-and-drop ordering.

  • Import maps

    Ships JavaScript straight to your browser as modules. There is no bundler, no Node, and no node_modules folder in this project.

  • Tailwind CSS

    Every style on every page, written as utility classes.

  • daisyUI

    The component layer on top of Tailwind: buttons, cards, tabs, dialogs, and the light and dark themes.

  • View Transitions API

    The native browser feature behind the sliding and fading between pages. No animation library involved.

Data

Everything lives in SQLite. Your subscriptions, your Tags, and how far you have read are rows in the same database the app reads on every request.

  • SQLite

    The database, in production as well as in development. Not a scaled-down stand-in for something bigger.

  • Solid Cache and Solid Cable

    Caching and live updates, each in its own separate SQLite database so a busy cache never slows down your Snips.

Running it

One container, deployed from a laptop with a single command. Nothing here needs a cloud console.

  • Docker

    The app, its Ruby version, and its system dependencies ship as one image.

  • Kamal

    Deploys that image with zero downtime, and rolls back the same way.

  • Puma and Thruster

    Serve the app itself, plus HTTP/2, TLS, compression, and caching of static files.

Keeping it honest

Small project, boring tooling. All of it runs before anything ships.

  • Minitest

    The test suite, including browser-driven tests for the parts that only break in a real page.

  • RuboCop

    Keeps the Ruby code consistent, with every exception to its rules written down and explained.

  • Brakeman and bundler-audit

    Scan the code for security issues and the dependencies for known vulnerabilities.

Why it's built this way

Most of these are unfashionable choices. Here is the reasoning behind each one, and what it means for you as a reader.

Why SQLite, and not a "real" database?

Because a feed reader is a read-heavy app with one writer, which is exactly what SQLite is good at. The database file sits on the same machine as the app, so a query has no network to cross and typically answers in well under a millisecond. Rails 8 made this a first-class setup rather than a compromise.

Why is there no JavaScript framework?

Because the server already knows what your Snips page should look like, and sending it as HTML is both faster to display and far less code to maintain. Turbo then keeps navigation snappy without a second copy of the app running in your browser. The practical upside for you: pages render immediately instead of showing empty placeholders first.

Why does it fetch Feeds with curl?

Publishers sit behind CDNs that are increasingly suspicious of anything that is not a browser, and Ruby's built-in HTTP client gets turned away by some of them while curl gets through. Feed fetching is the one thing this app absolutely must get right, so it uses the tool with the best track record rather than the most elegant one.

Why re-host thumbnails instead of linking the originals?

Every cover image you see was downloaded once, resized, and stored by Snippiz. That keeps lists fast, survives publishers who block hotlinking, and means your browser is not quietly announcing your reading habits to a dozen other servers while you scroll.

Why are Feeds shared between accounts?

One Feed is fetched once for everybody subscribed to it, instead of once per account, which is both kinder to the publisher and the reason a popular Feed is up to date the moment you add it. Everything personal (which Feeds are yours, your Tags, how far you have read) is kept separately, per account.

Why is read state not just a checkbox per Snip?

Storing one row per person per article would mean millions of rows to say very little. Instead Snippiz remembers a point in time per Feed ("everything older than this is read") plus the handful of exceptions you made by hand. That is why marking a whole Feed as read is instant no matter how much is in it.

Curious what changed when? The Changelog covers every new feature as it ships.