displace

News and events from many sources, aggregated in one place.

Hacker News, podcasts, YouTube and Warsaw events, one inbox per account.

WebWeb appNuxt 4, Vue 3, libSQLGPLv3

Your events page in the dark theme, listing upcoming events soonest first, with venue, city and date for each row.

What it is

displace is a multiuser aggregator for Hacker News, podcasts and YouTube news plus eBilet, Going and public Facebook events. Feeds are fetched centrally, once per feed; each account picks its own subscriptions from the catalogue. Unique links plus feed-membership rows keep shared stories deduplicated, so two people following overlapping feeds do not double the storage.

It is built on Nuxt 4 and Vue 3 with Tailwind CSS 4, daisyUI 5 and Nuxt UI, Drizzle ORM and libSQL, which means either Turso or a local SQLite file.

How it works

Refresh is a POST-only endpoint behind a bearer token; user sessions cannot trigger it. Each feed fails in isolation: a dead or malformed feed never aborts the others, malformed items are skipped one at a time, and the failure is stored as the feed's lastError. When anything failed the endpoint returns 207 with {attempted, refreshed, failed} and pnpm refresh exits nonzero. Reruns do not duplicate links. A cron line under ops/ runs it every six hours under flock.

The catalogue is a typed config file, server/feeds.config.ts, or a JSON override pointed at by NUXT_SUBSCRIPTIONS_FILE. Only operators edit it; users choose from it and cannot supply arbitrary scrape URLs.

Accounts sign in with email and a 12–128 character password hashed with scrypt. Sessions are random opaque cookies stored as SHA-256 hashes. Registration is closed by default, or invite-only with a code, or open.

What it does

  • /news: the account's news newest first, with a recency window of 24 hours, 7, 30 or 365 days.
  • /events: upcoming events as a List, an Agenda grouped by local day with a detail panel, or a monthly Calendar; filter by source, city and date range.
  • The first page of 50 items is server-rendered; further pages load on scroll. Source choices persist in a cookie so the first page is already filtered.
  • Daily briefs: one per account per completed UTC day, up to 10 news items and 10 upcoming events, with a plain-sentence text rendering meant for TTS. Operators can have their own brief sent as a Telegram voice note rendered with Piper.
  • Personal read APIs under /api/events, /api/news, /api/calendar and /api/sources.
  • Optional PostHog analytics, off by default, with a fresh random distinct_id per event.

Limitations

Provider JSON interfaces for eBilet and Going are unofficial and may change. Facebook events are scraped from logged-out public pages one request at a time, 5–7 s apart, and a login wall or rate limit fails those entries. There is no email delivery of briefs and no audio for ordinary users; briefs are read in the /digests inbox. There is no email verification or password recovery. It needs a running Nitro server and a persistent database; static generation cannot ingest feeds.

Run

pnpm install --frozen-lockfile
cp example.env .env   # set NUXT_REFRESH_TOKEN
pnpm migrate
pnpm dev

Node.js 22+ and pnpm 10.10.0. For production, pnpm build then node --env-file=.env .output/server/index.mjs behind HTTPS.