No description
  • Kotlin 77.1%
  • Svelte 18.7%
  • TypeScript 2.9%
  • Shell 0.6%
  • CSS 0.3%
  • Other 0.4%
Find a file
Renny 401ce41630
Some checks are pending
Build and Publish / scan (push) Blocked by required conditions
Build and Publish / publish (push) Blocked by required conditions
Build and Publish / backend (push) Has started running
Build and Publish / frontend (push) Successful in 9m37s
Build and Publish / mcp (push) Has started running
Merge pull request 'Update dependency org.springframework.boot:spring-boot-starter-parent to v4.1.0' (#74) from renovate/org.springframework.boot-spring-boot-starter-parent-4.x into main
2026-06-11 00:15:42 +02:00
.forgejo/workflows Merge renovate/node-24.x: update workflow node-version to 24 2026-05-22 18:39:54 +02:00
docs Fix postgres 18 volume mount + add PG16→18 migration tooling 2026-05-23 09:09:17 +02:00
podradar-backend Update dependency org.springframework.boot:spring-boot-starter-parent to v4.1.0 2026-06-10 22:02:45 +00:00
podradar-frontend fix(frontend): force fresh apk index on upgrade to clear libxml2 CVE 2026-06-09 11:55:43 +02:00
podradar-mcp Update dependency @types/node to v25.9.2 2026-06-06 22:10:42 +00:00
scripts Fix postgres 18 volume mount + add PG16→18 migration tooling 2026-05-23 09:09:17 +02:00
.env fix: Docker setup — env file for credentials, non-root user, JVM flags, profile improvements 2026-03-29 18:40:30 +02:00
.env.example chore(compose): db + frontend memory limits [H4.4] 2026-05-07 01:05:06 +02:00
.gitignore Fix postgres 18 volume mount + add PG16→18 migration tooling 2026-05-23 09:09:17 +02:00
BACKUP.md docs: BACKUP.md with pg_dump procedure [G2] 2026-05-07 00:24:52 +02:00
CLAUDE.md docs: fix CLAUDE.md router description + A6 callout [H4.3] [H4.11] 2026-05-07 01:06:53 +02:00
docker-compose.yml Fix postgres 18 volume mount + add PG16→18 migration tooling 2026-05-23 09:09:17 +02:00
PLAN.md docs: mark Phase 4 complete, add Phase 4A/4B implementation patterns and gotchas 2026-03-29 23:23:31 +02:00
PLAN.md:Zone.Identifier first commit 2026-03-29 17:21:10 +02:00
README.md docs: operating in production README section [G3] 2026-05-07 00:25:22 +02:00
renovate.json Add renovate.json 2026-04-03 22:31:52 +02:00
ROADMAP.md docs: add ROADMAP.md for Phase 5/6/7 and v1 definition 2026-05-04 14:08:18 +02:00
TASKS.md test(config): regression tests for A1 (sync executor) + A3 (actuator security) [H1.5] 2026-05-07 02:23:31 +02:00
VERSION ci: semver-tagged images alongside :latest [G8] 2026-05-07 00:27:21 +02:00

PodRadar

PodRadar is a self-hosted, multi-user podcast discovery engine. It replaces opodsync as your GPodder-compatible sync server while adding a Tinder-style swipe mechanic that learns your taste and surfaces new shows worth trying. Rate episodes, swipe on recommendations, and let the taste engine do the rest — all without leaving AntennaPod. The Svelte 5 frontend runs in the browser; AntennaPod talks to the same backend over the standard GPodder API.


Requirements

  • Docker and Docker Compose
  • (Optional) PodcastIndex API key + secret — improves search and metadata
  • (Optional) OpenRouter API key — enables AI features (episode summaries, swipe recommendations)

Quick Start

git clone https://git.midasvo.nl/midas/podradar.git
cd podradar
cp .env.example .env
# Edit .env and fill in values (see table below)
docker compose up -d

Open http://localhost:3000 in your browser.

The first user to register is automatically granted admin access and approved. Subsequent registrations follow the PODRADAR_REGISTRATION_MODE setting.


Environment Variables

Variable Required Description
PODRADAR_DB_PASSWORD Required Password for the PostgreSQL database
OPENROUTER_API_KEY Optional Enables AI features via OpenRouter (episode summaries, swipe recommendations). Leave blank to disable.
PODCASTINDEX_API_KEY Optional PodcastIndex API key for improved podcast search and metadata
PODCASTINDEX_API_SECRET Optional PodcastIndex API secret (required alongside the key)
PODRADAR_REGISTRATION_MODE Optional Controls who can register. Default: APPROVAL_REQUIRED. Options: OPEN, APPROVAL_REQUIRED, INVITE_ONLY, CLOSED
PODRADAR_CORS_ALLOWED_ORIGINS Optional Comma-separated list of origins allowed to call the API in the docker profile. Default: http://localhost:5173,http://localhost:3000. Set this to your production frontend origin (e.g. https://podradar.example.com).
PODRADAR_COOKIE_SECURE Optional Whether the session cookie has the Secure flag in the docker profile. Default: true (requires HTTPS). Set to false only when running over plain HTTP on a private network.
PODRADAR_SWAGGER_ENABLED Optional Enable the OpenAPI / Swagger UI endpoints (/v3/api-docs, /swagger-ui) in the docker profile. Default: false. Set to true if you want to expose the API docs to authenticated callers.
PODRADAR_CSRF_ORIGIN_CHECK Optional Enable the strict Origin / Referer check on state-changing /api/** requests in the docker profile. Default: true. Cross-origin POST/PUT/PATCH/DELETE whose Origin is not in PODRADAR_CORS_ALLOWED_ORIGINS are rejected with HTTP 403. GPodder (/api/2/...) and /actuator/... are exempt because their clients use HTTP Basic and do not send Origin. Set to false only for debugging.

AntennaPod Setup

To use PodRadar as your sync server in AntennaPod:

  1. Open AntennaPod and go to Settings → Synchronization
  2. Select gpodder.net compatible
  3. Set the server to your PodRadar URL:
    • Local: http://localhost:8080
    • Remote: https://your-domain.com
  4. Enter your PodRadar username and password

AntennaPod will sync subscriptions and episode playback positions to PodRadar automatically.


Architecture

Layer Technology
Backend Kotlin / Spring Boot (Maven)
Frontend Svelte 5 SPA (static assets)
Database PostgreSQL
Reverse proxy nginx (inside Docker)

The backend exposes the GPodder Sync API for AntennaPod compatibility alongside a REST API consumed by the frontend. Liquibase manages database migrations. In production everything runs as Docker containers behind the nginx proxy.


Development

Backend

Runs against H2 in-memory database by default (no PostgreSQL needed).

cd podradar-backend
mvn spring-boot:run

The backend starts on http://localhost:8080.

Frontend

Proxies /api requests to the backend on localhost:8080.

cd podradar-frontend
npm install
npm run dev

The dev server starts on http://localhost:5173.

Tests

cd podradar-backend
mvn test

Tests run against H2 with PostgreSQL compatibility mode. No external services required.


Operations

Operating in production checklist

Before exposing PodRadar to the internet, walk through this list:

  • HTTPS in front. Terminate TLS with Caddy / nginx / Traefik. The session cookie is Secure + SameSite=Strict (B4); over plain HTTP browsers will refuse to send it. Set PODRADAR_COOKIE_SECURE=false only if you are deliberately running on a private HTTP network.
  • Lock down registration. After your own user is created, set PODRADAR_REGISTRATION_MODE=CLOSED and recycle the backend container. New signups will be refused at the API layer regardless of what the frontend exposes.
  • Firewall outbound traffic to private IP ranges. PodRadar's SSRF guard (B2) blocks most cases at the application layer, but defense-in-depth: add an iptables / nftables / Docker network rule that drops outbound traffic from the backend container to RFC1918 ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) and link-local (169.254.0.0/16). Allow only the public APIs you actually use.
  • Secure the env file. chmod 600 .env and own it as the user that runs docker compose. The file holds your DB password and any LLM/API credentials — 0644 leaks them to anyone who reads /etc/passwd.
  • Set up backups. See BACKUP.md. Without pg_dump snapshots, a corrupted volume = total loss.
  • Monitor health. Schedule a curl health-check against /actuator/health from your monitoring system (UptimeKuma, Healthchecks.io, statping). A 200 with {"status":"UP"} means the app + database + Liquibase are all healthy. Optionally tail docker compose logs -f podradar-backend into a log aggregator (Loki, Logtail) and alert on ERROR lines.
  • Review the rate-limit + LLM cap. PODRADAR_LLM_DAILY_TOKEN_CAP (default 1M) is sized for 12 users. Bump it before adding more or you'll see hard failures on Ask/summary calls. PODRADAR_RATELIMIT_LLM_PER_MINUTE defaults to 10 (free-tier safe) — raise it on paid plans.

Image rollback

CI publishes both :latest and a semver-tagged image (:vX.Y.Z, read from the repo-root VERSION file — see G8). If a deploy ships a regression, pin the previous version explicitly and redeploy:

# Edit docker-compose.yml — change
#   image: git.midasvo.nl/midas/podradar-backend:latest
# to
#   image: git.midasvo.nl/midas/podradar-backend:v0.0.X
# (and the same for podradar-frontend)
docker compose pull
docker compose up -d

When the fix lands on main, edit the file back to :latest and bump VERSION for the next release.

HTTPS / session cookies

Production deployments must terminate TLS. The session cookie is set with Secure and SameSite=Strict in the docker profile, which means it will not be transmitted over plain HTTP and won't be sent on cross-origin requests. If you front PodRadar with HTTPS via reverse proxy (Caddy, nginx, Traefik), this works out of the box.

If you genuinely need to run over HTTP for a private network, set PODRADAR_COOKIE_SECURE=false.

CSRF / Origin check

In the docker profile, every state-changing /api/** request (POST/PUT/PATCH/DELETE) must carry an Origin (or, as a fallback, a Referer) header that matches PODRADAR_CORS_ALLOWED_ORIGINS. Browsers attach Origin automatically on these methods, so legitimate same-origin SPA traffic is unaffected. Cross-site form-submission attacks are rejected with HTTP 403 even when SameSite=Strict is somehow bypassed. GPodder (/api/2/..., used by AntennaPod) and /actuator/... are exempt — their clients authenticate via HTTP Basic and do not send Origin. Disable with PODRADAR_CSRF_ORIGIN_CHECK=false only for diagnosis.

Health checks

The backend exposes Spring Boot Actuator endpoints:

  • GET /actuator/health — overall app + Liquibase + database health (returns {"status":"UP",...} or DOWN). Anonymous callers see only the top-level status; authenticated admins see component details.
  • GET /actuator/health/liveness — used by the Docker healthcheck.
  • GET /actuator/health/readiness — true once the app is ready to serve traffic.
  • GET /actuator/info — app metadata (anonymous).
  • GET /actuator/metrics/** — admin-only metrics.

Database migration recovery

Liquibase acquires a row in DATABASECHANGELOGLOCK while applying migrations. If a previous backend container was killed mid-migration, the lock can be left behind and a new backend will block on startup. The backend sets liquibase.changeLogLockWaitTimeInMinutes=5 as a JVM system property in PodradarApplication.main() (Spring Boot's LiquibaseProperties does not expose this key in YAML), so it fails fast after 5 minutes rather than hanging forever. Override with -Dliquibase.changeLogLockWaitTimeInMinutes=N if needed.

Symptom. docker compose up -d brings everything up, but podradar-backend never reaches healthy and docker compose ps shows it stuck or restarting.

Diagnosis. Look for the Liquibase wait message:

docker compose logs podradar-backend | grep -i liquibase

You will see lines like Waiting for changelog lock.... repeating, and eventually Could not acquire change log lock.

Recovery (last resort, manual). Only do this when you are certain no other Liquibase process is running against the database (e.g., no other backend container is starting). With the backend stopped, clear the lock row directly:

docker exec -it podradar-db psql -U podradar -d podradar -c "DELETE FROM DATABASECHANGELOGLOCK;"
docker compose restart podradar-backend

Clearing the lock while another Liquibase process is genuinely mid-migration can corrupt the migration state — when in doubt, wait the lock out instead.

Note on changeset 032 (split crawl error counters). When this migration runs, every existing podcast row inherits consecutive_parse_errors=0 and consecutive_network_errors=0 regardless of the legacy consecutive_errors value. This is intentional: feeds at the edge of being marked DEAD get a fresh retry budget on the first crawl after deploy. Genuinely dead feeds will re-hit the threshold within ~10 crawl cycles.