Contents

atelier-socle/podcast-feed-maker-vapor

Vapor middleware for serving podcast RSS feeds with caching, streaming, Podping, and Fluent integration. Built on PodcastFeedMaker.

Features

  • Middleware — X-Generator header, ETag/304 HTTP caching, CORS with preflight
  • Route Builderapp.podcastFeed("feed.xml") DSL for feed routes
  • Streaming — Chunked XML streaming via StreamingFeedGenerator for large catalogs
  • Pagination — Query params parsing (?limit=N&offset=N) with safe clamping
  • Podping — Feed update notifications via webhook and real-time WebSocket
  • Batch Audit — Parallel feed quality scoring with grades and recommendations
  • Fluent Mapping — Pure Swift protocols for model-to-feed conversion
  • Redis Cache — Optional FeedCacheStore protocol + Redis implementation
  • Queue Workers — Optional background feed regeneration via Vapor Queues
  • Metrics — Feed request counters, latency timers, and response size recording via swift-metrics
  • Strict concurrency — All types Sendable, Swift 6.2 strict concurrency

Installation

Requirements

  • Swift 6.2+ with strict concurrency
  • Vapor 4.121+
  • PodcastFeedMaker 0.3.0+
  • Platforms: macOS 14+ · Linux (Ubuntu 22.04+)

Swift Package Manager

Add the dependency to your Package.swift:

dependencies: [
    .package(url: "https://github.com/atelier-socle/podcast-feed-maker-vapor.git", from: "0.3.0")
]

Four products are available — import only what you need:

// Core (required) — middleware, routes, encoding, Fluent mapping
.product(name: "PodcastFeedVapor", package: "podcast-feed-maker-vapor")

// Optional — Redis-backed feed cache
.product(name: "PodcastFeedVaporRedis", package: "podcast-feed-maker-vapor")

// Optional — Background feed regeneration jobs
.product(name: "PodcastFeedVaporQueues", package: "podcast-feed-maker-vapor")

// Optional — Feed serving metrics (Prometheus, StatsD, etc.)
.product(name: "PodcastFeedVaporMetrics", package: "podcast-feed-maker-vapor")

Quick Start

import PodcastFeedVapor

func configure(_ app: Application) throws {
    app.feedConfiguration = FeedConfiguration(
        ttl: .hours(1),
        prettyPrint: false,
        generatorHeader: "MyApp/1.0"
    )
    app.healthCheck()
    app.middleware.use(CORSFeedMiddleware())
    app.middleware.use(PodcastFeedMiddleware())
    app.grouped(FeedCacheMiddleware()).podcastFeed("feed.xml") { req in
        try await loadFeed(on: req.db)
    }
}

Key Concepts

Middleware Stack

Three middlewares add production headers to feed responses. Stack them in order: CORS (outermost), generator header, cache (innermost):

app.middleware.use(CORSFeedMiddleware())
app.middleware.use(PodcastFeedMiddleware())
app.grouped(FeedCacheMiddleware(ttl: .hours(2)))
    .podcastFeed("feed.xml") { _ in feed }

Feed Routes

Register feed routes with the podcastFeed() DSL. Supports static paths and dynamic parameters:

app.podcastFeed("feed.xml") { _ in feed }
app.podcastFeed("shows", ":showId", "feed.xml") { req in
    let showId = req.parameters.get("showId") ?? "unknown"
    return try await loadFeed(for: showId, on: req.db)
}

HTTP Caching

FeedCacheMiddleware adds ETag (SHA256), Last-Modified, and Cache-Control headers. Returns 304 Not Modified when the client's cached version is still valid:

app.grouped(FeedCacheMiddleware(ttl: .minutes(15)))
    .podcastFeed("feed.xml") { _ in feed }

Fluent Mapping

Three protocols map your types to feeds — no Fluent dependency required:

extension Show: FeedMappable {
    func toPodcastFeed() -> PodcastFeed { ... }
}
extension Episode: ItemMappable {
    func toItem() -> Item { ... }
}
let items = episodes.toItems()  // Array extension

Podping

Webhook notifications via PodpingNotifier and real-time WebSocket via PodpingWebSocketManager:

// Webhook (fire-and-forget)
let notifier = PodpingNotifier(client: req.client, authToken: "token")
try await notifier.notify(feedURL: "https://example.com/feed.xml")

// WebSocket (real-time, persistent)
app.podpingWebSocket("podping")
await app.podpingWebSocketManager.broadcast(
    feedURL: "https://example.com/feed.xml"
)

Batch Audit

Score feed quality in parallel:

app.batchAudit("feeds", "audit")
// GET /feeds/audit?urls=https://a.com/feed.xml,https://b.com/feed.xml

Metrics

Optional target for feed serving observability. Uses Apple's swift-metrics API — plug in any backend:

import PodcastFeedVaporMetrics

// Add metrics before other feed middleware
app.middleware.use(FeedMetricsMiddleware())
app.middleware.use(PodcastFeedMiddleware())

Emits: pfv_feed_requests_total (counter), pfv_feed_request_duration_seconds (timer), pfv_feed_response_size_bytes (recorder), pfv_feed_active_streams (gauge).

Redis and Queues

Optional targets for production scale — no Redis/Queues dependency in core:

// Redis cache
import PodcastFeedVaporRedis
let cache = app.redisFeedCache(keyPrefix: "myapp:feed:", defaultTTL: 600)
try await cache.set(identifier: "show-123", xml: feedXML, ttl: 300)

// Queue workers
import PodcastFeedVaporQueues
app.registerFeedRegenerationJob(handler: MyFeedRegenerator())
try await req.queue.dispatch(
    FeedRegenerationJob.self,
    FeedRegenerationPayload(feedIdentifier: "show-123", reason: "episode_added")
)

Architecture

Sources/
    PodcastFeedVapor/              # Core (Vapor + PodcastFeedMaker + Fluent)
        PodcastFeedMiddleware.swift
        FeedCacheMiddleware.swift
        CORSFeedMiddleware.swift
        FeedRouteBuilder.swift
        FeedResponseEncoder.swift
        FeedConfiguration.swift
        FeedPagination.swift
        StreamingFeedResponse.swift
        FluentFeedMapping.swift
        PodpingNotifier.swift
        PodpingMessage.swift
        PodpingWebSocketManager.swift
        PodpingWebSocketRoute.swift
        BatchAuditEndpoint.swift
        HealthCheck.swift
        Extensions/
            Request+Feed.swift
            Response+XML.swift
    PodcastFeedVaporRedis/         # Optional Redis cache
        RedisFeedCache.swift
    PodcastFeedVaporQueues/        # Optional queue workers
        FeedRegenerationJob.swift
    PodcastFeedVaporMetrics/       # Optional metrics middleware
        FeedMetricsMiddleware.swift
        FeedMetricsConfiguration.swift
        FeedActiveStreamsGauge.swift

Sample Server

A companion test server demonstrates all features with real HTTP endpoints. Run the setup script to scaffold it as a sibling directory:

./scripts/setup-sample-server.sh
cd ../sample-vapor-server
swift run

Or specify a custom path:

./scripts/setup-sample-server.sh /path/to/my-test-server

Then test with curl:

curl http://localhost:8080/health
curl http://localhost:8080/feed.xml
curl -sI http://localhost:8080/feed.xml | grep etag
curl "http://localhost:8080/feeds/audit?urls=https://feeds.simplecast.com/54nAGcIl"

See scripts/setup-sample-server.sh for the full list of endpoints and test scenarios.


Roadmap

  • [x] Streaming cache — Stream-through caching for very large feeds
  • [x] Metrics — Prometheus/StatsD middleware for feed serving metrics
  • [x] WebSocket Podping — Real-time podping via WebSocket instead of webhook

Specification References


Documentation

Full API documentation is available as a DocC catalog hosted on GitHub Pages and bundled with the package. Open the project in Xcode and select Product > Build Documentation to browse it locally.

Guides:

| Guide | Content | |-------|---------| | Getting Started | Installation and first feed route | | Middleware Guide | Caching, CORS, and generator header | | Feed Serving Guide | Route builder DSL, streaming, and pagination | | Fluent Integration | Protocol-based model mapping | | Advanced Features | Podping, batch audit, and health check | | Redis and Queues | Optional production targets | | Metrics | Feed serving metrics via swift-metrics |


Contributing

See CONTRIBUTING.md for guidelines.


License

This project is licensed under the Apache License 2.0.

Copyright 2026 Atelier Socle SAS. See NOTICE for details.

Package Metadata

Repository: atelier-socle/podcast-feed-maker-vapor

Homepage: https://atelier-socle.com/en/solutions/podcast-feed-vapor

Stars: 1

Forks: 0

Open issues: 0

Default branch: main

Primary language: swift

License: Apache-2.0

Topics: cache, fluent, linux, middleware, podcast, podcast-namespace, podping, rss, rss-feed, server-side-swift, swift, vapor, xml

README: README.md