Merges both files and adds full endpoint reference, .env variable table, setup guides (Spotify, Certbot, Obsidian/Quartz, Icecast), deprecated endpoint notes, and updated architecture with all services. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
My Web
Welcome to the source code for my website! Please contact me if you would like to collaborate and thank you for visiting.
This website is self-hosted on my Raspberry Pi. Any interference and the killswitch will activate and stop the UK national grid power system so please don't tamper with my domain :).
Architecture
All services run in Docker containers orchestrated by Docker Compose behind Nginx as a reverse proxy on a single bridge network.
vue ── Frontend build (outputs dist to shared volume)
nginx (80, 443) ── Frontend SPA + Reverse Proxy
backend (8080) ── Go API (GraphQL + REST)
db (5432) ── PostgreSQL 16
icecast2 (8000) ── Audio Streaming (Icecast2 + Liquidsoap)
gitea (3000) ── Self-Hosted Git
quartz (8080) ── Obsidian Notes Publisher (Quartz v4.4.0)
searxng (8080) ── Meta Search Engine
hasura (8080) ── Hasura GraphQL Engine
certbot ── SSL Certificate Management (disabled in dev)
Tech Stack
Frontend - Vue 3, Vite, Tailwind CSS v4, Pinia, Vue Router, markdown-it (wikilinks + KaTeX), Rust/WASM
Backend - Go (Gin), gqlgen (GraphQL), GORM, PostgreSQL, JWT auth, WebSockets
Integrations - Spotify API, Steam API, Anthropic Claude API, Icecast2
Infrastructure - Docker Compose, Nginx, Let's Encrypt (Certbot), Gitea + Act Runner
Features
- Spotify integration (currently playing, recently played)
- Steam integration (online status, recent games)
- Obsidian note viewer via Quartz
- Live radio streaming via Icecast2 + Liquidsoap
- Real-time chat over WebSockets with image/video uploads
- Blog with admin panel (CRUD)
- Activity and rowing session tracking
- Fan shrines (GTO, Evangelion, Demoman, Skip Skip Benben)
- Self-hosted Git (Gitea) with CI/CD and commit feed on homepage
- Claude AI integration
- Printable CV with role-specific sections
- SearXNG meta search engine
- Hasura GraphQL console
- Landing page with animated stamps section
- Route transitions (slide/fade) and performance optimizations (gzip, WOFF2 fonts, lazy loading)
Pages
| Route | Description |
|---|---|
/ |
Landing page |
/stp |
Home dashboard with grid layout |
/admin |
Admin panel (authenticated) |
/cv |
Curriculum Vitae (printable) |
/bookmarks |
Bookmarks |
/notes/:path |
Obsidian note viewer (via Quartz) |
/shrines |
Fan shrine index + individual shrines |
API
GraphQL
Endpoint: POST /api/graphql
Playground: GET /api/graphql (when GQL_PLAYGROUND=true)
| Operation | Type | Description |
|---|---|---|
users |
Query | Get all users |
user(id) |
Query | Get user by ID |
me |
Query | Get authenticated user |
posts |
Query | Get all posts |
post(id) |
Query | Get post by ID |
activities |
Query | Get all activities |
favorites |
Query | Get all favorites |
rowingSessions |
Query | Get all rowing sessions |
messages |
Query | Get all messages |
spotifyListening |
Query | Currently playing track |
spotifyRecent |
Query | Recently played tracks |
giteaFeed |
Query | Latest Gitea activity |
steamStatus |
Query | Steam online status |
login |
Mutation | Authenticate user |
logout |
Mutation | Logout |
refreshToken |
Mutation | Refresh auth token |
createPost / updatePost / deletePost |
Mutation | Post CRUD (admin) |
createUser / deleteUser |
Mutation | User management (admin) |
setUserAdmin |
Mutation | Toggle admin status |
createFavorite |
Mutation | Add favorite (admin) |
createActivity |
Mutation | Add activity (admin) |
REST Endpoints
Auth
| Method | Path | Description |
|---|---|---|
| POST | /api/auth/login |
Login (rate limited: 5/min) |
| POST | /api/auth/refresh |
Refresh token |
| GET | /api/auth/check |
Check token validity |
| POST | /api/auth/logout |
Logout |
Spotify
| Method | Path | Description |
|---|---|---|
| GET | /api/spotify/callback |
OAuth callback |
| GET | /api/spotify/listening |
Currently playing |
| GET | /api/spotify/recent |
Recently played |
Public
| Method | Path | Description |
|---|---|---|
| GET | /api/favorites |
Get all favorites |
| GET | /api/rowing |
Get all rowing sessions |
| GET | /api/activity |
Get all activities |
| GET | /api/posts |
Get all posts |
| GET | /api/posts/:id |
Get post by ID |
| GET | /api/user |
Get all users |
| GET | /api/user/:id |
Get user by ID |
Protected (auth required)
| Method | Path | Description |
|---|---|---|
| POST | /api/messages/upload |
Upload message file (rate limited: 5/min) |
Admin (auth + admin required)
| Method | Path | Description |
|---|---|---|
| POST | /api/favorites |
Create favorite |
| POST | /api/rowing |
Create rowing session |
| POST | /api/activity |
Create activity |
| POST | /api/posts |
Create post |
| PUT | /api/posts/:id |
Update post |
| DELETE | /api/posts/:id |
Delete post |
| POST | /api/user |
Create user |
| PUT | /api/user/:id |
Update user |
| DELETE | /api/user/:id |
Delete user |
| PATCH | /api/user/:id/admin |
Set/unset admin |
| POST | /api/radio/upload |
Upload radio song |
| GET | /api/radio/songs |
List radio songs |
| DELETE | /api/radio/songs/:filename |
Delete radio song |
| PATCH | /api/radio/songs/:filename/disable |
Disable radio song |
| PATCH | /api/radio/songs/:filename/enable |
Enable radio song |
WebSocket
| Method | Path | Description |
|---|---|---|
| GET | /api/ws |
WebSocket chat (10s ping keepalive) |
Nginx Proxy Routes
| Route | Target | Notes |
|---|---|---|
/api |
backend:8080 | API (rate limited: 30r/s) |
/radio |
icecast:8000 | Audio streaming |
/gitea |
gitea:3000 | Git service |
/hasura |
hasura:8080 | GraphQL console + WebSocket |
/notes |
quartz:8080 | Obsidian notes |
/searxng |
searxng:8080 | Search engine |
/uploads |
local alias | User-uploaded files |
Deprecated Endpoints
Backend note API (GET /api/notes/*path) - The backend has a REST endpoint that serves note files directly from the mounted Obsidian vault. This is superseded by the Quartz service which now handles note rendering at /notes/. The backend endpoint still exists in code but is no longer the primary note serving path.
Setup
.env
Create a .env file in the project root. All services read from this file.
| Variable | Description |
|---|---|
POSTGRES_USER |
PostgreSQL username |
POSTGRES_PASSWORD |
PostgreSQL password |
POSTGRES_DB |
Main app database name |
POSTGRES_PORT |
PostgreSQL port (typically 5432) |
POSTGRES_HOST |
PostgreSQL hostname (use db for Docker) |
GITEA_HOST |
Gitea hostname (use gitea for Docker) |
GITEA_PORT |
Gitea HTTP port (typically 3000) |
GITEA_INTERNAL_TOKEN |
Gitea internal API token (generate with gitea generate secret INTERNAL_TOKEN) |
GITEA_LFS_JWT_SECRET |
Gitea LFS JWT secret (generate with gitea generate secret LFS_JWT_SECRET) |
GITEA_OAUTH2_JWT_SECRET |
Gitea OAuth2 JWT secret (generate with gitea generate secret JWT_SECRET) |
POSTGRES_GITEA_DB |
Gitea database name |
UPTIMEKUMA_HOST |
Uptime Kuma hostname (planned) |
UPTIMEKUMA_PORT |
Uptime Kuma port (planned) |
SEARXNG_HOST |
SearXNG hostname (use searxng for Docker) |
SEARXNG_PORT |
SearXNG port (typically 8080) |
SEARXNG_SECRET_KEY |
SearXNG secret key (random hex string) |
WALLABAG_HOST |
Wallabag hostname (planned) |
WALLABAG_PORT |
Wallabag port (planned) |
QUARTZ_HOST |
Quartz hostname (use quartz for Docker) |
QUARTZ_PORT |
Quartz port (typically 8080) |
GITEA_RUNNER_HOST |
Gitea runner hostname |
GITEA_RUNNER_NAME |
Gitea runner display name |
GITEA_RUNNER_REGISTRATION_TOKEN |
Token to register Gitea Actions runner |
BACKEND_PORT |
Backend port (typically 8080) |
BACKEND_HOST |
Backend hostname (use backend for Docker) |
BACKEND_SECRET |
JWT signing secret |
BACKEND_ENDPOINT |
API path prefix (typically /api) |
OBSIDIAN_DIR |
Absolute path to Obsidian vault on host machine |
SPOTIFY_CLIENT_ID |
Spotify app client ID |
SPOTIFY_CLIENT_SECRET |
Spotify app client secret |
SPOTIFY_REDIRECT_URI |
Spotify OAuth redirect (e.g. https://www.<DOMAIN>/api/spotify/callback) |
SPOTIFY_AUTH_STATE |
Arbitrary state string for Spotify OAuth |
ICECAST_SOURCE_PASSWORD |
Icecast source connection password |
ICECAST_RELAY_PASSWORD |
Icecast relay password |
ICECAST_ADMIN_USER |
Icecast admin username |
ICECAST_ADMIN_PASSWORD |
Icecast admin password |
ICECAST_HOST |
Icecast hostname (use icecast for Docker) |
ICECAST_PORT |
Icecast port (typically 8000) |
ICECAST_MOUNT |
Icecast mount point (e.g. /stream) |
LIQUIDSOAP_HARBOR_MOUNT |
Liquidsoap live input mount (e.g. /live) |
LIQUIDSOAP_HARBOR_PORT |
Liquidsoap harbor port (e.g. 8005) |
DOMAIN |
Production domain name |
EMAIL |
Email for Let's Encrypt registration |
CLAUDE_API_KEY |
Anthropic Claude API key |
STEAM_API_KEY |
Steam Web API key |
STEAM_ID |
Steam user ID |
HASURA_GRAPHQL_ADMIN_SECRET |
Hasura admin secret |
HASURA_HOST |
Hasura hostname (use hasura for Docker) |
HASURA_PORT |
Hasura port (typically 8080) |
SEED_DB |
Set to true to seed test data on startup |
Gitea Config
Copy from the template and fill in secrets:
cp gitea/config/app.ini.template gitea/config/app.ini
Populate LFS_JWT_SECRET, SECRET_KEY, INTERNAL_TOKEN, JWT_SECRET, and the database PASSWD. Alternatively, the Gitea entrypoint generates app.ini from the template using environment variables.
SearXNG Config
Copy from the template:
cp searxng/settings.yml.template searxng/settings.yml
The Docker entrypoint handles environment variable substitution (${BASE_URL}, ${SEARXNG_SECRET_KEY}) automatically, so manual setup is only needed when running outside Docker.
Spotify Token Setup
- Create a Spotify app at developer.spotify.com and set the redirect URI to
https://www.<DOMAIN>/api/spotify/callback - Set
SPOTIFY_CLIENT_ID,SPOTIFY_CLIENT_SECRET,SPOTIFY_REDIRECT_URI, andSPOTIFY_AUTH_STATEin.env - Start the stack, then visit the Spotify auth URL logged by the backend on startup to authorize the app
- After authorization, Spotify redirects to the callback endpoint which stores tokens at
/backend/token/spotify_token.json - Tokens are refreshed automatically; the file persists across container restarts via volume mount
Obsidian Notes Setup
- Set
OBSIDIAN_DIRin.envto the absolute path of your Obsidian vault on the host machine - The vault is mounted read-only into the Quartz container at
/quartz/content - Quartz builds a static site from the vault on startup and serves it at
/notes/ - The backend also mounts the vault at
/backend/notes(legacy, see deprecated endpoints above)
SSL Certificates (Certbot)
Initial setup (production):
- Set
DOMAINandEMAILin.env - On first run, Nginx starts with
nginx_setup.conf.templatewhich only serves the ACME challenge route at/.well-known/acme-challenge/ - Certbot requests a certificate via
certbot certonly --webroot - Once the certificate is issued to
certbot/conf/live/<DOMAIN>/, restart Nginx — it will detect the certs and switch to the fullnginx.conf.template - Certbot checks for renewal every 12 hours automatically
Dev mode: Nginx generates a self-signed certificate for localhost automatically.
Icecast Radio
Place at least one .mp3 file in icecast2/fallback_music/. Liquidsoap plays these as fallback when no live source is connected. Connect a live source to port 8001 on the LIQUIDSOAP_HARBOR_MOUNT.
Gitea Runner
- Download the
act_runnerbinary from Gitea releases and place ingitea-runner/ - Set
GITEA_RUNNER_REGISTRATION_TOKENin.env - The runner registers automatically on first startup
Dev Mode
Run the full stack over plain HTTP without SSL certificates:
docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build
This:
- Uses an HTTP-only Nginx config with all routing (SPA, backend proxy, radio, gitea, etc.)
- Generates a self-signed certificate for localhost
- Disables certbot
- Seeds the database with test data (
SEED_DB=true) - Enables GraphQL playground and introspection
- Enables Hasura console and dev mode
Visit http://localhost to test.
Frontend only (hot reload)
cd vue && npm run dev
Vite dev server proxies /api to localhost:8080, /gitea to localhost:3000, /radio to localhost:8000.
Untracked Files
These files are git-ignored and must be created manually:
| File | Notes |
|---|---|
.env |
See setup section above |
gitea/config/app.ini |
Copy from app.ini.template or let entrypoint generate it |
searxng/settings.yml |
Copy from settings.yml.template or let entrypoint generate it |
certbot/conf/, certbot/www/ |
Created automatically by certbot; use dev mode to skip |
backend/token/ |
Created automatically by Docker volume mount |
icecast2/fallback_music/*.mp3 |
Place at least one MP3 file |
gitea-runner/act_runner |
Download from Gitea releases |
gitea-runner/.runner |
Generated on first runner startup |
Future Ideas
- More Rust to WASM
- ML for chatboards
- Cache requests
- Design more webpages
- Calendar to show radio times
- Nice smooth function background and transitions
- Design shrines
- Redis (not really but practical experience)
