No description
  • Svelte 52.8%
  • TypeScript 40.7%
  • JavaScript 2.7%
  • CSS 1.4%
  • Dockerfile 0.9%
  • Other 1.5%
Find a file
Marvin Dalheimer 7b5dc9b5a8
Some checks failed
CI/CD / build-hltb (push) Has been skipped
CI/CD / scan-hltb (push) Has been skipped
CI/CD / build-postgres (push) Has been skipped
CI/CD / scan-postgres (push) Has been skipped
CI/CD / build-app (push) Has been skipped
CI/CD / build-container (push) Has been skipped
CI/CD / scan-app (push) Has been skipped
CI/CD / changelog-update (push) Has been skipped
CI/CD / release (push) Has been skipped
CI/CD / audit-python (push) Failing after 47s
CI/CD / lint (push) Failing after 23s
CI/CD / audit-pnpm (push) Failing after 23s
CI/CD / test (push) Failing after 24s
CI/CD / detect-changes (push) Failing after 1m18s
feat: prevent duplicate entries
2026-05-26 14:03:24 +02:00
.beads chore(beads): initialize bd issue tracker 2026-03-26 16:17:53 +01:00
.forgejo/workflows ci: remove LFS checkout configuration and install git-lfs explicitly where required 2026-05-14 21:28:38 +02:00
docker/postgres fix(ci): debug pipeline errors 2026-04-12 20:36:38 +02:00
drizzle feat: add stopped status to board_column enum and update UI components 2026-05-14 20:15:12 +02:00
hltb-service fix(ci): debug pipeline errors 2026-04-12 13:52:56 +02:00
messages feat: prevent duplicate entries 2026-05-26 14:03:24 +02:00
project.inlang chore: initial project scaffolding 2026-03-12 09:39:22 +01:00
scripts fix(ci): debug pipeline errors 2026-04-10 08:53:35 +02:00
src feat: prevent duplicate entries 2026-05-26 14:03:24 +02:00
static chore(lfs): convert existing static images to LFS pointers 2026-03-15 11:04:45 +01:00
.env.example feat(covers): add SteamGridDB cover picker and Steam Grid card layout 2026-03-22 19:24:10 +01:00
.gitattributes chore(lfs): track binary image files with Git LFS 2026-03-15 11:04:24 +01:00
.gitignore feat(server): updated csp policies for images to allow data URIs and CDN sources 2026-03-31 17:19:55 +02:00
.gitlab-ci.yml Here is a possible commit message: 2026-03-31 17:50:43 +02:00
.npmrc chore: initial project scaffolding 2026-03-12 09:39:22 +01:00
.trivyignore fix(security): fix app container trivy findings 2026-03-22 20:39:24 +01:00
CHANGELOG.md chore(changelog): update 2026-05-15 07:38:29 +00:00
CLAUDE.md Here is a possible commit message: 2026-03-31 17:50:43 +02:00
docker-compose.dev.yml fix(steam): use portrait grid covers (library_600x900.jpg) instead of header.jpg 2026-03-22 19:35:53 +01:00
docker-compose.hltb.yml chore: format all files with prettier; fix svelte-check warnings 2026-03-15 23:55:56 +01:00
docker-compose.migrate.yml feat: multi-stage Dockerfile and Docker Compose setup 2026-03-12 09:46:14 +01:00
docker-compose.prod.yml feat: security hardening, pg_cron cache cleanup, admin cache invalidation, polish 2026-03-13 10:56:45 +01:00
Dockerfile fix(ci): debug pipeline errors 2026-04-12 18:07:11 +02:00
drizzle.config.js Replace drizzle ts with js 2026-03-27 22:15:08 +01:00
eslint.config.js Fix security and lint issues 2026-03-27 19:06:32 +01:00
LICENSE chore: add PolyForm Noncommercial 1.0.0 license 2026-03-15 16:18:39 +01:00
package.json chore: update project dependencies to latest versions 2026-05-26 13:56:27 +02:00
PLAN.md chore: format all files with prettier; fix svelte-check warnings 2026-03-15 23:55:56 +01:00
playwright.config.ts chore: initial project scaffolding 2026-03-12 09:39:22 +01:00
pnpm-lock.yaml chore: update project dependencies to latest versions 2026-05-26 13:56:27 +02:00
pnpm-workspace.yaml chore: upgrade project dependencies, bump version to 1.0.0, and configure pnpm workspace builds 2026-05-16 21:56:14 +02:00
prettier.config.js chore: initial project scaffolding 2026-03-12 09:39:22 +01:00
README.md chore: format all files with prettier; fix svelte-check warnings 2026-03-15 23:55:56 +01:00
svelte.config.js chore: initial project scaffolding 2026-03-12 09:39:22 +01:00
tsconfig.json chore: initial project scaffolding 2026-03-12 09:39:22 +01:00
vite.config.ts fix: build errors and missing files in container build 2026-04-07 15:28:35 +02:00
vitest.config.ts chore: format all files with prettier; fix svelte-check warnings 2026-03-15 23:55:56 +01:00

Pile of Shame

A self-hosted PWA for managing your gaming backlog as a Kanban board.

Features: Kanban board (Backlog / Next / Playing / Paused / Done) · Steam library import · Game metadata via IGDB/RAWG · Game time estimates via HowLongToBeat (IGDB fallback) · Cover art with grayscale→color progress effect · Configurable column limits · Multi-user with local accounts + OAuth (Google, Twitch, Steam) + custom OIDC (Keycloak, Authentik, …) · Full offline PWA with background sync · Material Design 3 · Dark/light mode · i18n (de/en)


Tech Stack

Area Technology
Framework SvelteKit (Node adapter)
Language TypeScript (strict mode)
Styling Tailwind CSS v4
Design System Material Design 3 (custom components, no external lib)
Database PostgreSQL 16
ORM Drizzle ORM
Auth Better-Auth
i18n Paraglide JS (compile-time, type-safe)
Drag & Drop svelte-dnd-action
PWA @vite-pwa/sveltekit + Workbox
Offline queue idb (IndexedDB)
Testing Vitest + Playwright
Deployment Docker + Docker Compose

Requirements

  • Node.js 22+
  • PostgreSQL 16+
  • Docker + Docker Compose (for containerized setup)

Environment Variables

Copy .env.example to .env and fill in the values:

cp .env.example .env
Variable Required Description
DATABASE_URL Yes PostgreSQL connection string
BETTER_AUTH_SECRET Yes Random secret (openssl rand -base64 32)
BETTER_AUTH_URL Yes Full base URL of the app (e.g. https://example.com)
ENABLED_AUTH_PROVIDERS Yes Comma-separated: local, google, twitch, steam, and/or custom OIDC provider IDs
TWITCH_CLIENT_ID No Twitch app credentials also enables IGDB game search
TWITCH_CLIENT_SECRET No
GOOGLE_CLIENT_ID No Google OAuth credentials
GOOGLE_CLIENT_SECRET No
STEAM_API_KEY No Steam Web API key enables Steam library import
RAWG_API_KEY No RAWG.io fallback when IGDB is not configured
HLTB_SERVICE_URL No URL of the optional HLTB microservice (see HLTB Service)
UPLOAD_DIR Yes Path for user-uploaded cover images (default /app/uploads)
MAX_UPLOAD_SIZE_MB No Upload size limit in MB (default 5)
ALLOW_PUBLIC_REGISTRATION No Set to true to allow public sign-up (default false)
SSO_AUTO_CREATE_ACCOUNT No Set to false to require a pre-existing account for SSO login (default true)

Note on IGDB: IGDB uses Twitch Client Credentials (server-to-server). Users do not need a Twitch account unless Twitch SSO is explicitly enabled.


Development

Local (no Docker)

# Install dependencies
npm install

# Start dev server with hot reload on http://localhost:5173
npm run dev

# Type-check
npm run check

# Lint
npm run lint

# Format
npm run format

With Docker

# Build and start dev container (hot reload on http://localhost:5173)
docker compose -f docker-compose.dev.yml up --build

The following paths are bind-mounted changes reload instantly without rebuilding the image: src/, static/, messages/, project.inlang/, svelte.config.js, vite.config.ts


Database

# Generate migration files after schema changes
npm run db:generate

# Apply migrations
npm run db:migrate

# Open Drizzle Studio (visual DB browser on http://localhost:4983)
npm run db:studio

Management CLI

A built-in CLI for managing users, database, and the application. It runs inside the Docker Compose network so the database never needs to be exposed on a host port.

# Production stack
docker compose -f docker-compose.prod.yml --profile manage run --rm manage

# Development stack
docker compose -f docker-compose.dev.yml --profile manage run --rm manage

# Pass a command directly (skips interactive menu)
docker compose -f docker-compose.prod.yml --profile manage run --rm manage user list
docker compose -f docker-compose.prod.yml --profile manage run --rm manage db migrate
docker compose -f docker-compose.prod.yml --profile manage run --rm manage setup

Available commands:

Command Description
setup First-time guided setup (env · migrations · seed · user)
env Validate environment configuration
user list List all users
user create Create a new user
user reset Reset a user's password
user info Show user details
user delete Delete a user and all their data
user promote Grant admin rights to a user
user demote Revoke admin rights from a user
db migrate Apply database migrations
db generate Generate migration files from schema
db seed Seed predefined tags
db backup Dump database to SQL file (pg_dump)
db restore Restore database from SQL file
db studio Open Drizzle Studio
db cache Clear IGDB cache
app Start / stop / logs (Docker)

Testing

# Unit tests (Vitest)
npm run test

# Unit tests in watch mode
npm run test:watch

# Coverage report
npm run test:coverage

# E2E tests (Playwright  requires a running app)
npm run test:e2e

Production

# 1. Copy and configure environment
cp .env.example .env
# Edit .env  at minimum set BETTER_AUTH_SECRET and BETTER_AUTH_URL

# 2. Build and start
docker compose -f docker-compose.prod.yml up -d --build

# 3. View logs
docker compose -f docker-compose.prod.yml logs -f app

# Stop
docker compose -f docker-compose.prod.yml down

App runs on http://localhost:3000 by default.

Manual build

npm ci
npm run build
NODE_ENV=production DATABASE_URL=... node build

HLTB Service

Game time estimates are provided by an optional Python microservice (hltb-service/) that queries HowLongToBeat using the howlongtobeatpy package. When the service is unavailable, the app falls back to IGDB's game_time_to_beats endpoint.

Enabling the HLTB service

Merge docker-compose.hltb.yml into your existing Compose stack:

# Development
docker compose -f docker-compose.dev.yml -f docker-compose.hltb.yml up --build

# Production
docker compose -f docker-compose.prod.yml -f docker-compose.hltb.yml up --build

The overlay file automatically sets HLTB_SERVICE_URL=http://hltb:8080 on the app service. No manual env var changes are needed.

Standalone (without Docker)

cd hltb-service
pip install -r requirements.txt
uvicorn app:app --host 0.0.0.0 --port 8080

Then set HLTB_SERVICE_URL=http://localhost:8080 in your .env.

API

Endpoint Description
GET /search?q=<title> Returns { main, extra, completionist } in hours
GET /health Health check

Project Structure

src/
├── lib/
│   ├── components/
│   │   ├── board/       # KanbanBoard, KanbanColumn, board-utils
│   │   ├── card/        # GameCard, CardProgressBar, card-utils
│   │   ├── modals/      # AddGameModal, EditGameModal
│   │   └── ui/          # Button, Input, Modal, SyncIndicator
│   ├── server/
│   │   ├── db/          # Drizzle schema, migrations, seed
│   │   ├── auth.ts      # Better-Auth config
│   │   ├── hltb.ts      # HLTB microservice client (optional)
│   │   ├── igdb.ts      # IGDB client + PostgreSQL cache (search + time-to-beat fallback)
│   │   ├── oidc.ts      # Custom OIDC provider config (parsed from env)
│   │   ├── rawg.ts      # RAWG fallback search
│   │   └── steam.ts     # Steam library import
│   ├── client/
│   │   ├── offline-queue.ts  # IndexedDB mutation queue
│   │   └── sync.ts           # Replay pending mutations on reconnect
│   └── types.ts
├── routes/
│   ├── api/             # REST endpoints (games, tags, search, import, auth)
│   ├── auth/            # Login, register, Steam OAuth pages
│   ├── settings/        # Account + board settings
│   └── +page.svelte     # Kanban board (root)
├── app.css              # Material Design 3 tokens + Tailwind theme
└── service-worker.ts    # Workbox PWA + background sync
messages/
├── en.json              # English strings
└── de.json              # German strings
hltb-service/            # Optional Python microservice for HowLongToBeat lookups
├── app.py               # FastAPI application
├── requirements.txt
└── Dockerfile

Auth Providers

Provider Setup
Local Always available email + password
Google Google Cloud Console → OAuth 2.0 Client ID, redirect URI: {BASE_URL}/api/auth/callback/google
Twitch Twitch Dev Console → redirect URI: {BASE_URL}/api/auth/callback/twitch. Same credentials enable IGDB game search.
Steam Steam API Key enables OpenID login + library import
Custom OIDC Any OIDC-compliant provider (Keycloak, Authentik, …) see below

Multiple providers can be linked to a single account via the account settings page.

Custom OIDC Providers (Keycloak, Authentik, …)

Any OIDC-compliant identity provider can be added. Add its identifier to ENABLED_AUTH_PROVIDERS, then set the three required env vars. The discovery document is fetched automatically from {ISSUER_URL}/.well-known/openid-configuration.

Env var pattern (replace {ID} with the provider identifier in SCREAMING_SNAKE_CASE):

OIDC_{ID}_CLIENT_ID=...
OIDC_{ID}_CLIENT_SECRET=...
OIDC_{ID}_ISSUER_URL=...
OIDC_{ID}_DISPLAY_NAME=...   # optional  button label, defaults to capitalized ID

Keycloak

  1. In Keycloak Admin → your realm → ClientsCreate client
  2. Client ID: pile-of-shame, Client authentication: on, Standard flow: on
  3. Add redirect URI: {BASE_URL}/api/auth/callback/keycloak
  4. Copy the client secret from the Credentials tab
ENABLED_AUTH_PROVIDERS=local,keycloak

OIDC_KEYCLOAK_CLIENT_ID=pile-of-shame
OIDC_KEYCLOAK_CLIENT_SECRET=<client-secret>
OIDC_KEYCLOAK_ISSUER_URL=https://keycloak.example.com/realms/myrealm
OIDC_KEYCLOAK_DISPLAY_NAME=Keycloak

Authentik

  1. In Authentik Admin → ApplicationsCreate an application
  2. Choose or create an OAuth2/OpenID Provider
  3. Set redirect URI: {BASE_URL}/api/auth/callback/authentik
  4. Set scopes: openid, profile, email
  5. Copy the Client ID and Client Secret from the provider
ENABLED_AUTH_PROVIDERS=local,authentik

OIDC_AUTHENTIK_CLIENT_ID=<client-id>
OIDC_AUTHENTIK_CLIENT_SECRET=<client-secret>
# The slug is the URL-identifier of your Authentik application provider
OIDC_AUTHENTIK_ISSUER_URL=https://auth.example.com/application/o/<slug>
OIDC_AUTHENTIK_DISPLAY_NAME=Authentik

Multiple OIDC providers can be active simultaneously: ENABLED_AUTH_PROVIDERS=local,google,keycloak,authentik


Internationalisation (i18n)

The app uses Paraglide JS for compile-time, type-safe i18n. Translations live in messages/ as plain JSON files and are compiled into strongly-typed TypeScript at build time no runtime lookups, full IDE autocompletion.

Supported locales

Code Language
en English (base locale)
de German

The active locale is determined by the URL prefix (/en/, /de/). The base locale (en) is also reachable at /.

Message files

Each locale has a flat JSON file at messages/{locale}.json:

{
	"$schema": "https://inlang.com/schema/inlang-message-format",
	"greeting": "Hello",
	"welcome_user": "Welcome, {name}!"
}
  • Keys are snake_case strings
  • Values are plain strings; placeholders use {variableName} syntax
  • The $schema line must be present

Adding or editing messages

  1. Add the key to both locale files messages/en.json and messages/de.json

    // messages/en.json
    "my_new_key": "Some text"
    
    // messages/de.json
    "my_new_key": "Irgendein Text"
    
  2. Recompile Paraglide this regenerates the typed message functions in src/lib/paraglide/:

    npx @inlang/paraglide-js compile --project ./project.inlang --outdir ./src/lib/paraglide
    

    The dev server (npm run dev) compiles automatically via the Vite plugin, so the manual step is only needed when running outside of Vite (e.g. standalone scripts or after pulling changes in Docker).

Using messages in components

Import the generated message module and call functions by key:

<script lang="ts">
	import * as m from '$paraglide/messages.js';
</script>

<h1>{m.greeting()}</h1><p>{m.welcome_user({ name: 'Alice' })}</p>
  • Every key becomes a zero-argument or typed-argument function TypeScript will error if you forget a required placeholder or mistype a key
  • The $paraglide alias resolves to src/lib/paraglide (configured in svelte.config.js)

Using messages in server-side code

The same import works in +page.server.ts, +server.ts, and any server utility:

import * as m from '$paraglide/messages.js';

return fail(400, { error: m.error_generic() });

Adding a new locale

  1. Add the locale code to project.inlang/settings.json:

    "locales": ["en", "de", "fr"]
    
  2. Create messages/fr.json with all keys translated

  3. Recompile Paraglide (see above)

  4. Paraglide and SvelteKit's routing will pick up the new locale automatically


Docker Stages

Stage Used by Purpose
deps internal npm ci only shared base layer
dev docker-compose.dev.yml Vite dev server, source bind-mounted for hot reload
manage --profile manage Management CLI + migration scripts
builder internal Full production build
runner docker-compose.prod.yml Lean runtime image, non-root user

Compose overlays

File Purpose
docker-compose.dev.yml Development stack
docker-compose.prod.yml Production stack
docker-compose.migrate.yml Database migrations (auto-included)
docker-compose.hltb.yml Optional HLTB microservice