Marx

A personal bookmark manager with tag filtering, backed by SQLite. Publicly readable, password-protected for edits.

Live demo

How it works

Marx is a single-file Val Town HTTP val. Everything — routing, auth, database queries, and HTML rendering — lives in main.ts.

Storage: Uses Val Town's built-in SQLite via std/sqlite. Three tables:

  • bookmarks — stores URL, title, description, and save date
  • tags — unique tag names
  • bookmark_tags — many-to-many join table

Auth: A single password set via an environment variable. On login, the password is hashed with SHA-256 to produce a session token, which is stored as an HttpOnly cookie valid for 30 days. No user accounts — it's single-owner.

Public vs. private: Anyone can browse bookmarks and filter by tag. Only authenticated users see edit/delete controls and can call the mutation APIs.

Routing is handled manually in the default export function:

RouteMethodAuth required
/GETNo
/tags/:slug/GETNo
/loginGET / POSTNo
/logoutGETNo
/api/bookmarks/updatePOSTYes
/api/bookmarks/deletePOSTYes
/api/tags/renamePOSTYes
/api/tags/deletePOSTYes
/api/importPOSTYes

Forking and setting up

1. Fork the val

On Val Town, open snptrs/marx and click Fork.

2. Set your password

In your val's environment settings, add:

AUTH_PASSWORD=your-password-here

3. Initialize the database

The val doesn't auto-create its tables. Run this once as a Val Town script to set up the schema:

import { sqlite } from "https://esm.town/v/std/sqlite/main.ts"; await sqlite.execute(` CREATE TABLE IF NOT EXISTS bookmarks ( id INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT NOT NULL, title TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '', saved TEXT NOT NULL DEFAULT (datetime('now')) ) `); await sqlite.execute(` CREATE TABLE IF NOT EXISTS tags ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE ) `); await sqlite.execute(` CREATE TABLE IF NOT EXISTS bookmark_tags ( bookmark_id INTEGER NOT NULL REFERENCES bookmarks(id), tag_id INTEGER NOT NULL REFERENCES tags(id), UNIQUE(bookmark_id, tag_id) ) `);

4. Import existing bookmarks (optional)

POST a JSON array to /api/import with a Cookie header containing your session token, or just log in to your instance first and use your browser session.

[ { "url": "https://example.com", "title": "Example Site", "description": "Optional notes", "saved": "2024-06-01T12:00:00Z", "tags": ["design", "reference"] } ]

Managing bookmarks

Once logged in:

  • Edit — hover a bookmark and click "edit" to update its title, URL, description, or tags
  • Delete — hover a bookmark and click "delete"
  • Tag autocomplete — the tag input suggests existing tags as you type
  • Rename/delete tags — navigate to a tag page and use the Rename/Delete buttons in the sidebar