# SocialCasts API > Social podcast platform — search, listen, subscribe, share with friends > Version 1.0 | Symfony 7.3 | PHP 8.3 | PostgreSQL 16 | JWT Auth ## Quick Reference - Production: https://podcast.meetthemusic.fr - Development: http://localhost:8080 - Documentation: GET / - Health: GET /health → {"status":"ok"} - LLM context: GET /llm.txt (this file) - Bruno collection: GET /download/bruno-collection ## Authentication Flow 1. POST /api/register {"email":"…","password":"…"} → 201 {"data":{"id":1,"email":"…"}} 2. POST /api/login {"email":"…","password":"…"} → 200 {"token":"eyJ…"} 3. Use token: Authorization: Bearer (24h TTL) 4. Logout: client-side (delete token) Public routes: GET /health, POST /api/register, POST /api/login All other /api/* routes → 401 without valid JWT ## All Endpoints (18 total) ### Auth [Public] POST /api/register {email, password(8+)} → 201 {data:{id,email}} POST /api/login {email, password} → 200 {token} ### Search [JWT required] GET /api/search?term=X → 200 {data:[{itunes_id,title,author,artwork_url,feed_url}]} GET /api/podcasts/{itunesId}/episodes → 200 {data:[{title,description,audio_url,duration(sec),published_at}]} ### Library [JWT required] POST /api/subscriptions/{itunesId} → 201 {data:{id,itunes_id,title,author}} DELETE /api/subscriptions/{itunesId} → 204 GET /api/subscriptions → 200 {data:[{itunes_id,title,author,artwork_url}]} POST /api/history {itunes_id,last_position,is_completed} → 201(new)|200(update) {data:{itunes_id,title,last_position,is_completed,played_at}} GET /api/history?limit=20&offset=0 → 200 {data:[…],total:N} ### Social [JWT required] GET /api/users/search?email=X → 200 {data:{id,email}|null} POST /api/friends/{userId}/request → 201 {data:{status:"pending"}} PUT /api/friends/{userId}/accept → 200 {data:{status:"accepted"}} DELETE /api/friends/{userId} → 204 GET /api/friends → 200 {data:[{id,email}]} GET /api/friends/requests → 200 {data:[{id,email}]} ### Notifications [JWT required] POST /api/recommendations {receiver_id,itunes_id,message?} → 201 {data:{id,sender:{id,email},receiver:{id,email},podcast:{itunes_id,title},message,created_at}} GET /api/notifications?unread=true|false → 200 {data:[{id,sender:{email},podcast:{title,artwork_url},message,created_at}]} PUT /api/notifications/{id}/read → 200 {data:{id,is_read:true}} ## Rate Limiting (sliding window) POST /api/register → 5 req/min per IP POST /api/login → 5 req/min per IP GET /api/search → 30 req/min per authenticated user Exceeded → 429 {"error":true,"message":"…","code":"RATE_LIMITED"} ## Response Conventions Success: {"data":{…}} or {"data":[…]} or {"data":[…],"total":N} Error: {"error":true,"message":"Human-readable message","code":"MACHINE_CODE"} Empty: {"data":null} or {"data":[]} ## Error Codes 400 VALIDATION_ERROR — Invalid input (email format, password <8 chars, missing fields) 400 DUPLICATE_EMAIL — Email already registered 400 ALREADY_SUBSCRIBED — Already subscribed to this podcast 400 ALREADY_FRIENDS — Already friends with this user 400 REQUEST_ALREADY_SENT — Friend request already pending 400 NOT_FRIENDS — Cannot recommend to non-friend 401 INVALID_CREDENTIALS — Wrong email/password or expired JWT 404 PODCAST_NOT_FOUND — Podcast not in database (search first to upsert) 404 USER_NOT_FOUND — Target user does not exist 404 NOT_SUBSCRIBED — Not subscribed to this podcast 404 REQUEST_NOT_FOUND — No pending friend request from this user 404 FRIENDSHIP_NOT_FOUND — No friendship/request exists 404 NOTIFICATION_NOT_FOUND — Notification not found or not yours 429 RATE_LIMITED — Too many requests 503 ITUNES_UNAVAILABLE — iTunes Search API unreachable 500 SERVER_ERROR — Unhandled exception (JSON in /api/*) ## Data Model (6 entities, PostgreSQL) User → id, email(unique), password(bcrypt), roles(json) Podcast → id, itunes_id(unique bigint), title, author, artwork_url, feed_url, created_at Subscription → id, user_id→User, podcast_id→Podcast, created_at | unique(user,podcast) PlayHistory → id, user_id→User, podcast_id→Podcast, last_position(int/sec), is_completed(bool), played_at | unique(user,podcast) Friendship → id, requester_id→User, addressee_id→User, status(pending|accepted), created_at | unique(requester,addressee) Recommendation → id, sender_id→User, receiver_id→User, podcast_id→Podcast, message(nullable), is_read(bool), created_at ## Key Behaviors - Search results are cached 15min (Symfony filesystem cache) and podcasts are upserted to DB - Episodes are parsed from RSS/XML in real-time (not cached) via podcast's feed_url - PlayHistory uses upsert: same user+podcast → updates position instead of creating duplicate - Friendship is bidirectional: findBetween() checks both (a→b) and (b→a) - Notifications = Recommendations with is_read flag (no separate entity) - User search excludes self (cannot add yourself as friend) - All timestamps are ISO 8601 / ATOM format ## Typical Integration Flow 1. Register → Login → get token 2. Search podcasts → subscribe to favorites 3. Get episodes → play → save position to history 4. Search users → send friend request → friend accepts 5. Recommend podcast to friend → friend sees notification → marks as read ## Testing - PHPUnit: 37 functional + unit tests, 95 assertions - Bruno collection: 28 .bru files in 4 folders (Auth, Search, Library, Social) - Fixtures: alice@test.com + bob@test.com (password: secret123) Pre-configured: friendship(accepted) + podcast(JavaScript Jabber) + recommendation(unread) - CI: GitLab CI with parallel jobs (lint + phpstan + phpunit) → build → deploy ## Tech Stack - Framework: Symfony 7.3 (PHP 8.3) - Server: FrankenPHP (Caddy-based) - Database: PostgreSQL 16 with Doctrine ORM - Auth: LexikJWT (RS256, 24h TTL) - HTTP Client: symfony/http-client (iTunes proxy, RSS fetch) - Cache: symfony/cache filesystem adapter - Rate Limiter: symfony/rate-limiter sliding window - CORS: NelmioCorsBundle - Deploy: Coolify on VPS from Docker multi-stage build