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-inSQL). - Postgres — the shared mRiver Supabase, in a dedicated
dasbesschema. All queries schema-qualify their tables, so the connectionsearch_pathis irrelevant. - Client (
public/app.js) — voting, suggesting and admin delete; the device token lives inlocalStorage.
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;votesis a denormalized counter kept in sync with thevotestable 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.