khannurien 79b7adce8f
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 2m59s
v3: fix the queue and re-resolve edge cases in native bandcamp playback
Re-resolving a stream is a network round-trip, and nothing checked that the
player still belonged to that item once it landed. Picking another dump —
or closing the player — while a resolve was in flight let the stale result
swap the queue back; from onError, which resolves with autoplay, it would
also start playing over whatever was chosen instead. A generation counter,
bumped by playQueue/advanceTo/stop, now drops any result that no longer owns
the player, with a separate counter owning the resolving flag so a
superseded resolve can't clear a newer one's spinner.

A track that stays dead after a re-resolve is one track, not one album: the
cooldown path now steps to the next entry and only falls back to the iframe
on the last one. A failed resolve still goes straight to the embed, since
that's an album-wide failure rather than a single bad URL. And a track that
has vanished from the release, or lost its streamable flag to Bandcamp's
free-play cap, no longer resumes track 1 at the dead track's offset — the
findIndex miss resets the offset instead of seeking past the end of another
track.

Clicking the playing row rewound it but never resumed: seekTo only moves
currentTime, so re-selecting a finished or paused track looked like a
no-op. It now resumes as well. Same idea in the compact rich-content card,
whose button advertises "Pause" while active but re-played on click — in
native mode that meant a fresh /api/bandcamp/tracks and a restart from
track 1. It pauses for streams now; embeds have no transport of their own
and keep restarting, as before.

playRichContent reports whether playback actually started, so a native-mode
Bandcamp page with no streams and no stored embedUrl — an artist root, a
/music index, a preorder — is no longer a dead click: the journal card
navigates to the dump as it used to, and the rich-content card opens the
source page.

Also: the tralbum fetch releases the response body on its two error paths.
The endpoint is unauthenticated, so random /track/<slug> URLs would leak a
connection per request while also defeating the cache.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E56F55N7m5GGKKEwoYFJtH
2026-08-30 17:29:14 +00:00
2026-06-29 20:25:10 +00:00
2026-06-27 10:42:31 +00:00
2026-03-15 17:15:46 +00:00

gerbeur

A small invite-only social platform for sharing links and files. Users can post URLs and media (YouTube, SoundCloud, Bandcamp, images, audio, video, …), vote, comment, follow each other, build playlists, and search content. A real-time WebSocket layer handles live presence, vote counts, and notifications. The UI is localized (English and French) and ships multiple visual themes.

Stack

Layer Technology
Runtime Deno 2.x
API Oak (HTTP + WebSocket)
Database SQLite via node:sqlite
Frontend React 19 + Vite 8

Development

Prerequisites

  • Deno 2.x

Setup

cp .env.example .env
# Edit .env: set GERBEUR_JWT_SECRET to the output of: openssl rand -hex 32

Run

deno task dev

This starts both the API server (port 8000, with file watching) and the Vite dev server (port 3000) concurrently.

Open http://localhost:3000. On first run a default admin / admin account is created — change the password immediately.

Environment variables

See .env.example for the full list with descriptions. Key variables:

Variable Description Default
GERBEUR_JWT_SECRET JWT signing secret — required, generate with openssl rand -hex 32
GERBEUR_PUBLIC_URL Public-facing URL of the server (no trailing slash) — used for CORS, WebSocket origin, email links, OG URLs http://localhost:GERBEUR_PORT
GERBEUR_PORT Internal port Oak listens on 8000
GERBEUR_LISTEN_HOST Network interface Oak binds to; use 127.0.0.1 to restrict to loopback 0.0.0.0
GERBEUR_FRONTEND_URL Frontend base URL for email links and CORS; auto-added to allowed origins — the only variable needed when the frontend runs on a separate host GERBEUR_PUBLIC_URL
GERBEUR_ALLOWED_ORIGINS Comma-separated extra origins for CORS/WebSocket; PUBLIC_URL and FRONTEND_URL are always included — typically only needed in dev for the Vite server "" (empty)
GERBEUR_SITE_NAME Site name used in the browser tab, app header, OG meta tags, and emails gerbeur
GERBEUR_SMTPS_URL SMTPS connection URL for outgoing email (smtps://user:pass@host:465) unset
GERBEUR_FROM_EMAIL Sender address for outgoing emails — required when GERBEUR_SMTPS_URL is set unset
GERBEUR_WELCOME_EMAIL_BODY Markdown body for the account-creation welcome email; supports {{username}} and {{site_name}} built-in template
VITE_API_PROTOCOL API protocol baked into the frontend bundle (see Production) http
VITE_API_HOSTNAME API hostname baked into the frontend bundle localhost
VITE_API_PORT API port baked into the frontend bundle 8000

Production

The standard deployment runs API and frontend in a single container. The API server (Oak) serves the compiled frontend as static files, so both share the same origin — no VITE_API_* build args needed. Set GERBEUR_PUBLIC_URL to the externally-visible URL; it is automatically allowed for HTTP/WebSocket requests.

docker build -t gerbeur .

docker build -t gerbeur .

docker run -d \
  -p 8000:8000 \
  -v gerbeur-db:/app/api/sql \
  -v gerbeur-uploads:/app/api/uploads \
  -e GERBEUR_JWT_SECRET=$(openssl rand -hex 32) \
  -e GERBEUR_PUBLIC_URL=https://example.com \
  -e GERBEUR_SITE_NAME=mysite \
  --name gerbeur \
  gerbeur

The two volumes are required for persistence:

  • gerbeur-db — SQLite database (api/sql/gerbeur.db), initialized automatically on first run
  • gerbeur-uploads — user-uploaded files (api/uploads/)

Separate API and frontend (optional)

If you need to run the API on a different host than the frontend, pass the API location as build args so it gets baked into the frontend bundle. In that setup, add the frontend origin to GERBEUR_ALLOWED_ORIGINS so cross-origin HTTP/WebSocket requests are accepted:

docker build \
  --build-arg VITE_API_PROTOCOL=https \
  --build-arg VITE_API_HOSTNAME=api.example.com \
  --build-arg VITE_API_PORT=443 \
  -t gerbeur .

Reverse proxy

Put a reverse proxy (nginx, Caddy, …) in front of the container to handle TLS. Forward everything to port 8000. Example Caddyfile:

example.com {
    reverse_proxy localhost:8000
}

Project structure

api/
  main.ts          # Entry point — Oak application, middleware, routes
  config.ts        # Environment variables
  middleware/      # errorMiddleware, authMiddleware
  routes/          # HTTP routes + WebSocket
  services/        # Business logic
    providers/     # Link metadata providers (YouTube, SoundCloud, Bandcamp, …)
  model/           # Row types, type guards
  lib/             # JWT, pagination, slugify, upload, static helpers, …
  db/
    schema.sql     # Database schema
    init.ts        # First-run database initialisation
    migrate.ts     # Migration runner
    migrations/    # Versioned schema migrations
  sql/
    gerbeur.db     # SQLite database (not committed)
  uploads/         # User-uploaded files — avatars, dumps, thumbnails, … (not committed)
src/               # React frontend (Vite)
  config/          # API base URL, validation constants, feed tabs, upload limits
  pages/           # Route-level components
    index/         # Feed views (hot, new, followed, journal)
  components/      # Shared UI components
    form/          # Reusable form controls
  contexts/        # Auth, WebSocket, player, follows, theme
  hooks/           # Data fetching and UI hooks
  utils/           # Formatting, URL, hot-score, waveform, … helpers
  locales/         # Lingui message catalogues (en, fr)
  themes/          # Per-theme CSS files
  model.ts         # Shared frontend types
  i18n.ts          # Lingui runtime setup
public/            # Static assets (favicon, manifest, service worker)
Description
🚚 à dégager
Readme MIT 4.9 MiB
Languages
TypeScript 81.5%
CSS 18.2%
HTML 0.2%
Dockerfile 0.1%