dasbes.de

A small community "das beste X" voting app. Per topic, visitors see the top-voted answer big, can submit their own sentence (optionally with a link), and upvote others. Anonymous, one vote per device, no login.

m's canonical format — full sentences with correct gender per noun:

Das beste Fahrrad ist das, das du gerne fährst.

Visit dasbes.de/<thema> (e.g. /fahrrad, /kaffee). The root redirects to a random topic that already has content. dasbes.de/alle is the overview: every topic with content, its top answer and activity (suggestions + total votes), sorted by votes. Linked from the footer of every page.

Stack

  • Bun server (server.ts) — SSRs the topic page and serves a tiny JSON API. No web framework; no DB driver dependency (uses Bun's built-in SQL).
  • Postgres — the shared mRiver Supabase, in a dedicated dasbes schema. All queries schema-qualify their tables, so the connection search_path is irrelevant.
  • Client (public/app.js) — voting, suggesting and admin delete; the device token lives in localStorage.

Files

File Purpose
server.ts HTTP server: SSR + JSON API, validation, rate limiting, admin
db.ts Data layer (Bun SQL), all queries schema-qualified to dasbes
views.ts Server-side HTML rendering (dark/yellow theme)
public/app.js Client: device token, vote toggle, suggest, admin delete
schema.sql dasbes schema + tables
migrate.ts Applies schema.sql, then seeds the starter sentences (idempotent)

Schema (dasbes)

  • topics(slug, created_at) — topic registry.
  • suggestions(id, topic, text, url, votes, hidden, created_at) — one sentence per row; votes is a denormalized counter kept in sync with the votes table inside the vote transaction.
  • votes(suggestion_id, device, created_at) — enforces anonymous one-vote-per-device per suggestion (toggle deletes the row).
  • submissions(id, device, ip, created_at) — log for rate limiting.

API

Method Path Body Returns
GET /api/<thema>?device=X {topic, suggestions:[{id,text,url,votes}], voted:[id]}
POST /api/suggest {topic, text, url?, device} created suggestion / {error}
POST /api/vote {id, device} {voted, votes} (toggle)
POST /api/delete {id, token} {ok} — admin only (hides one suggestion)
POST /api/delete-topic {topic, token} {ok} — admin only (hard-deletes a whole topic + its suggestions/votes; the /alle overview's moderation tool)

The /alle overview is public (clean list). With ?admin=<token> it additionally lists hidden/empty topics (dashed) and reveals a per-row delete control.

Anti-spam: HTML stripped, text ≤ 280 chars, links must be http(s), and submissions are rate-limited per device and per IP.

Moderation

Set ADMIN_TOKEN. Then dasbes.de/<thema>?admin=<token> reveals delete buttons; the server verifies the token (constant-time) on every delete.

Local dev

cp .env.example .env      # fill DATABASE_URL + ADMIN_TOKEN
bun run migrate           # create schema + seed (idempotent)
bun run dev               # http://localhost:3000 with --hot

Deploy

Deployed as a Dokploy compose on mLake (docker-compose.yml), Bun container on port 3000, attached to dokploy-network, routed by Traefik for dasbes.de (+ www.dasbes.de). DATABASE_URL and ADMIN_TOKEN are set in the Dokploy compose env, never committed. The container runs migrate.ts (idempotent) on start, then serves.

DB lives on the shared mRiver Supabase (100.99.98.203:6789, dasbes schema). A git push to m/dasbes triggers the Dokploy build + deploy.

Description
dasbes.de — community "das beste X" voting app (Bun + Supabase)
Readme 46 KiB
Languages
TypeScript 85.4%
JavaScript 13.3%
Dockerfile 1.3%