Files
StarPunk/docs/designs/phase-5-rss-and-container.md
Phil Skentelbery 6863bcae67 docs: add Phase 5 design and architectural review documentation
- Add ADR-014: RSS Feed Implementation
- Add ADR-015: Phase 5 Implementation Approach
- Add Phase 5 design documents (RSS and container)
- Add pre-implementation review
- Add RSS and container validation reports
- Add architectural approval for v0.6.0 release

Architecture reviews confirm 98/100 (RSS) and 96/100 (container) scores.
Phase 5 approved for production deployment.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 10:30:55 -07:00

1258 lines
36 KiB
Markdown

# Phase 5: RSS Feed & Production Container - Implementation Design
**Date**: 2025-11-18
**Phase**: 5
**Status**: Design Complete, Ready for Implementation
**Dependencies**: Phase 4 (Web Interface) complete
**Version**: 0.6.0 target
## Implementation Guidelines
### Version Management
- **Current Version**: 0.5.1 (in `starpunk/__init__.py`)
- **Target Version**: 0.6.0 (skip intermediate versions)
- **Rationale**: Phase 5 introduces significant new features warranting minor version bump
### Git Workflow
- **Branch Strategy**: Use feature branch `feature/phase-5-rss-container`
- **Workflow**:
1. Create feature branch from main
2. Implement all Phase 5 features
3. Create PR for review
4. Merge to main when complete
- **Rationale**: Cleaner history, easier rollback, follows project standards for larger features
## Executive Summary
Phase 5 implements RSS feed generation with full IndieWeb compliance and creates a production-ready container for deployment testing. This phase completes the core V1 feature set for content syndication and enables testing of IndieAuth authentication in a production-like environment.
**Key Deliverables**:
- RSS 2.0 feed generation with proper microformats
- Feed route and caching strategy
- Production-ready container (Podman/Docker compatible)
- Container configuration for public-facing deployment
- IndieAuth testing capability with HTTPS
- Container orchestration for development and production
## Scope
### In Scope for Phase 5
**RSS Feed Generation**:
- RSS 2.0 compliant XML feed
- Recent published notes (configurable limit)
- Proper RFC-822 date formatting
- HTML content with CDATA wrapping
- Unique GUID for each entry
- Channel metadata (title, link, description)
- Feed caching (5-minute default)
- Auto-discovery link in templates
**Production Container**:
- Containerfile (Podman/Docker compatible)
- Multi-stage build for optimization
- Gunicorn WSGI server configuration
- Health check endpoint
- Volume mounts for persistent data
- Environment variable configuration
- Production-ready security settings
- Container compose configuration
**Deployment Features**:
- HTTPS support via reverse proxy
- IndieAuth callback handling
- Production logging configuration
- Graceful shutdown handling
- Data persistence strategy
- Container networking setup
### Out of Scope
**Deferred to Phase 6+**:
- Micropub endpoint implementation
- Feed pagination
- Feed filtering/categories
- Atom feed format
- JSON Feed format
- Feed validation UI
- Auto-update notification
- Feed analytics
- Multiple feed types
- Advanced caching strategies
## Architecture Overview
### Component Diagram
```
┌────────────────────────────────────────────────────────┐
│ Client (Browser/RSS Reader) │
└────────────┬───────────────────────────────────────────┘
│ HTTPS (port 443)
┌────────────────────────────────────────────────────────┐
│ Reverse Proxy (Caddy/Nginx) - Host │
│ - SSL Termination │
│ - Proxy to container │
└────────────┬───────────────────────────────────────────┘
│ HTTP (port 8000)
┌────────────────────────────────────────────────────────┐
│ Container (starpunk:0.6.0) │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Gunicorn WSGI Server (4 workers) │ │
│ └─────────────┬──────────────────────────────────────┘ │
│ │ │
│ ↓ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Flask Application │ │
│ │ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ Web Routes │ │ Feed Route │ │ │
│ │ │ (existing) │ │ /feed.xml │ │ │
│ │ └──────────────┘ └──────┬───────┘ │ │
│ │ │ │ │
│ │ ↓ │ │
│ │ ┌──────────────────┐ │ │
│ │ │ Feed Generator │ │ │
│ │ │ (new module) │ │ │
│ │ └──────┬───────────┘ │ │
│ └───────────────────────────┼────────────────────────┘ │
│ │ │
│ ↓ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Business Logic (Notes Module) │ │
│ └─────────────┬──────────────────────────────────────┘ │
│ │ │
│ ↓ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Data Layer (Volume Mount) │ │
│ │ ┌──────────────┐ ┌────────────────┐ │ │
│ │ │ Markdown │ │ SQLite DB │ │ │
│ │ │ Files │ │ starpunk.db │ │ │
│ │ └──────────────┘ └────────────────┘ │ │
│ └────────────────────────────────────────────────────┘ │
│ /data (bind mount to host) │
└────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────┐
│ Host Filesystem (Persistent Data) │
│ ./container-data/ │
│ ├── notes/ (markdown files) │
│ └── starpunk.db (database) │
└────────────────────────────────────────────────────────┘
```
### Feed Generation Flow
```
RSS Reader → GET /feed.xml
Route Handler → Check cache (5 min)
Cache Hit? → Yes → Return cached XML
↓ No
Feed Generator → list_notes(published=True, limit=50)
Notes Module → Query database + read files
Feed Generator → Build RSS XML:
- Channel metadata
- For each note:
- Title (first line or timestamp)
- Link (permalink)
- Description (HTML content)
- PubDate (RFC-822 format)
- GUID (permalink)
Cache XML (5 minutes)
RSS Reader ← Return application/rss+xml
```
## RSS Feed Specification
### Module: `starpunk/feed.py`
**Purpose**: Generate RSS 2.0 compliant feed from published notes
**Functions**:
```python
def generate_feed(site_url: str, site_name: str, site_description: str,
notes: list[Note], limit: int = 50) -> str:
"""
Generate RSS 2.0 XML feed
Args:
site_url: Base URL of the site
site_name: Site title for the feed
site_description: Site description
notes: List of Note objects to include
limit: Maximum number of entries (default 50)
Returns:
RSS 2.0 XML string
Raises:
ValueError: If site_url or site_name is empty
"""
def format_rfc822_date(dt: datetime) -> str:
"""
Format datetime to RFC-822 format for RSS
Args:
dt: Datetime object to format
Returns:
RFC-822 formatted date string
Example: "Mon, 18 Nov 2024 12:00:00 +0000"
"""
def get_note_title(note: Note) -> str:
"""
Extract title from note content (first line or timestamp)
Args:
note: Note object
Returns:
Title string (max 100 chars)
"""
def clean_html_for_rss(html: str) -> str:
"""
Ensure HTML is safe for RSS CDATA wrapping
Args:
html: Rendered HTML content
Returns:
Cleaned HTML safe for CDATA
"""
```
### RSS Feed Structure
**Channel Elements**:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>Site Name</title>
<link>https://example.com/</link>
<description>Site description</description>
<language>en-us</language>
<lastBuildDate>Mon, 18 Nov 2024 12:00:00 +0000</lastBuildDate>
<atom:link href="https://example.com/feed.xml" rel="self" type="application/rss+xml"/>
<!-- Items -->
<item>
<title>Note Title or Timestamp</title>
<link>https://example.com/note/my-note-slug</link>
<guid isPermaLink="true">https://example.com/note/my-note-slug</guid>
<pubDate>Mon, 18 Nov 2024 10:30:00 +0000</pubDate>
<description><![CDATA[
<p>Rendered HTML content goes here</p>
]]></description>
</item>
<!-- More items... -->
</channel>
</rss>
```
**Standards Compliance**:
- RSS 2.0 specification
- RFC-822 date format
- CDATA wrapping for HTML content
- Atom self-link for feed discovery
- Proper GUID (permalink as GUID)
- XML declaration with UTF-8 encoding
### Route: `GET /feed.xml`
**Handler**: `public.feed()`
**Location**: Add to `starpunk/routes/public.py`
**Implementation**:
```python
from flask import Response, current_app
from starpunk.feed import generate_feed
from starpunk.notes import list_notes
import hashlib
# Simple in-memory cache
_feed_cache = {'xml': None, 'timestamp': None, 'etag': None}
@bp.route("/feed.xml")
def feed():
"""
RSS 2.0 feed of published notes
Returns:
XML response with RSS feed
Headers:
Content-Type: application/rss+xml; charset=utf-8
Cache-Control: public, max-age=300
ETag: hash of feed content
Caching:
- Server-side: 5 minute in-memory cache
- Client-side: 5 minute cache via Cache-Control
- Conditional requests: ETag support
"""
from datetime import datetime, timedelta
# Check if cache is valid (5 minutes)
cache_duration = timedelta(minutes=5)
now = datetime.utcnow()
if _feed_cache['xml'] and _feed_cache['timestamp']:
if now - _feed_cache['timestamp'] < cache_duration:
# Return cached feed with ETag
response = Response(
_feed_cache['xml'],
mimetype='application/rss+xml; charset=utf-8'
)
response.headers['Cache-Control'] = 'public, max-age=300'
response.headers['ETag'] = _feed_cache['etag']
return response
# Generate fresh feed
notes = list_notes(published_only=True, limit=50)
feed_xml = generate_feed(
site_url=current_app.config['SITE_URL'],
site_name=current_app.config['SITE_NAME'],
site_description=current_app.config.get('SITE_DESCRIPTION', ''),
notes=notes
)
# Calculate ETag
etag = hashlib.md5(feed_xml.encode('utf-8')).hexdigest()
# Update cache
_feed_cache['xml'] = feed_xml
_feed_cache['timestamp'] = now
_feed_cache['etag'] = etag
# Return response
response = Response(feed_xml, mimetype='application/rss+xml; charset=utf-8')
response.headers['Cache-Control'] = 'public, max-age=300'
response.headers['ETag'] = etag
return response
```
### Template Updates
**Add to `templates/base.html`**:
```html
<head>
<!-- Existing head content -->
<!-- RSS Feed Discovery -->
<link rel="alternate" type="application/rss+xml"
title="{{ config.SITE_NAME }} RSS Feed"
href="{{ url_for('public.feed', _external=True) }}">
</head>
```
**Add feed link to homepage** (`templates/index.html`):
```html
<nav>
<a href="/">Home</a>
<a href="{{ url_for('public.feed') }}">RSS Feed</a>
</nav>
```
## Production Container Specification
### Containerfile (Podman/Docker Compatible)
**Location**: `/home/phil/Projects/starpunk/Containerfile`
**Purpose**: Multi-stage production-optimized container
**Contents**:
```dockerfile
# syntax=docker/dockerfile:1
# Build stage - minimal Python build environment
FROM python:3.11-slim AS builder
# Install uv for fast dependency installation
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
WORKDIR /build
# Copy dependency files
COPY requirements.txt .
# Create virtual environment and install dependencies
RUN uv venv /opt/venv && \
. /opt/venv/bin/activate && \
uv pip install --no-cache -r requirements.txt
# Runtime stage - minimal runtime image
FROM python:3.11-slim
# Create non-root user for security
RUN useradd --create-home --shell /bin/bash starpunk && \
mkdir -p /app /data/notes && \
chown -R starpunk:starpunk /app /data
# Copy virtual environment from builder
COPY --from=builder /opt/venv /opt/venv
# Set PATH to use venv
ENV PATH="/opt/venv/bin:$PATH" \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1
WORKDIR /app
# Copy application code
COPY --chown=starpunk:starpunk . .
# Switch to non-root user
USER starpunk
# Expose application port
EXPOSE 8000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD python3 -c "import httpx; httpx.get('http://localhost:8000/health', timeout=2.0)" || exit 1
# Run gunicorn
CMD ["gunicorn", \
"--bind", "0.0.0.0:8000", \
"--workers", "4", \
"--worker-class", "sync", \
"--worker-tmp-dir", "/dev/shm", \
"--max-requests", "1000", \
"--max-requests-jitter", "50", \
"--timeout", "30", \
"--graceful-timeout", "30", \
"--access-logfile", "-", \
"--error-logfile", "-", \
"--log-level", "info", \
"app:app"]
```
### Container Compose Configuration
**Location**: `/home/phil/Projects/starpunk/compose.yaml`
**Purpose**: Production deployment orchestration
**Contents**:
```yaml
version: '3.8'
services:
starpunk:
image: starpunk:0.6.0
container_name: starpunk
build:
context: .
dockerfile: Containerfile
# Restart policy
restart: unless-stopped
# Ports - only expose to localhost
ports:
- "127.0.0.1:8000:8000"
# Environment variables
env_file:
- .env
environment:
# Override .env for container environment
- FLASK_ENV=production
- FLASK_DEBUG=0
- DATA_PATH=/data
- NOTES_PATH=/data/notes
- DATABASE_PATH=/data/starpunk.db
# Volume mounts for persistent data
volumes:
- ./container-data:/data:rw
# Health check
healthcheck:
test: ["CMD", "python3", "-c", "import httpx; httpx.get('http://localhost:8000/health', timeout=2.0)"]
interval: 30s
timeout: 3s
retries: 3
start_period: 10s
# Resource limits
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.25'
memory: 128M
# Logging
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# Network
networks:
- starpunk-net
networks:
starpunk-net:
driver: bridge
```
### Health Check Endpoint
**Add to `starpunk/__init__.py`**:
```python
@app.route('/health')
def health_check():
"""
Health check endpoint for containers and monitoring
Returns:
JSON with status and basic info
Response codes:
200: Application healthy
500: Application unhealthy
Checks:
- Database connectivity
- File system access
- Basic application state
"""
from flask import jsonify
import os
try:
# Check database
from starpunk.database import get_db
with app.app_context():
db = get_db(app)
db.execute("SELECT 1").fetchone()
db.close()
# Check file system
data_path = app.config['DATA_PATH']
if not os.path.exists(data_path):
raise Exception("Data path not accessible")
return jsonify({
'status': 'healthy',
'version': app.config.get('VERSION', '0.6.0'),
'environment': app.config.get('ENV', 'unknown')
}), 200
except Exception as e:
return jsonify({
'status': 'unhealthy',
'error': str(e)
}), 500
```
### Container Build & Run Instructions
**Build container**:
```bash
# Using Docker
docker build -t starpunk:0.6.0 -f Containerfile .
# Using Podman
podman build -t starpunk:0.6.0 -f Containerfile .
```
**Run container (development)**:
```bash
# Create data directory
mkdir -p container-data/notes
# Run with docker
docker run -d \
--name starpunk \
-p 127.0.0.1:8000:8000 \
-v $(pwd)/container-data:/data:rw \
--env-file .env \
starpunk:0.6.0
# Run with podman
podman run -d \
--name starpunk \
-p 127.0.0.1:8000:8000 \
-v $(pwd)/container-data:/data:rw,Z \
--env-file .env \
starpunk:0.6.0
```
**Run with compose**:
```bash
# Docker Compose
docker compose up -d
# Podman Compose
podman-compose up -d
```
### Reverse Proxy Configuration
**Caddy** (recommended for auto-HTTPS):
**Location**: `/home/phil/Projects/starpunk/Caddyfile.example`
```caddy
# Caddyfile for StarPunk reverse proxy
# Rename to Caddyfile and update domain
your-domain.com {
# Reverse proxy to container
reverse_proxy localhost:8000
# Logging
log {
output file /var/log/caddy/starpunk.log
}
# Security headers
header {
# Remove server header
-Server
# Security headers
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
X-XSS-Protection "1; mode=block"
Referrer-Policy "strict-origin-when-cross-origin"
}
# Compress responses
encode gzip zstd
# Static file caching
@static {
path /static/*
}
header @static Cache-Control "public, max-age=31536000, immutable"
}
```
**Nginx** (alternative):
**Location**: `/home/phil/Projects/starpunk/nginx.conf.example`
```nginx
# Nginx configuration for StarPunk
# Save to /etc/nginx/sites-available/starpunk
upstream starpunk {
server localhost:8000;
}
server {
listen 80;
server_name your-domain.com;
# Redirect to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name your-domain.com;
# SSL certificates (use certbot)
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
# SSL configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Logging
access_log /var/log/nginx/starpunk-access.log;
error_log /var/log/nginx/starpunk-error.log;
# Max upload size
client_max_body_size 10M;
# Proxy to container
location / {
proxy_pass http://starpunk;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Timeouts
proxy_connect_timeout 30s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
}
# Static files caching
location /static/ {
proxy_pass http://starpunk;
proxy_cache_valid 200 1y;
add_header Cache-Control "public, max-age=31536000, immutable";
}
# RSS feed caching
location /feed.xml {
proxy_pass http://starpunk;
proxy_cache_valid 200 5m;
add_header Cache-Control "public, max-age=300";
}
}
```
## Configuration Updates
### Environment Variables
**Add to `.env.example`**:
```bash
# =============================================================================
# RSS FEED CONFIGURATION
# =============================================================================
# Maximum number of items in RSS feed (default: 50)
FEED_MAX_ITEMS=50
# Feed cache duration in seconds (default: 300 = 5 minutes)
FEED_CACHE_SECONDS=300
# =============================================================================
# CONTAINER CONFIGURATION
# =============================================================================
# Application version
VERSION=0.6.0
# Environment: development or production
ENVIRONMENT=production
# Number of Gunicorn workers (default: 4)
# Recommendation: (2 x CPU cores) + 1
WORKERS=4
# Worker timeout in seconds (default: 30)
WORKER_TIMEOUT=30
# Max requests per worker before restart (prevents memory leaks)
MAX_REQUESTS=1000
```
### Config Module Updates
**Update `starpunk/config.py`**:
```python
# RSS feed configuration
app.config['FEED_MAX_ITEMS'] = int(os.getenv('FEED_MAX_ITEMS', '50'))
app.config['FEED_CACHE_SECONDS'] = int(os.getenv('FEED_CACHE_SECONDS', '300'))
# Application version
app.config['VERSION'] = os.getenv('VERSION', '0.6.0')
# Container/deployment configuration
app.config['ENVIRONMENT'] = os.getenv('ENVIRONMENT', 'development')
app.config['WORKERS'] = int(os.getenv('WORKERS', '4'))
```
## Testing Strategy
### Unit Tests
**File**: `tests/test_feed.py`
**Tests**:
```python
def test_generate_feed_basic():
"""Test feed generation with minimal notes"""
def test_generate_feed_empty():
"""Test feed generation with no notes"""
def test_generate_feed_limit():
"""Test feed respects item limit"""
def test_rfc822_date_format():
"""Test RFC-822 date formatting"""
def test_note_title_extraction():
"""Test title extraction from note content"""
def test_html_cleaning_for_cdata():
"""Test HTML cleaning for CDATA wrapping"""
def test_feed_xml_structure():
"""Test XML structure is valid"""
def test_feed_includes_all_required_elements():
"""Test all required RSS elements present"""
```
**File**: `tests/test_routes_feed.py`
**Tests**:
```python
def test_feed_route_returns_xml():
"""Test /feed.xml returns XML"""
def test_feed_route_content_type():
"""Test correct Content-Type header"""
def test_feed_route_caching():
"""Test feed caching behavior"""
def test_feed_route_etag():
"""Test ETag generation and validation"""
def test_feed_route_only_published():
"""Test feed only includes published notes"""
def test_feed_route_limit():
"""Test feed respects limit configuration"""
```
### Integration Tests
**Container Testing**:
```bash
#!/bin/bash
# Test container build and basic functionality
# Build container
podman build -t starpunk:0.6.0-test -f Containerfile .
# Run container
podman run -d \
--name starpunk-test \
-p 8000:8000 \
--env-file .env.test \
starpunk:0.6.0-test
# Wait for startup
sleep 5
# Test health endpoint
curl -f http://localhost:8000/health || exit 1
# Test feed endpoint
curl -f http://localhost:8000/feed.xml || exit 1
# Check feed is valid XML
curl -s http://localhost:8000/feed.xml | xmllint --noout - || exit 1
# Cleanup
podman stop starpunk-test
podman rm starpunk-test
echo "Container tests passed!"
```
### Manual Validation
**RSS Validation**:
1. Visit https://validator.w3.org/feed/
2. Enter feed URL
3. Verify no errors
4. Check all required elements present
**IndieAuth Testing**:
1. Deploy container with public HTTPS URL
2. Configure ADMIN_ME with your domain
3. Test IndieLogin authentication flow
4. Verify callback works correctly
5. Test session creation and validation
**Feed Reader Testing**:
1. Add feed to RSS reader (Feedly, NewsBlur, etc.)
2. Verify notes appear correctly
3. Check formatting and links
4. Test update behavior
## File Organization
```
starpunk/
├── feed.py # RSS feed generation (new)
├── routes/
│ └── public.py # Add feed route (update)
├── __init__.py # Add health check (update)
└── config.py # Add feed config (update)
/home/phil/Projects/starpunk/ (root)
├── Containerfile # Container build (new)
├── compose.yaml # Container orchestration (new)
├── Caddyfile.example # Caddy config (new)
├── nginx.conf.example # Nginx config (new)
├── .containerignore # Container build ignore (new)
└── .env.example # Update with feed/container config
templates/
├── base.html # Add RSS discovery link (update)
└── index.html # Add RSS link in nav (update)
tests/
├── test_feed.py # Feed generation tests (new)
└── test_routes_feed.py # Feed route tests (new)
docs/
├── designs/
│ ├── phase-5-rss-and-container.md # This file
│ └── phase-5-quick-reference.md # Implementation guide
└── decisions/
└── ADR-014-rss-feed-implementation.md # RSS decision record
```
## Security Considerations
### Feed Security
- No authentication required (public feed)
- Only published notes included
- HTML content sanitized via markdown rendering
- CDATA wrapping prevents XSS
- No user input in feed generation
- Rate limiting via reverse proxy
### Container Security
- Non-root user (starpunk:starpunk)
- Minimal base image (python:3.11-slim)
- No unnecessary packages
- Read-only root filesystem (optional)
- Resource limits (CPU, memory)
- Health checks for failure detection
- Secrets via environment variables (not in image)
- Volume permissions (UID/GID mapping)
### Production Deployment
- HTTPS required (via reverse proxy)
- Security headers enforced
- Session cookies: Secure, HttpOnly, SameSite
- CSRF protection maintained
- No debug mode in production
- Logging without sensitive data
- Regular security updates (base image)
## Performance Considerations
### RSS Feed
- Server-side caching (5 minutes)
- Client-side caching (Cache-Control)
- Conditional requests (ETag)
- Limit feed items (default 50)
- Efficient database query
- Pre-rendered HTML (no render on feed generation)
### Container Optimization
- Multi-stage build (smaller image)
- Gunicorn with multiple workers
- Worker recycling (max-requests)
- Shared memory for worker tmp
- Minimal logging overhead
- Resource limits prevent resource exhaustion
### Expected Performance
- Feed generation: < 100ms (cached: < 10ms)
- Container startup: < 5 seconds
- Memory usage: < 256MB (typical)
- CPU usage: < 10% (idle), < 50% (load)
- Container image size: < 200MB
## Migration from Phase 4
### Code Changes
- Add `starpunk/feed.py` module
- Update `starpunk/routes/public.py` (add feed route)
- Update `starpunk/__init__.py` (add health check)
- Update `starpunk/config.py` (add feed config)
- Update templates (RSS discovery links)
### New Files
- `Containerfile`
- `compose.yaml`
- `Caddyfile.example`
- `nginx.conf.example`
- `.containerignore`
### Configuration
- Update `.env.example` with feed/container variables
- No breaking changes to existing configuration
- Backward compatible
### Database
- No schema changes required
- No migrations needed
## Acceptance Criteria
Phase 5 is complete when:
### Functional Requirements
- [ ] RSS feed generates valid RSS 2.0 XML
- [ ] Feed includes recent published notes
- [ ] Feed respects configured item limit
- [ ] Feed has proper RFC-822 dates
- [ ] Feed includes HTML content in CDATA
- [ ] Feed route is accessible at /feed.xml
- [ ] Feed caching works (5 minutes)
- [ ] Feed discovery link in templates
- [ ] Container builds successfully
- [ ] Container runs application correctly
- [ ] Health check endpoint works
- [ ] Data persists across container restarts
- [ ] Container works with both Podman and Docker
- [ ] Compose configuration works
### Quality Requirements
- [ ] Feed validates with W3C validator
- [ ] Test coverage > 90%
- [ ] All tests pass
- [ ] No linting errors (flake8)
- [ ] Code formatted (black)
- [ ] Container image < 250MB
- [ ] Container startup < 10 seconds
- [ ] Memory usage < 512MB under load
### Security Requirements
- [ ] Feed only shows published notes
- [ ] Container runs as non-root user
- [ ] No secrets in container image
- [ ] Security headers configured
- [ ] HTTPS enforced in production config
- [ ] Resource limits configured
### Documentation Requirements
- [ ] RSS implementation documented
- [ ] Container build documented
- [ ] Deployment guide complete
- [ ] Reverse proxy configs provided
- [ ] IndieAuth testing documented
- [ ] CHANGELOG updated
- [ ] Version incremented to 0.6.0
### Production Testing Requirements
- [ ] Container deployed with public URL
- [ ] IndieAuth tested with real IndieLogin
- [ ] RSS feed accessible via HTTPS
- [ ] Feed appears in RSS readers
- [ ] Session persistence works
- [ ] Container restarts gracefully
## Implementation Checklist
### Phase 5.1: RSS Feed Implementation
- [ ] Create `starpunk/feed.py` module
- [ ] Implement `generate_feed()` function
- [ ] Implement `format_rfc822_date()` function
- [ ] Implement `get_note_title()` function
- [ ] Implement `clean_html_for_rss()` function
- [ ] Add feed route to `public.py`
- [ ] Implement feed caching
- [ ] Add ETag support
- [ ] Update templates with RSS discovery
- [ ] Test feed generation
### Phase 5.2: Feed Testing
- [ ] Write unit tests for feed.py
- [ ] Write route tests for feed endpoint
- [ ] Test caching behavior
- [ ] Test ETag validation
- [ ] Validate with W3C Feed Validator
- [ ] Test with RSS readers
- [ ] Verify date formatting
- [ ] Check HTML rendering
### Phase 5.3: Container Implementation
- [ ] Create Containerfile
- [ ] Create compose.yaml
- [ ] Create .containerignore
- [ ] Add health check endpoint
- [ ] Configure Gunicorn
- [ ] Test container build
- [ ] Test container run
- [ ] Test data persistence
- [ ] Test health checks
### Phase 5.4: Production Configuration
- [ ] Create Caddyfile.example
- [ ] Create nginx.conf.example
- [ ] Update .env.example
- [ ] Document environment variables
- [ ] Create deployment guide
- [ ] Test reverse proxy configs
- [ ] Document SSL setup
- [ ] Document IndieAuth testing
### Phase 5.5: Integration Testing
- [ ] Build and run container
- [ ] Test with Podman
- [ ] Test with Docker
- [ ] Test compose orchestration
- [ ] Test health endpoint
- [ ] Test feed accessibility
- [ ] Test data persistence
- [ ] Test graceful shutdown
### Phase 5.6: Production Deployment Testing
- [ ] Deploy to public-facing server
- [ ] Configure reverse proxy with HTTPS
- [ ] Test IndieAuth authentication
- [ ] Verify callback URLs work
- [ ] Test session creation
- [ ] Verify feed in RSS reader
- [ ] Check security headers
- [ ] Monitor resource usage
### Phase 5.7: Documentation
- [ ] Complete ADR-014 (RSS implementation)
- [ ] Document feed generation
- [ ] Document container setup
- [ ] Document deployment process
- [ ] Document IndieAuth testing
- [ ] Update CHANGELOG
- [ ] Increment version to 0.6.0
- [ ] Create quick reference guide
## Success Metrics
Phase 5 is successful if:
1. **RSS feed is valid** and passes W3C validation
2. **Feed appears in RSS readers** correctly
3. **Container builds** in under 2 minutes
4. **Container runs** reliably with no crashes
5. **IndieAuth works** with public HTTPS URL
6. **Data persists** across container restarts
7. **Memory usage** stays under 512MB
8. **Feed caching** reduces load
9. **All tests pass** with >90% coverage
10. **Documentation** enables easy deployment
## Dependencies
### Python Packages (Existing)
- feedgen (already in requirements.txt)
- Flask
- markdown
- httpx
### No New Python Dependencies Required
### External Dependencies
- Podman or Docker (user provides)
- Reverse proxy: Caddy or Nginx (user provides)
- Public domain with HTTPS (user provides)
### Build Tools
- Container runtime (podman/docker)
- Python 3.11+
- uv (for fast dependency installation)
## Risk Assessment
### Technical Risks
**Risk: Feed caching causes stale content**
- Likelihood: Low
- Impact: Low
- Mitigation: 5-minute cache is acceptable delay, ETag support, clear cache on note creation
**Risk: Container fails to start**
- Likelihood: Low
- Impact: High
- Mitigation: Health checks, startup probes, extensive testing, fallback to direct deployment
**Risk: IndieAuth callback fails with HTTPS**
- Likelihood: Medium
- Impact: High
- Mitigation: Clear documentation, example configs, testing guide, troubleshooting section
**Risk: Data loss on container restart**
- Likelihood: Low
- Impact: Critical
- Mitigation: Volume mounts, data persistence testing, backup documentation
### Operational Risks
**Risk: Incorrect reverse proxy configuration**
- Likelihood: Medium
- Impact: High
- Mitigation: Example configs, testing guide, common issues documentation
**Risk: Resource exhaustion in container**
- Likelihood: Low
- Impact: Medium
- Mitigation: Resource limits, health checks, monitoring guidance
**Risk: RSS feed validation failures**
- Likelihood: Low
- Impact: Low
- Mitigation: Extensive testing, W3C validation, feed reader testing
## Future Enhancements (V2+)
Deferred features:
- Feed pagination (older entries)
- Multiple feed formats (Atom, JSON Feed)
- Feed categories/tags
- Feed discovery for individual tags
- Feed analytics
- Podcast RSS support
- Media enclosures
- Container orchestration (Kubernetes)
- Container registry publishing
- Auto-update mechanisms
- Feed notification (WebSub)
## References
### Internal Documentation
- [Phase 4: Web Interface](/home/phil/Projects/starpunk/docs/design/phase-4-web-interface.md)
- [Architecture Overview](/home/phil/Projects/starpunk/docs/architecture/overview.md)
- [ADR-002: Flask Extensions](/home/phil/Projects/starpunk/docs/decisions/ADR-002-flask-extensions.md)
- [Versioning Strategy](/home/phil/Projects/starpunk/docs/standards/versioning-strategy.md)
### External Standards
- [RSS 2.0 Specification](https://www.rssboard.org/rss-specification)
- [RFC-822 Date Format](https://www.rfc-editor.org/rfc/rfc822)
- [W3C Feed Validator](https://validator.w3.org/feed/)
- [Dockerfile Best Practices](https://docs.docker.com/develop/dev-best-practices/)
- [Podman Documentation](https://docs.podman.io/)
- [Gunicorn Configuration](https://docs.gunicorn.org/en/stable/settings.html)
---
**Phase**: 5
**Version Target**: 0.6.0
**Status**: Design Complete, Ready for Implementation
**Next Phase**: Phase 6 (Micropub Implementation)