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

36 KiB

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:

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 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:

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:

<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):

<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:

# 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:

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:

@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:

# 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):

# 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:

# 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

# 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 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:

# =============================================================================
# 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:

# 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:

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:

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:

#!/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

External Standards


Phase: 5 Version Target: 0.6.0 Status: Design Complete, Ready for Implementation Next Phase: Phase 6 (Micropub Implementation)