khannurien 563daa720c
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 50s
Build and Deploy Static Documentation Site / build-and-push (push) Successful in 22s
v3: src/ is documented like api/ and shared/, and the reference covers the whole repository — the frontend had no module docs, JSDoc on some components and hooks and not on others, props typed inline in half the components and in private interfaces in the rest, // comments on declarations, and no return types on 147 exported functions, so deno doc --lint over src/ found 637 errors. The rule api/ follows is applied to every non-test module of src/: a /** … @module */ block, JSDoc on every export and every field of an exported interface, /** */ for declarations and // for notes, verbs for functions ("Renders…" for a component), parameters named in the prose, {@link} only for symbols in scope, and a page's JSDoc names its route and who may see it (App.tsx's module doc lists the route table). For deno doc to show them, every exported function states its return type: components React.JSX.Element (or | null, or a portal), memo and forwardRef components their exotic types, contexts React.Context<…>, and a hook returning several things a documented Use<Name>Result interface (useAuth, useRequiredAuth, useFading, useEmojiTrigger, useMentionAutocomplete, useDumpCardLink, usePlayRichContent; FeedCacheResult, YouTubePlaylistControls and UseApiFormReturn are renamed to match). Every component that takes props takes an exported, documented <Name>Props interface — the 64 inline prop types are lifted into one, DumpFeed gets DumpFeedProps extending MainFeedProps. React types go through the global React namespace: the four default imports of react are gone, since deno doc can't follow them. Two duplicates go too: useMentionAutocomplete's UserResult was shared/'s UserSuggestion, and JournalCard re-exported JournalShape for no one; i18n.ts no longer re-exports Lingui's i18n, which its three users now import from @lingui/core. deno task doc and doc:lint now take src/ (but its tests and the generated locales), the index page gains a Frontend section linking every module by layer and domain, mod.test.ts walks src/ as well, and the docs workflow runs on changes to src/. deno doc doesn't document npm packages, so it takes a type imported from one — react-hook-form's FieldValues in the form fields, frimousse's SkinTone — for a private one; doc:lint now goes through doc-lint.ts, which fails on every diagnostic but those, 25 of them. The README states the frontend's part of the rule. No behaviour changes; 205 backend and 628 frontend tests pass, and tsc, deno check and deno lint are clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 07:31:22 +00:00
v3: src/ is documented like api/ and shared/, and the reference covers the whole repository — the frontend had no module docs, JSDoc on some components and hooks and not on others, props typed inline in half the components and in private interfaces in the rest, // comments on declarations, and no return types on 147 exported functions, so deno doc --lint over src/ found 637 errors. The rule api/ follows is applied to every non-test module of src/: a /** … @module */ block, JSDoc on every export and every field of an exported interface, /** */ for declarations and // for notes, verbs for functions ("Renders…" for a component), parameters named in the prose, {@link} only for symbols in scope, and a page's JSDoc names its route and who may see it (App.tsx's module doc lists the route table). For deno doc to show them, every exported function states its return type: components React.JSX.Element (or | null, or a portal), memo and forwardRef components their exotic types, contexts React.Context<…>, and a hook returning several things a documented Use<Name>Result interface (useAuth, useRequiredAuth, useFading, useEmojiTrigger, useMentionAutocomplete, useDumpCardLink, usePlayRichContent; FeedCacheResult, YouTubePlaylistControls and UseApiFormReturn are renamed to match). Every component that takes props takes an exported, documented <Name>Props interface — the 64 inline prop types are lifted into one, DumpFeed gets DumpFeedProps extending MainFeedProps. React types go through the global React namespace: the four default imports of react are gone, since deno doc can't follow them. Two duplicates go too: useMentionAutocomplete's UserResult was shared/'s UserSuggestion, and JournalCard re-exported JournalShape for no one; i18n.ts no longer re-exports Lingui's i18n, which its three users now import from @lingui/core. deno task doc and doc:lint now take src/ (but its tests and the generated locales), the index page gains a Frontend section linking every module by layer and domain, mod.test.ts walks src/ as well, and the docs workflow runs on changes to src/. deno doc doesn't document npm packages, so it takes a type imported from one — react-hook-form's FieldValues in the form fields, frimousse's SkinTone — for a private one; doc:lint now goes through doc-lint.ts, which fails on every diagnostic but those, 25 of them. The README states the frontend's part of the rule. No behaviour changes; 205 backend and 628 frontend tests pass, and tsc, deno check and deno lint are clean.
2026-09-28 07:31:22 +00:00
2026-06-29 20:25:10 +00:00
v3: src/ is documented like api/ and shared/, and the reference covers the whole repository — the frontend had no module docs, JSDoc on some components and hooks and not on others, props typed inline in half the components and in private interfaces in the rest, // comments on declarations, and no return types on 147 exported functions, so deno doc --lint over src/ found 637 errors. The rule api/ follows is applied to every non-test module of src/: a /** … @module */ block, JSDoc on every export and every field of an exported interface, /** */ for declarations and // for notes, verbs for functions ("Renders…" for a component), parameters named in the prose, {@link} only for symbols in scope, and a page's JSDoc names its route and who may see it (App.tsx's module doc lists the route table). For deno doc to show them, every exported function states its return type: components React.JSX.Element (or | null, or a portal), memo and forwardRef components their exotic types, contexts React.Context<…>, and a hook returning several things a documented Use<Name>Result interface (useAuth, useRequiredAuth, useFading, useEmojiTrigger, useMentionAutocomplete, useDumpCardLink, usePlayRichContent; FeedCacheResult, YouTubePlaylistControls and UseApiFormReturn are renamed to match). Every component that takes props takes an exported, documented <Name>Props interface — the 64 inline prop types are lifted into one, DumpFeed gets DumpFeedProps extending MainFeedProps. React types go through the global React namespace: the four default imports of react are gone, since deno doc can't follow them. Two duplicates go too: useMentionAutocomplete's UserResult was shared/'s UserSuggestion, and JournalCard re-exported JournalShape for no one; i18n.ts no longer re-exports Lingui's i18n, which its three users now import from @lingui/core. deno task doc and doc:lint now take src/ (but its tests and the generated locales), the index page gains a Frontend section linking every module by layer and domain, mod.test.ts walks src/ as well, and the docs workflow runs on changes to src/. deno doc doesn't document npm packages, so it takes a type imported from one — react-hook-form's FieldValues in the form fields, frimousse's SkinTone — for a private one; doc:lint now goes through doc-lint.ts, which fails on every diagnostic but those, 25 of them. The README states the frontend's part of the rule. No behaviour changes; 205 backend and 628 frontend tests pass, and tsc, deno check and deno lint are clean.
2026-09-28 07:31:22 +00:00
v3: the backend is documented for deno doc, one way in every file, and the docs build and publish themselves — comments were in three shapes: file headers as // lines, as an untagged /** */ (which deno doc drops, only an @module block counts) or missing; 43 exports described by a // deno doc can't see, two JSDoc blocks sitting on the wrong declaration and one duplicated; endpoints commented in 5 of 18 route files, three ways, on router.get() calls no doc tool reads. The rule, applied to every non-test module of api/, migrations included: each opens with a /** … @module */ block; every export has JSDoc, and so does every field of an exported interface; a comment describing a declaration is /** */, // only notes code; functions open with a verb, everything else with a noun phrase; parameters are named in the prose, {@link} only for symbols in scope; whatever throws to its caller says so with @throws (for an APIException, the statuses and when); a router's JSDoc lists its endpoints, method and path, who may call it, what it does. deno doc --lint goes from 528 errors to none in api/ (shared/ keeps its 385; it is next). The lint also wanted explicit types on config.ts's settings and a few return types, JsonLdResult exported, and the routers typed Router. deno task doc builds the HTML reference into docs/ from every module of api/ and shared/ but the tests, deno task doc:lint lints it; the index page is the module doc of a root mod.ts (deno.json's exports), which introduces the backend, shared/ and the frontend to come and links every module, and mod.test.ts fails when one is missing. The docs workflow builds it on every push to main that touches them and publishes it to the gh-pages branch, like publish-assistant's. Two defects found along the way are fixed: a private playlist's cover was served to anyone who had its id or slug, and now needs its owner's media token, as private dump files do (playlistImageUrl adds it for private playlists only, so public cover URLs stay the same for everyone); og.ts and static.ts each filled index.html's placeholders, and now share lib/index-html.ts, so the OG middleware picks up a rebuilt index.html instead of keeping the first it read. The README states the documentation rule. 205 backend and 628 frontend tests pass.
2026-09-27 21:07:40 +00:00
v3: src/ is documented like api/ and shared/, and the reference covers the whole repository — the frontend had no module docs, JSDoc on some components and hooks and not on others, props typed inline in half the components and in private interfaces in the rest, // comments on declarations, and no return types on 147 exported functions, so deno doc --lint over src/ found 637 errors. The rule api/ follows is applied to every non-test module of src/: a /** … @module */ block, JSDoc on every export and every field of an exported interface, /** */ for declarations and // for notes, verbs for functions ("Renders…" for a component), parameters named in the prose, {@link} only for symbols in scope, and a page's JSDoc names its route and who may see it (App.tsx's module doc lists the route table). For deno doc to show them, every exported function states its return type: components React.JSX.Element (or | null, or a portal), memo and forwardRef components their exotic types, contexts React.Context<…>, and a hook returning several things a documented Use<Name>Result interface (useAuth, useRequiredAuth, useFading, useEmojiTrigger, useMentionAutocomplete, useDumpCardLink, usePlayRichContent; FeedCacheResult, YouTubePlaylistControls and UseApiFormReturn are renamed to match). Every component that takes props takes an exported, documented <Name>Props interface — the 64 inline prop types are lifted into one, DumpFeed gets DumpFeedProps extending MainFeedProps. React types go through the global React namespace: the four default imports of react are gone, since deno doc can't follow them. Two duplicates go too: useMentionAutocomplete's UserResult was shared/'s UserSuggestion, and JournalCard re-exported JournalShape for no one; i18n.ts no longer re-exports Lingui's i18n, which its three users now import from @lingui/core. deno task doc and doc:lint now take src/ (but its tests and the generated locales), the index page gains a Frontend section linking every module by layer and domain, mod.test.ts walks src/ as well, and the docs workflow runs on changes to src/. deno doc doesn't document npm packages, so it takes a type imported from one — react-hook-form's FieldValues in the form fields, frimousse's SkinTone — for a private one; doc:lint now goes through doc-lint.ts, which fails on every diagnostic but those, 25 of them. The README states the frontend's part of the rule. No behaviour changes; 205 backend and 628 frontend tests pass, and tsc, deno check and deno lint are clean.
2026-09-28 07:31:22 +00:00
v3: src/ is documented like api/ and shared/, and the reference covers the whole repository — the frontend had no module docs, JSDoc on some components and hooks and not on others, props typed inline in half the components and in private interfaces in the rest, // comments on declarations, and no return types on 147 exported functions, so deno doc --lint over src/ found 637 errors. The rule api/ follows is applied to every non-test module of src/: a /** … @module */ block, JSDoc on every export and every field of an exported interface, /** */ for declarations and // for notes, verbs for functions ("Renders…" for a component), parameters named in the prose, {@link} only for symbols in scope, and a page's JSDoc names its route and who may see it (App.tsx's module doc lists the route table). For deno doc to show them, every exported function states its return type: components React.JSX.Element (or | null, or a portal), memo and forwardRef components their exotic types, contexts React.Context<…>, and a hook returning several things a documented Use<Name>Result interface (useAuth, useRequiredAuth, useFading, useEmojiTrigger, useMentionAutocomplete, useDumpCardLink, usePlayRichContent; FeedCacheResult, YouTubePlaylistControls and UseApiFormReturn are renamed to match). Every component that takes props takes an exported, documented <Name>Props interface — the 64 inline prop types are lifted into one, DumpFeed gets DumpFeedProps extending MainFeedProps. React types go through the global React namespace: the four default imports of react are gone, since deno doc can't follow them. Two duplicates go too: useMentionAutocomplete's UserResult was shared/'s UserSuggestion, and JournalCard re-exported JournalShape for no one; i18n.ts no longer re-exports Lingui's i18n, which its three users now import from @lingui/core. deno task doc and doc:lint now take src/ (but its tests and the generated locales), the index page gains a Frontend section linking every module by layer and domain, mod.test.ts walks src/ as well, and the docs workflow runs on changes to src/. deno doc doesn't document npm packages, so it takes a type imported from one — react-hook-form's FieldValues in the form fields, frimousse's SkinTone — for a private one; doc:lint now goes through doc-lint.ts, which fails on every diagnostic but those, 25 of them. The README states the frontend's part of the rule. No behaviour changes; 205 backend and 628 frontend tests pass, and tsc, deno check and deno lint are clean.
2026-09-28 07:31:22 +00:00
v3: api/ gets one layout rule and keeps to it, and whatever crosses the wire is defined once, in shared/ — api/model/interfaces.ts and src/model.ts declared the same 40 types by hand and had drifted: the error envelope, email required on one side and optional on the other, Invitee/InviteTreeEntry and LinkPreview/UrlPreviewResponse for one shape each. The validation limits, the role permissions, the chat length cap, the @mention pattern and the Bandcamp and YouTube player responses were copied too, behind "keep in sync" comments. shared/ now holds them once, one file per domain (api, category, chat, comment, dump, follow, mention, notification, permissions, player, playlist, search, user, validation, ws), and imports nothing from either half. The server sends Dates; the client reads Wire<T>, the same type with every Date an ISO string, and turns it back with src/utils/shared/deserialize.ts. APIErrorCode becomes a const object, since the frontend's erasableSyntaxOnly rejects an enum. The shared types showed two real gaps: the avatar upload and GET /api/users/me return the full self user and the frontend read them as public users, and a stored session may predate media tokens, so the client keeps it as its own Session type. The backend rule: each folder holds one kind of module, each file is named after its domain. routes/ parse and answer; services/ hold the logic and every SQL query (lib/auth.ts's token check moves to user-service); remote/ is the only place that fetches from other sites — the guarded fetch and the HTML scraping out of rich-content-service, public-address, the Bandcamp and YouTube resolvers, the link providers, the image proxy (fetch, checks and cache, out of routes/preview.ts) and the Twemoji glyphs (out of lib/site-icons.ts). model/ holds per table a row type, its guard and its mappers; validators/ the request guards; db/connection.ts the handle and transaction(), db/startup.ts the purges and default admin that db.ts ran on import, called at the same moment. Tests mirror the source one file per module, test/<layer>/<module>.test.ts, with shared/'s under shared/test/ (deno task test runs both); the test that covered the bandcamp and soundcloud providers together is two now, and the backend can() has its own, so 202 tests. deno lint src/ is clean too: window.* becomes globalThis.*, and test bodies and act() callbacks that never awaited lose their async. The README states the rule. No behaviour changes; the catalogues only move their source references.
2026-09-27 20:19:48 +00:00
v3: the frontend ships compressed, caches for good, and stops reaching for other people's servers — a review of the bundle's size found the server was the biggest cost. Oak's send() compressed nothing and sent every hashed chunk with max-age=0, so a first visit to the home page moved about 710 KB of JS and CSS where 200 KB would do, and every later one revalidated some thirty files. The build now writes .br and .gz next to every JS, CSS, JSON and SVG file (a precompress plugin, brotli 11), which send() already serves when the browser accepts them; /assets and /emojibase are cached for a year as immutable, index.html is no-cache, and static responses carry Vary: Accept-Encoding. A missing file under either prefix is a 404 rather than index.html with a 200, which an open tab after a deploy used to try to run as a script; the app now reloads on vite:preloadError, at most once a minute. manualChunks sent everything in node_modules to one vendor chunk, so react-hook-form and frimousse loaded with every page though only the lazy form and editor chunks use them, and its markdown rule missed hast, vfile, unified and property-information while every feed loads Markdown through DumpCard anyway; only react, react-dom, scheduler and react-router get a chunk of their own now, the rest is left to the bundler. Up-front JS goes from 133 to 96 KB gzip, the home page from 178 to 160. Fonts: index.html blocked rendering on a Google Fonts stylesheet, opening two more hosts and handing each visitor's IP to Google; they come from @fontsource now, with src/fonts.css declaring only the latin and latin-ext faces (the packages' own CSS declares every subset, which added 6 KB gzip to the stylesheet), so Vite hashes the woff2 files into /assets and a browser still only downloads its theme's font. The families are the variable builds ("Saira Variable" and so on); Fraunces keeps its opsz, SOFT and WONK axes. Emoji: frimousse fetched emojibase-data@latest from jsDelivr, unpinned and third-party; emojibase-data 17.0.0 is a dependency now, its data and messages for each lingui locale are emitted under /emojibase/17.0.0 (served from node_modules in dev), and the picker is pointed there.
2026-09-27 11:44:08 +00:00
2026-03-15 17:15:46 +00:00
v3: src/ is documented like api/ and shared/, and the reference covers the whole repository — the frontend had no module docs, JSDoc on some components and hooks and not on others, props typed inline in half the components and in private interfaces in the rest, // comments on declarations, and no return types on 147 exported functions, so deno doc --lint over src/ found 637 errors. The rule api/ follows is applied to every non-test module of src/: a /** … @module */ block, JSDoc on every export and every field of an exported interface, /** */ for declarations and // for notes, verbs for functions ("Renders…" for a component), parameters named in the prose, {@link} only for symbols in scope, and a page's JSDoc names its route and who may see it (App.tsx's module doc lists the route table). For deno doc to show them, every exported function states its return type: components React.JSX.Element (or | null, or a portal), memo and forwardRef components their exotic types, contexts React.Context<…>, and a hook returning several things a documented Use<Name>Result interface (useAuth, useRequiredAuth, useFading, useEmojiTrigger, useMentionAutocomplete, useDumpCardLink, usePlayRichContent; FeedCacheResult, YouTubePlaylistControls and UseApiFormReturn are renamed to match). Every component that takes props takes an exported, documented <Name>Props interface — the 64 inline prop types are lifted into one, DumpFeed gets DumpFeedProps extending MainFeedProps. React types go through the global React namespace: the four default imports of react are gone, since deno doc can't follow them. Two duplicates go too: useMentionAutocomplete's UserResult was shared/'s UserSuggestion, and JournalCard re-exported JournalShape for no one; i18n.ts no longer re-exports Lingui's i18n, which its three users now import from @lingui/core. deno task doc and doc:lint now take src/ (but its tests and the generated locales), the index page gains a Frontend section linking every module by layer and domain, mod.test.ts walks src/ as well, and the docs workflow runs on changes to src/. deno doc doesn't document npm packages, so it takes a type imported from one — react-hook-form's FieldValues in the form fields, frimousse's SkinTone — for a private one; doc:lint now goes through doc-lint.ts, which fails on every diagnostic but those, 25 of them. The README states the frontend's part of the rule. No behaviour changes; 205 backend and 628 frontend tests pass, and tsc, deno check and deno lint are clean.
2026-09-28 07:31:22 +00:00
v3: src/ is documented like api/ and shared/, and the reference covers the whole repository — the frontend had no module docs, JSDoc on some components and hooks and not on others, props typed inline in half the components and in private interfaces in the rest, // comments on declarations, and no return types on 147 exported functions, so deno doc --lint over src/ found 637 errors. The rule api/ follows is applied to every non-test module of src/: a /** … @module */ block, JSDoc on every export and every field of an exported interface, /** */ for declarations and // for notes, verbs for functions ("Renders…" for a component), parameters named in the prose, {@link} only for symbols in scope, and a page's JSDoc names its route and who may see it (App.tsx's module doc lists the route table). For deno doc to show them, every exported function states its return type: components React.JSX.Element (or | null, or a portal), memo and forwardRef components their exotic types, contexts React.Context<…>, and a hook returning several things a documented Use<Name>Result interface (useAuth, useRequiredAuth, useFading, useEmojiTrigger, useMentionAutocomplete, useDumpCardLink, usePlayRichContent; FeedCacheResult, YouTubePlaylistControls and UseApiFormReturn are renamed to match). Every component that takes props takes an exported, documented <Name>Props interface — the 64 inline prop types are lifted into one, DumpFeed gets DumpFeedProps extending MainFeedProps. React types go through the global React namespace: the four default imports of react are gone, since deno doc can't follow them. Two duplicates go too: useMentionAutocomplete's UserResult was shared/'s UserSuggestion, and JournalCard re-exported JournalShape for no one; i18n.ts no longer re-exports Lingui's i18n, which its three users now import from @lingui/core. deno task doc and doc:lint now take src/ (but its tests and the generated locales), the index page gains a Frontend section linking every module by layer and domain, mod.test.ts walks src/ as well, and the docs workflow runs on changes to src/. deno doc doesn't document npm packages, so it takes a type imported from one — react-hook-form's FieldValues in the form fields, frimousse's SkinTone — for a private one; doc:lint now goes through doc-lint.ts, which fails on every diagnostic but those, 25 of them. The README states the frontend's part of the rule. No behaviour changes; 205 backend and 628 frontend tests pass, and tsc, deno check and deno lint are clean.
2026-09-28 07:31:22 +00:00
v3: src/ is documented like api/ and shared/, and the reference covers the whole repository — the frontend had no module docs, JSDoc on some components and hooks and not on others, props typed inline in half the components and in private interfaces in the rest, // comments on declarations, and no return types on 147 exported functions, so deno doc --lint over src/ found 637 errors. The rule api/ follows is applied to every non-test module of src/: a /** … @module */ block, JSDoc on every export and every field of an exported interface, /** */ for declarations and // for notes, verbs for functions ("Renders…" for a component), parameters named in the prose, {@link} only for symbols in scope, and a page's JSDoc names its route and who may see it (App.tsx's module doc lists the route table). For deno doc to show them, every exported function states its return type: components React.JSX.Element (or | null, or a portal), memo and forwardRef components their exotic types, contexts React.Context<…>, and a hook returning several things a documented Use<Name>Result interface (useAuth, useRequiredAuth, useFading, useEmojiTrigger, useMentionAutocomplete, useDumpCardLink, usePlayRichContent; FeedCacheResult, YouTubePlaylistControls and UseApiFormReturn are renamed to match). Every component that takes props takes an exported, documented <Name>Props interface — the 64 inline prop types are lifted into one, DumpFeed gets DumpFeedProps extending MainFeedProps. React types go through the global React namespace: the four default imports of react are gone, since deno doc can't follow them. Two duplicates go too: useMentionAutocomplete's UserResult was shared/'s UserSuggestion, and JournalCard re-exported JournalShape for no one; i18n.ts no longer re-exports Lingui's i18n, which its three users now import from @lingui/core. deno task doc and doc:lint now take src/ (but its tests and the generated locales), the index page gains a Frontend section linking every module by layer and domain, mod.test.ts walks src/ as well, and the docs workflow runs on changes to src/. deno doc doesn't document npm packages, so it takes a type imported from one — react-hook-form's FieldValues in the form fields, frimousse's SkinTone — for a private one; doc:lint now goes through doc-lint.ts, which fails on every diagnostic but those, 25 of them. The README states the frontend's part of the rule. No behaviour changes; 205 backend and 628 frontend tests pass, and tsc, deno check and deno lint are clean.
2026-09-28 07:31:22 +00:00
v3: the frontend ships compressed, caches for good, and stops reaching for other people's servers — a review of the bundle's size found the server was the biggest cost. Oak's send() compressed nothing and sent every hashed chunk with max-age=0, so a first visit to the home page moved about 710 KB of JS and CSS where 200 KB would do, and every later one revalidated some thirty files. The build now writes .br and .gz next to every JS, CSS, JSON and SVG file (a precompress plugin, brotli 11), which send() already serves when the browser accepts them; /assets and /emojibase are cached for a year as immutable, index.html is no-cache, and static responses carry Vary: Accept-Encoding. A missing file under either prefix is a 404 rather than index.html with a 200, which an open tab after a deploy used to try to run as a script; the app now reloads on vite:preloadError, at most once a minute. manualChunks sent everything in node_modules to one vendor chunk, so react-hook-form and frimousse loaded with every page though only the lazy form and editor chunks use them, and its markdown rule missed hast, vfile, unified and property-information while every feed loads Markdown through DumpCard anyway; only react, react-dom, scheduler and react-router get a chunk of their own now, the rest is left to the bundler. Up-front JS goes from 133 to 96 KB gzip, the home page from 178 to 160. Fonts: index.html blocked rendering on a Google Fonts stylesheet, opening two more hosts and handing each visitor's IP to Google; they come from @fontsource now, with src/fonts.css declaring only the latin and latin-ext faces (the packages' own CSS declares every subset, which added 6 KB gzip to the stylesheet), so Vite hashes the woff2 files into /assets and a browser still only downloads its theme's font. The families are the variable builds ("Saira Variable" and so on); Fraunces keeps its opsz, SOFT and WONK axes. Emoji: frimousse fetched emojibase-data@latest from jsDelivr, unpinned and third-party; emojibase-data 17.0.0 is a dependency now, its data and messages for each lingui locale are emitted under /emojibase/17.0.0 (served from node_modules in dev), and the picker is pointed there.
2026-09-27 11:44:08 +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

shared/            # Imported by both halves: API and socket types, limits, permissions
  test/            # Tests of shared/ (Deno): test/<module>.test.ts
api/
  main.ts          # Entry point — Oak application, middleware, routes
  config.ts        # Environment variables
  middleware/      # Oak middleware: errors, auth, OG tags
  routes/          # One router per URL prefix: HTTP + WebSocket, no SQL
  services/        # Business logic and every SQL query
  remote/          # Everything that fetches from other sites
    providers/     # Link metadata providers (YouTube, SoundCloud, Bandcamp, …)
  model/           # Per table: row type, row guard, row ↔ API mappers
  validators/      # Per domain: request body guards
  lib/             # Helpers with no SQL and no outbound fetch: JWT, slugs, files, …
  db/
    connection.ts  # The database handle and transaction()
    startup.ts     # Housekeeping run when the database opens
    schema.sql     # Database schema
    init.ts        # First-run database initialisation
    migrate.ts     # Migration runner
    migrations/    # Versioned schema migrations
  test/            # Backend tests (Deno): test/<layer>/<module>.test.ts
  sql/
    gerbeur.db     # SQLite database (not committed)
  uploads/         # User-uploaded files — avatars, dumps, thumbnails, … (not committed)
src/               # React frontend (Vite)
  config/          # API base URL, feed tabs, player mode
  pages/           # One component per route, nothing else
  components/      # React components only (.tsx)
  contexts/        # FooContext.ts + FooProvider.tsx pairs
  hooks/           # Every hook (useFoo.ts)
  utils/           # Every non-React module: pure helpers, storage, API glue
  test/            # Frontend tests (Vitest): test/<layer>/<domain>/<Module>.test.tsx
  locales/         # Lingui message catalogues (en, fr)
  themes/          # Per-theme CSS files
  i18n.ts          # Lingui runtime setup
public/            # Static assets (favicon, manifest, service worker)

Whatever crosses the wire is defined once, in shared/: the API envelope (shared/api.ts), each domain's types and request bodies (shared/dump.ts, shared/user.ts, …), the socket protocol (shared/ws.ts), the validation limits and the role permissions. The server sends Dates; the client reads Wire<T>, the same type with every Date an ISO string, and turns it back with src/utils/shared/deserialize.ts. shared/ imports nothing from either half.

The backend is split by layer, one kind of module per folder, and each file is named after its domain: the dumps are routes/dumps.ts, services/dump-service.ts, model/dump.ts and validators/dump.ts. Routes parse and answer, services hold the logic and the SQL, remote/ is the only place that fetches from other sites. Tests mirror the source one file per module, so the tests of services/chat-service.ts are in test/services/chat-service.test.ts.

All three parts — api/, shared/ and src/ — are documented for deno doc, the same way in every file. Each module opens with a /** … @module */ block saying what it is for. Every export has a JSDoc comment, and so does every field of an exported interface; a comment that describes a declaration is always /** */, and // is only for notes on the code inside. Functions open with a verb ("Returns…", "Checks…", "Renders…" for a component), everything else with a noun phrase. Parameters are named in the prose, in backticks. {@link} points at symbols the file already has in scope. A function that can throw to its caller says so with @throws: for an APIException, the statuses and when. A router's JSDoc lists its endpoints, one line each: method and path, who may call it, what it does; a page's names its route and who may see it.

In the frontend, every exported function states its return type, so the reference can show it: a component returns React.JSX.Element (or | null), a hook returning several things returns a documented Use<Name>Result interface. A component that takes props takes an exported <Name>Props interface, each prop documented. React's types are used through the global React namespace rather than a default import of react, which deno doc can't follow.

deno task doc builds the HTML reference into docs/ from every module of api/, shared/ and src/ but the tests (and the generated src/locales/). Its index page is the module doc of mod.ts at the root, which introduces the repository's three parts and links every documented module (mod.test.ts checks it), and the docs workflow publishes it on every push to main that touches them. deno task doc:lint checks the rules the linter can see, through doc-lint.ts: deno doc doesn't document npm packages, so it takes a type imported from one (react-hook-form's FieldValues, say) for a private one, and those references alone are let through.

Each of the frontend's components/, contexts/, hooks/ and utils/ is split into the same domain folders — auth, category, chat, comment, dump, editor, feed, form, layout, player, playlist, profile, social, theme, ws — plus shared/ for what belongs to no single domain. So the chat lives in components/chat/, contexts/chat/, hooks/chat/ and utils/chat/, and its tests in test/<same path>. Files are imported directly (no index.ts barrels), and everything uses named exports.

Description
🚚 tiny community engine for sharing bookmarks
https://pub.sqrt.fr/vincent/gerbeur/
Readme MIT 11 MiB
Languages
TypeScript 87.9%
CSS 10.4%
JavaScript 1.5%