Architecture Overview

This document describes the high-level architecture of Django Cast.

Django Cast is a blogging and podcasting application built on Django and Wagtail CMS.

High-Level Design

Django Cast follows a layered architecture:

┌─────────────────────────────────────────────────────┐
│                   Frontend Layer                    │
│         (Templates, JavaScript, HTMX)               │
└─────────────────────────────────────────────────────┘
                         │
┌─────────────────────────────────────────────────────┐
│                     API Layer                       │
│         (REST Framework + Wagtail API v2)           │
└─────────────────────────────────────────────────────┘
                         │
┌─────────────────────────────────────────────────────┐
│                   Models Layer                      │
│      (Django ORM - Wagtail Pages + Models)          │
└─────────────────────────────────────────────────────┘
                         │
┌─────────────────────────────────────────────────────┐
│                  Repository Layer                   │
│     (PostQuerySnapshot, PostDetailContext, etc.)      │
└─────────────────────────────────────────────────────┘
                         │
┌─────────────────────────────────────────────────────┐
│                  Database Layer                     │
│              (PostgreSQL/SQLite)                    │
└─────────────────────────────────────────────────────┘

Page Hierarchy

Cast uses Wagtail’s page tree to organize content:

HomePage (cast.HomePage)
├── Blog (cast.Blog) - Index page for posts
│   ├── Post (cast.Post) - Individual blog posts
│   └── Post (cast.Post)
└── Podcast (cast.Podcast) - Blog with podcast metadata (iTunes fields, etc.)
    ├── Episode (cast.Episode) - Post with audio enclosure
    └── Episode (cast.Episode)
  • Podcast: A Blog subclass with extensive podcast metadata (iTunes categories, explicit content flags, etc.)

  • Episode: A Post subclass that includes audio files which become enclosure elements in RSS feeds

Repository Pattern

The repository pattern sits between the database and Django models. It provides an abstraction layer that returns dictionaries (not model instances) that are JSON-serializable. This enables caching between the database queries and model instantiation.

Flow:

Database → Django ORM/Raw SQL → Repository → Dict → JSON Cache → Dict → Django Models → Template

Key Repository Classes

PostQuerySnapshot

Base class that converts querysets to dictionaries with prefetch optimization.

PostDetailContext

Fetches single post data with all relations in minimal queries.

FeedContext

Optimized for RSS/podcast feed generation.

BlogIndexContext

Handles blog index pages with pagination.

The repositories:

  • Execute optimized queries (ORM or raw SQL)

  • Return dictionary structures

  • Enable JSON caching of query results

  • Allow dynamic model reconstruction from cached data

Models Architecture

Cast models are organized into:

  1. Page Models (models/pages.py):

    • HomePage: Site root

    • Blog: Container for posts

    • Post: Blog entries with StreamField content

    • Podcast: Blog subclass with iTunes metadata, categories, etc.

    • Episode: Post subclass with audio fields for RSS enclosures

  2. Media Models:

    • Wagtail Image: Images with collection/uploader ownership semantics

    • Audio: Audio files with duration and metadata

    • Video: Video files with poster frames

    • Gallery: Collection of images

    • ChapterMark: Timestamps for podcast chapters

    • Transcript: Text for audio/video

  3. Moderation (models/moderation.py):

    • SpamFilter: Naive Bayes spam detection

Domain Packages and Import Boundaries

Two subsystems are factored into standalone packages with deliberate, test-enforced import boundaries so the model layer stays free of upward and cyclic dependencies.

Transcripts (cast/transcripts/)

Format handling is split into layers by what each may import:

  • Pure, Django-free format modules (parsing, webvtt, podlove, dote, known_speakers) import neither Django nor cast.models.

  • services.py holds model-facing orchestration but performs no runtime cast.models import (type hints are TYPE_CHECKING-only, and it works through related managers), so models/transcript.py → services stays acyclic.

  • editing.py holds view-facing editor orchestration and is imported only by views, so it may import models freely.

Transcript keeps its full public API as thin delegates; the model class and its migrations stay in cast.models.transcript.

Voxhelm (cast/voxhelm/)

Transcription-backend integration lives in its own subpackage. A subprocess test pins the invariant that django.setup() (which imports cast.models) must not import cast.voxhelm.

cast/voxhelm_tasks.py deliberately keeps its module path: django-tasks stores the task path in database rows (moving it would strand queued work across an upgrade), and @task(backend="cast_transcripts") resolves — and fails — its TASKS backend at import time when that backend is not configured. The function-body import of the completion task in service._resolve_completion_task, called first thing by enqueue_audio_transcript_generation, is therefore the load-bearing optionality seam: installs that never enqueue never need the cast_transcripts backend. It runs before any remote submission or database write, so a missing backend raises ImproperlyConfigured instead of stranding a submitted job behind a queued generation row; the cast.E009 system check reports the missing backend up front.

There is intentionally no [voxhelm] packaging extra. django-tasks stays a hard dependency (it is lightweight, and making it optional would trade clean ImproperlyConfigured/no-op degradation for ImportError crashes in half-configured installs). The optionality that matters is behavioral: models and migrations are unconditional (Django model discovery cannot be optional), and the TASKS backend is only required at first enqueue. The worker itself ships behind the separate transcript-worker extra.

StreamField Structure

Posts use a two-section StreamField:

body = StreamField([
    ("overview", blocks.StreamBlock([...])),
    ("detail", blocks.StreamBlock([...])),
])

This allows showing just the overview on index pages and full content on detail pages.

Available block types include:

  • Text blocks (paragraph rich text, code)

  • Media blocks (image, gallery, video, audio)

  • Embed blocks (HTML, external embeds)

Performance Optimization

Query Optimization

The repository pattern minimizes database queries through:

  1. select_related: For one-to-many foreign keys

  2. prefetch_related: For many-to-many relations

  3. Bulk operations: For rendition creation

Caching

  • Repository-ready data: repositories can produce JSON-serializable dicts (useful for caching, if configured)

  • Rendition cache: Generated image sizes

  • Feed cache: Feed endpoints are wrapped with cast.feeds.restriction_aware_cache_page (see src/cast/urls.py), a cache_page whose key prefix carries a feed cache generation. The generation is captured when the request starts, before the restricted-root guard or the render queries the database, so a snapshot-isolated request never pairs pre-change rows with the post-change key. A committed PageViewRestriction save or delete rotates that generation (src/cast/receivers.py), so cached feeds and renders in flight during the change are never served afterwards. Cache misses create request-local feed instances so the renderer’s request, page, repository, and audio-format state is never shared between concurrent requests.

Media Handling

Media Pipeline:

  1. Upload: Via Wagtail admin or API

  2. Storage: Django storage backend (local or S3)

  3. Processing:

    • Images: Wagtail renditions

    • Audio: Duration extraction

    • Video: Poster generation

  4. Delivery: Direct or via CDN

Upload and fetch safety

All audio, video, and transcript upload transports call cast.media_ingest.ingest_upload. Audio and video transports acquire the shared per-user upload_lock once, before form binding, and select either an editor or admin probe budget. The service randomizes stored basenames, saves inside a transaction, removes newly written files after failure, and deletes replaced files only after commit. Audio and video form saves continue into cast.media_derivation for duration, chapter, and poster processing inside that ingest boundary. Future upload transports must use the same service instead of saving media forms directly.

Remote byte fetches use cast.safe_fetch.fetch_bytes with an explicit FetchPolicy. The policy controls allowed origins, redirects, response and error-body byte caps, timeouts, and private-address blocking. When block_private_addresses is enabled, addresses that Python’s ipaddress module classifies as non-global, plus multicast addresses, are rejected before the request opens. Redirect following cannot be combined with this check. Operator-configured Voxhelm origins deliberately disable it while retaining same-origin and size controls.

The resolution check does not pin the connection to the validated address, so it is not by itself a complete defense against DNS rebinding. Before accepting attacker-controlled URLs, a future importer must extend the open_url transport with connect-time address pinning and redirect revalidation, while retaining the policy checks above.

API Architecture

Cast provides:

  1. Wagtail API v2: Page content access

  2. Custom REST endpoints: Media uploads, search

  3. Feed endpoints: RSS/podcast XML

Note: The REST Framework serializers are not performance-optimized. Performance optimization happens at the repository and feed generation level.

Search and Facet Flow

Search/filter/facet behavior is implemented through two parallel paths:

1. Blog list and legacy facet counts

  • Source queryset: Blog.unfiltered_published_posts (live descendants, ordered by -visible_date)

  • Filter layer: Blog.get_filterset(...) creates PostFilterset

  • Legacy facet API: FacetCountSerializer.get_facet_counts reads facet counts from the filterset and returns facet_counts.date_facets/category_facets/tag_facets

Frontend Components

  • Templates: Django/Wagtail templates

  • JavaScript:

    • Build system: Vite

    • Podcast player: Podlove (Vue.js app wrapped in web component)

    • Web components: Gallery viewer (used by GalleryWithLayout blocks)

    • Interactivity: HTMX

  • Styling: CSS (fully customizable via themes)

Code Organization

src/cast/
├── api/              # REST API
├── blocks.py         # StreamField blocks
├── content/          # Transport-neutral author/StreamField conversion
├── feeds.py          # RSS/podcast feeds
├── management/       # Django commands
├── migrations/       # Database migrations
├── models/           # Model definitions
├── static/           # Built assets
├── templates/        # Django templates
├── views/            # Django views
└── wagtail_hooks.py  # Admin customizations

The cast.content package owns conversion, rich-text normalization, media reference resolution, validation, and body section selection and merging without depending on REST Framework. Its sections module accepts raw StreamField section data and merges converted replacements while preserving existing section IDs, order, and omitted values. Missing overview/detail sections are inserted in canonical order. The editor API and test-only Wagtail v3 adapter share these functions. This package is an internal architectural boundary, not yet a supported extension API.

Extension Points

  1. StreamField Blocks: Add new content types in blocks.py

  2. Themes: Override templates or install theme packages (e.g., cast-vue, cast-bootstrap5)

  3. Management Commands: Add CLI tools

  4. API Endpoints: Extend REST API

Theme System

Themes provide complete control over the frontend:

  • Template replacement: Override any/all templates

  • Full customization: Use any CSS framework or approach

  • SPA support: Can function as headless CMS (e.g., cast-vue uses Vue.js SPA)

  • Package distribution: Themes as PyPI packages

The theme system allows everything from minor template tweaks to complete frontend replacements.

Deployment

Typical deployment:

  • Application: Django app server

  • Static files: Collected and served via your preferred static pipeline (e.g. nginx/CDN; whitenoise optional)

  • Media files: CDN or Django direct serving

  • Database: PostgreSQL (recommended) or SQLite

  • Caching: Django cache backend (for cache_page and any optional repository caching)

Security

  • User isolation: Users see only their own media

  • Spam filtering: Naive Bayes for comments

  • CSRF protection: Django middleware

  • XSS prevention: Template auto-escaping