ADR-064: Canonical Server Layer with Feature-Flagged Route Groups¶
- Status: Accepted
- Date: 2026-04-03
- Authors: Podcast Scraper Team
- Related RFCs: RFC-062
- Related Issues: #50, #347, #489
Context & Problem Statement¶
The project needs a server layer for two purposes arriving at different times: the viewer v2 (v2.6) needs API endpoints for search, explore, and artifact loading; the platform (#50, #347) needs API endpoints for feed management, episode browsing, and job submission (v2.7). Building two separate servers would duplicate middleware, config, and deployment patterns. Building one server named "viewer" would require a rename when platform work starts.
Decision¶
We create a canonical server layer in src/podcast_scraper/server/:
- Module naming:
server/, notviewer/orapi/. This is the project's single HTTP server module. - Feature-flagged route groups:
create_app(enable_viewer=True, enable_platform=False). Route groups mount or not based on flags. podcast serveCLI command: New top-level subcommand. Supports--output-dir,--port, and future--platformflag.- Viewer routes (v2.6):
/api/health,/api/artifacts,/api/search,/api/explore,/api/index/stats. Mounted by default. - Platform route stubs (v2.7): Empty files for
/api/feeds,/api/episodes,/api/jobs. Present in tree but not mounted untilenable_platform=True. - FastAPI app factory pattern: One
create_app()function, routers registered conditionally, shared dependencies (config, output_dir, vector_store).
Rationale¶
- Megasketch alignment: Constraint A.2 — "One pipeline core, multiple shells." The CLI is one shell, the server is another. Not two servers.
- Avoid rename tax: Naming it
server/now avoids a migration when platform routes land. Every import path, test path, and config reference would need updating. - Additive growth: v2.7 adds routes and views to the existing server, not a new module. No architectural restructuring between versions.
- Shared infrastructure: Middleware (CORS, future auth), dependencies (config, vector store), and error handling are defined once.
Alternatives Considered¶
src/podcast_scraper/viewer/naming: Rejected; forces rename when platform routes arrive. Implies the server exists only for the viewer.- Separate viewer and platform servers: Rejected; duplicates middleware, config loading, deployment. Two processes for one application.
- No server — CLI-only with subprocess calls from frontend: Rejected; loses async, loses shared state (loaded vector index), poor developer experience.
- Flask instead of FastAPI: Rejected; FastAPI provides async, auto-docs, Pydantic validation, and typed route parameters natively.
Consequences¶
- Positive: One server, one CLI command, one deployment unit. Additive growth path from viewer to platform. Shared dependencies and middleware.
- Negative: Viewer routes and platform routes share a module; must maintain clean separation between route groups (solved by separate files and feature flags).
- Neutral:
podcast serveis a new CLI subcommand that replacesscripts/gi_kg_viz_server.py.
Implementation Notes¶
- Module:
src/podcast_scraper/server/ - Entry point:
podcast serve --output-dir ./output [--platform] [--port 8100] - App factory:
server.app.create_app(output_dir, enable_viewer=True, enable_platform=False) - Makefile:
make serve(combined),make serve-api(backend only) - Relationship to
service.py:service.run()is one-shot pipeline execution.server/is long-lived HTTP. Platform jobs will callservice.run()internally.