translate for the lift, rotate for the tilt, transform for the float — since that's what lets them compose rather than the last one declared winning outright; the tilt and the float run on periods that don't divide each other (2.6s against 3.4s) so the pair never lines up the same way twice and never settles into a metronome, and the pivot sits on the bottom edge so it reads as a head on a neck rather than as a button spinning about its middle. Arriving and leaving is what the machinery is for: the amplitude is a registered custom property the keyframes multiply through, so pointing at the button eases the rocking up from nothing while the lift is still settling, and leaving eases it back down under an animation that is merely paused — no switch from still to full throw, and no snap square when the cursor goes. Geocities and brutalist re-declare the whole transition shorthand on every button and neither list mentions translate, which outweighed the header's own rule and dropped the lift onto the frame it started on, so that rule takes a :not(:disabled) to win the weight back. nyt stays flat ink and brutalist goes on pressing into the page, both opting out by name; reduced motion keeps the lift and drops the rocking.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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
Docker (recommended)
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 rungerbeur-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)