linkstash is a small experiment for collecting and sharing interesting links and short notes you find during the week.
Previously we used a self‑hosted parser (
lava), but the service now leverages the public defuddle.md converter. Append any URL as a path component (e.g.curl https://defuddle.md/stephango.com) and you'll get back Markdown with YAML frontmatter.
ashdetects links posted to a WhatsApp group (bridged to Matrix) and forwards them to the Cloudflare Worker API.linkstashcalls the Markdown parser (defuddle.mdby default) and stores the markdown + rich frontmatter in Turso- The app lists saved links and renders the parsed Markdown in a Hacker News–style feed.
- SvelteKit (App Router, runes) + Vite, TypeScript (strict)
- Deployed to Cloudflare Workers via
@sveltejs/adapter-cloudflare @libsql/client(Turso), PostHog analytics- Package manager: bun (
bun.lock). Do not commit apackage-lock.jsonoryarn.lock.
bun install
cp .env.example .env
bun run dev # vite dev server (http://localhost:3000)To preview the production Worker build locally:
cp .env.example .dev.vars # wrangler reads secrets from .dev.vars (gitignored)
bun run build # vite build -> .cloudflare/worker.js + .cloudflare/static
bunx wrangler dev # run the built Worker on http://localhost:8787bun run dev # vite dev
bun run build # vite build (adapter-cloudflare)
bun run check # svelte-check (type-check + lint warnings)
bun run preview # build + wrangler dev
bun run deploy # build + wrangler deploy (requires wrangler auth)
bun run db:init # initialize the Turso DB schema
bun run cf-typegen # regenerate cloudflare-env.d.ts from wrangler.jsoncAUTH_KEY: Your authentication keyTURSO_DATABASE_URL: Your Turso database URLTURSO_AUTH_TOKEN: Your Turso auth tokenPUBLIC_POSTHOG_KEY/PUBLIC_POSTHOG_HOST: PostHog analytics (client + server)VOTE_COOLDOWN_MS: Cooldown between upvotes from the same submitter/room (default 6h)LAVA_URL: Optional; overrides the default parser (https://defuddle.md/) for testing or backwards compatibility.
PUBLIC_-prefixed vars are exposed to the client;AUTH_KEY, Turso and parser vars are server-only and read via$env/dynamic/private.
POST /api/add- Add a new linkGET /api/links- Get all links (paginated, supportsmode/offset/limit/url)GET /api/feed- RSS feed of all linksGET /api/summary- Summary of links for a date rangeGET /api/content/[key]- Get content by keyGET /api/related/[key]- Semantically related linksGET /api/proxy- SSRF-guarded URL proxyDELETE /api/admin/link- Delete a link (admin)
See api.md for details.
/admin— simple admin UI to delete links (protected byAUTH_KEY). This page lets you enter yourAUTH_KEYand delete individual links. Deletions call the server (DELETE /api/admin/link) and are gated by the sameAUTH_KEYused for adding links.
Below are quick curl examples for local development (assumes bun run dev or
bunx wrangler dev) and that AUTH_KEY is set in your environment.
curl -X POST "http://localhost:3000/api/add" \
-H "Authorization: Bearer $AUTH_KEY" \
-H "Content-Type: application/json" \
-d '{"link": {"url": "https://example.com/article", "submittedBy": "bot"}}'- Success returns
{ "ok": true }. - Possible responses:
401 Unauthorized(bad/absent auth),400 Bad Request(missing link),500on server errors.
curl "http://localhost:3000/api/content/<key>"- Returns the saved
content(plain text / markdown) for the given linkid. - If not found you'll get a JSON
404response:{ "error": "Not found" }.
Note:
/api/linksis a lightweight index and returns the linkmetafields directly (spread into the response). It does not include the fullcontent. Sensitive fields such as submitter identifiers (submittedBy) and room identifiers (roomId) are stripped from API responses, but room comments (roomComment) are preserved and included in responses. Use/api/content/<key>to fetch the stored markdown or content for a link.
curl "http://localhost:3000/api/feed"- Returns an RSS 2.0 XML feed of the latest links, ordered by timestamp (newest first).
- The feed includes the link title, URL, domain, vote count, room comment, submitter attribution, tags, and an excerpt.
- Supports
mode(latest,top) andlimitquery parameters, matching/api/linksbehavior. - Defaults to the 50 most recent links (max 200).
You can query link metadata by providing the url query parameter (the service will normalize the URL before matching):
curl "http://localhost:3000/api/links?url=https%3A%2F%2Fexample.com%2Farticle"- Returns the matching link record (JSON) or a
404if not present.