DMITRY MELNIKOV
Project 08· Personal

Read Later

Distillation at your own pace. Capture articles, videos, and notes into clean markdown, read anywhere offline.

Stack
Vue 3 · TypeScript · Supabase (PostgreSQL) · Bun · Pinia · Docker · Vite PWA
Backend
Docker · Supabase · Node.js
Third-party
Mozilla Readability · Turndown · Tabler Icons
Protocols
HTTP · REST
One-liner
Distill the web into clean, focused markdown with tags, search, and public sharing.
Fig. 01

Read Later list view, light/dark theme.

Overview01

The focused reading list. Convert noisy web pages and notes into clean, offline-ready markdown.

Read Later is a lightweight, self-hosted reading platform designed to eliminate clutter and save articles, YouTube videos, and web pages for offline reading. Unlike commercial bookmarking services that lock content behind proprietary reader formats, Read Later extracts raw HTML into clean, standardized Markdown saved directly in PostgreSQL.

Content capture spans multiple channels: a zero-dependency browser bookmarklet for desktop and iOS Safari, native PWA Web Share Target for Android, and an iOS Shortcut—all routing through a unified Supabase edge function. Saved links trigger a background worker that fetches and parses content, while free-form text shares instantly bypass worker queues as direct notes (type='note', status='ready').

The system includes weighted Postgres full-text search, create-on-type relational tagging, unguessable public link sharing (/s/:id), and atomic URL deduplication.

Translation of the content - by URL (articles added - Google Translate web-site) or inline (for snippets - DeepL API).

Timeline Q2 · 2026 Scope Product Architecture
Vue 3 PWA Reader
Parser Worker
Search & Tagging Engine Scale Personal Archive
Multi-device Sync

Architecture02

Decoupled components for zero friction and total privacy.

Read Later architecture diagram showing Bookmarklet, Share Target, Supabase Edge Function, PostgreSQL, VPS worker, and Vue 3 PWA
Fig. 02 — System Topology · Multi-channel capture (Bookmarklet, Share Target, iOS Shortcut), edge ingestion routing, background VPS parser worker, PostgreSQL with FTS/relational tags, and Vue 3 PWA Reader
Decisions03

Engineered for simplicity and durability.

/01
Multi-Channel Capture via Unified Edge Ingestion

A single-line JavaScript bookmarklet works seamlessly across desktop browsers and iOS Safari without background battery drain or browser store approvals. Combined with a Web App Manifest share_target for Android and iOS Shortcut integration, all capture channels share a single static bearer key on the Supabase capture edge function.

Tradeoff · cross-platform simplicity vs. native app overhead
/02
Weighted Postgres FTS over External Search Engines

Markdown content is indexed directly in PostgreSQL using a generated tsvector column (search_vector) with weighted titles (setweight 'A') and body content (setweight 'B'), indexed with GIN (bookmarks_search_idx). Client queries debounce 300ms, and gracefully degrade to offline title/excerpt substring matching when disconnected.

Tradeoff · generated stored tsvector column vs. Meilisearch infrastructure
/03
Normalized Relational Tagging Schema

Replaced flat array storage with a relational tags and bookmark_tags schema. Creating indexes with tag_id leading ensures sub-millisecond tag-based filtering (search_vector @@ query AND tag_id in (...)), while create-on-type upserts avoid client-side race conditions.

Tradeoff · explicit join table complexity vs. array column query bottlenecks
/04
RPC-Gated Public Sharing over Blanket RLS

Public sharing uses unguessable UUID URLs (/s/:id) exempted from the PWA’s auth route guard. Instead of a blanket is_public = true RLS policy—which allows anyone with the publishable key to enumerate all public bookmarks—reads are restricted to a security definer RPC function (get_public_bookmark) requiring the exact bookmark ID.

Tradeoff · security definer RPC function vs. public row enumeration vulnerability
/05
Atomic Database URL Normalization & Deduplication

A generated url_normalized column (lowercased host, stripped trailing slashes) with a unique index (bookmarks_url_normalized_uniq) enforces duplicate prevention at the database level. Conflicts return a duplicate status, allowing users to cancel or re-trigger a background parse (POST /capture/:id/refresh).

Tradeoff · generated column storage vs. check-then-insert race conditions
/06
Direct Note Ingestion Pipeline

Capturing non-URL text automatically creates a direct note entry (type='note', status='ready'). This bypasses the VPS worker queue entirely, giving instant zero-latency availability for personal snippets and thoughts.

Tradeoff · bypass parser worker vs. uniform article status flow
Outcome05

Distillation at your fingertips.

Read Later delivers a fast, private, and fully customizable reading archive. The updated architecture seamlessly combines multi-channel capture, instant full-text search, relational tagging, unguessable public link sharing, and offline PWA capabilities—providing a clutter-free environment for deep reading and knowledge retention.

Impact Zero reading distractions
Instant full-text search
Relational tags & notes
Unguessable public links
Offline PWA support Next Inline note editor
URL query tracking stripper
Prerendered Open Graph cards

Fig. 02

Reading article.

Fig. 03

Reading progress.