Skip to content

Latest commit

 

History

History
1235 lines (948 loc) · 60.8 KB

File metadata and controls

1235 lines (948 loc) · 60.8 KB

Product Requirements Document: devopsdays.org Webapp Migration

Version: 1.1 (Draft — hybrid frontend architecture revision) Date: 2026-04-10 Author: Matty Stratton, with research assistance from Claude Status: Draft for Review


Table of Contents

  1. Executive Summary
  2. Problem Statement
  3. Goals and Non-Goals
  4. User Personas
  5. Data Model
  6. System Architecture
  7. Feature Requirements
  8. Tech Stack Recommendation
  9. Migration Strategy
  10. Infrastructure and Deployment
  11. Security and Access Control
  12. Integration Points
  13. Risks and Mitigations
  14. Success Metrics
  15. Open Questions

1. Executive Summary

devopsdays.org is the central website for the global devopsdays conference series — approximately 60-80 events per year across 6 continents, organized by independent local volunteer teams. The site currently runs on Hugo (a static site generator) with content managed via GitHub pull requests. This workflow, while functional for developers, creates significant barriers for non-technical organizers who struggle with git, Hugo, and the PR-based contribution model.

This document proposes migrating to a database-backed web application with a browser-based UI, enabling city organizers to manage their event pages without touching git, YAML, or a command line — while preserving 15+ years of devopsdays history (the canonical record of the devops movement) and maintaining the project's commitment to FOSS, volunteer maintainability, and decentralized governance.


2. Problem Statement

Current State

The devopsdays website is built on Hugo 0.152.2, deployed via Netlify, with all content stored in a GitHub repository. To update an event page, a city organizer must:

  1. Fork the repository (~11GB, including 4.1GB of git history)
  2. Install Hugo locally (version-sensitive, platform-specific issues common)
  3. Clone their fork, create a branch
  4. Edit YAML data files and Markdown content files (following specific naming conventions)
  5. Run Hugo locally to preview changes
  6. Commit, push, and open a pull request
  7. Wait for a core team member to verify their identity and merge

Core Problems

Accessibility barrier. devopsdays intentionally attracts organizers from across the tech spectrum — operations, security, leadership — not just developers. Many organizers cannot complete the workflow above. They hit walls at "install Hugo," "resolve merge conflicts," or "why does my YAML have a tab character." This is the primary forcing function for this project.

Repo bloat. The repository is ~11GB with ~13,000 markdown files, ~4,000 sponsor YAML files, and 600+ event asset directories. Cloning is painful. Hugo build times are non-trivial at this scale.

Manual authorization. Core team members must manually verify that PR submitters are authorized organizers for the events they're modifying. There is no programmatic access control.

Fragile content structure. Content is split between data/events/YYYY/city/main.yml (structured data), content/events/YYYY-city/*.md (page content), and static/events/YYYY-city/ (assets). A single misplaced field, wrong date format, or YAML indentation error can break the build for the entire site.

Shell-script tooling. Event setup relies on bash scripts (add_new_event.sh, add_speakers.sh, add_sponsors.sh, add_program.sh) that require command-line comfort and don't validate input.

No real-time updates. Changes require a full build-deploy cycle. During a live event, updating the schedule (e.g., open space topics) means going through the entire PR workflow.

What Works Today (Preserve These)

  • The event data model is mature and well-understood after 15+ years
  • Sponsor data is shared across events (reuse, not re-entry)
  • Netlify preview deployments give PR authors a way to verify changes
  • The decentralized governance model — local teams own their events
  • Integration with external tools (Pretalx for CFP, Pretix for ticketing) via links

3. Goals and Non-Goals

Goals

ID Goal Priority
G1 City organizers can manage their event pages entirely through a web browser P0
G2 Core team can onboard new events and manage user access through the web UI P0
G3 All historical event data (2009-present) remains accessible at stable URLs P0
G4 The system runs on FOSS software, self-hosted on infrastructure the core team controls P0
G5 Mobile-friendly editing interface for changes during live events P1
G6 Role-based access control: organizers see only their city/year, core sees everything P1
G7 Optional integration with Pretalx (CFP) and Pretix (ticketing) P2
G8 Sponsor directory is shared across events with per-event overrides P1
G9 The codebase is approachable for volunteer contributors (good DX, clear conventions) P1
G10 The system supports AI-assisted development without requiring it P1

Non-Goals (Explicitly Out of Scope)

ID Non-Goal Rationale
NG1 Replace Pretalx for CFP management Pretalx is a mature, purpose-built tool already in use
NG2 Replace Pretix for ticketing Same rationale as above
NG3 Sponsor self-service portal Future feature; city organizers manage sponsors initially
NG4 Speaker self-service profiles Future feature; city organizers manage speakers initially
NG5 Attendee accounts or community features The site is informational, not a community platform
NG6 Financial management or payment processing Handled externally (ConferenceOps, Stichting DevOps Foundation)
NG7 Email marketing or newsletter management Handled by Sendy
NG8 Built-in video hosting or media management YouTube/external hosting is fine

4. User Personas

City Organizer (Primary User)

Who: A local volunteer (often 1 of 3-25 team members) organizing a devopsdays event in their city. May or may not be a developer. Could be an ops engineer, a manager, a security professional, or a community organizer.

Needs:

  • Create and edit event pages (welcome, location, sponsors, schedule, speakers, contact)
  • Upload images (logos, speaker headshots, venue photos)
  • Update the event schedule, including day-of changes during live events
  • Add/remove team members displayed on the event page
  • Add sponsors from the shared directory or create new ones
  • Preview changes before they go live
  • Manage CFP and registration links (pointing to external Pretalx/Pretix or other tools)

Pain points today: git workflow, Hugo installation, YAML formatting, PR wait times, repo size.

Success looks like: "I can update our event page from my phone during the conference."

Core Team Member (Admin User)

Who: One of ~12 active core organizers (plus ~4 advisory members) who maintain the global devopsdays infrastructure and brand.

Needs:

  • Onboard new cities/events (create the event shell, set up initial organizer access)
  • Grant and revoke access for city organizers per event/year
  • Edit any event page (for corrections, emergencies, or when a city team needs help)
  • Manage global content (blog posts, about pages, organizing guide, code of conduct)
  • View dashboard of all events and their status (upcoming, active, past, cancelled)
  • Manage the shared sponsor directory

Pain points today: Manual PR review for identity verification, managing a massive repo, coordinating across 60+ simultaneous events.

Success looks like: "A new city emails us, I hop on a call, and 15 minutes later their event page exists and their team has access."

Public Visitor (Read-Only User)

Who: Potential attendees, speakers, sponsors, and anyone interested in devopsdays.

Needs:

  • Find upcoming events near them
  • View event details (dates, location, schedule, speakers, sponsors)
  • Access historical event information
  • Read blog posts

No authentication required.


5. Data Model

The data model is derived from 15+ years of the existing Hugo site's content structure. Entity names and relationships are battle-tested.

Entity Relationship Overview

                         ┌──────────────┐
                         │  CoreTeam    │
                         │  Member      │
                         └──────┬───────┘
                                │ manages
                                ▼
┌──────────┐  has many  ┌──────────────┐  has many  ┌──────────────┐
│   City   │◄──────────►│    Event     │◄──────────►│  EventPage   │
└──────────┘            └──────┬───────┘            └──────────────┘
                               │
              ┌────────────────┼────────────────┐
              │                │                │
              ▼                ▼                ▼
     ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
     │  TeamMember  │ │EventSponsor  │ │ProgramEntry  │
     │  (per-event) │ │  (junction)  │ │              │
     └──────────────┘ └──────┬───────┘ └──────┬───────┘
                             │                │
                             ▼                ▼
                      ┌──────────────┐ ┌──────────────┐
                      │   Sponsor    │ │    Talk      │
                      │  (global)    │ │              │
                      └──────────────┘ └──────┬───────┘
                                              │
                                              ▼
                                       ┌──────────────┐
                                       │   Speaker    │
                                       │  (per-event) │
                                       └──────────────┘

Core Entities

City

Represents a geographic location that hosts (or has hosted) devopsdays events.

Field Type Notes
id PK Auto-generated
slug string, unique Lowercase, no spaces: amsterdam, new-york-city, sao-paulo
display_name string Capitalized: Amsterdam, New York City, São Paulo
country string Optional, for filtering
coordinates lat/lng For map display and "events near me"
created_at timestamp

Event

A specific year's devopsdays in a specific city. This is the central entity.

Field Type Notes
id PK Auto-generated
city FK → City
year integer
slug string, unique Derived: 2024-austin
status enum planning, announced, cfp_open, registration_open, active, past, cancelled
description text Event tagline
start_date datetime w/ tz
end_date datetime w/ tz
cfp_date_start datetime w/ tz Nullable
cfp_date_end datetime w/ tz Nullable
cfp_date_announce datetime w/ tz Nullable
cfp_open boolean Manual override
cfp_link url External CFP URL (Pretalx, Sessionize, etc.)
registration_date_start datetime w/ tz Nullable
registration_date_end datetime w/ tz Nullable
registration_closed boolean Manual override
registration_link url External registration URL (Pretix, Eventbrite, etc.)
location_name string Venue name
location_address string Street address
location_coordinates lat/lng Venue-specific coordinates
organizer_email email city@devopsdays.org
event_social_twitter string Twitter/X handle
event_social_linkedin url
event_social_bluesky url
event_social_youtube url
event_social_mastodon url
ga_tracking_id string Optional event-specific analytics
masthead_background image
sharing_image image Open Graph / social sharing image
sponsors_accepted boolean Whether the "Become a Sponsor" link shows
created_at timestamp
updated_at timestamp

Unique constraint: (city, year)

EventPage

Freeform content pages belonging to an event. Replaces the individual .md files.

Field Type Notes
id PK
event FK → Event
page_type enum welcome, location, contact, conduct, sponsor, propose, registration, schedule, speakers, custom
title string Page title
slug string URL slug
content text (Markdown/rich text) Page body
is_visible boolean Controls nav visibility
sort_order integer Nav ordering
created_at timestamp
updated_at timestamp

Unique constraint: (event, slug)

Sponsor (Global)

Shared sponsor directory. A sponsor entity exists once globally; events reference it.

Field Type Notes
id PK
slug string, unique Lowercase identifier: newrelic, hashicorp
name string Display name: New Relic, HashiCorp
url url Company website
logo image Current logo (PNG, transparent/white bg)
twitter string Optional
created_at timestamp
updated_at timestamp

SponsorLevel

Defines the sponsorship tiers available for a specific event.

Field Type Notes
id PK
event FK → Event
slug string gold, platinum, coffee, happyhour
label string Display: Gold, Platinum, Coffee Bar, Happy Hour
sort_order integer Display ordering
max_sponsors integer 0 = unlimited

Unique constraint: (event, slug)

EventSponsor (Junction)

Links a sponsor to an event at a specific level, with optional overrides.

Field Type Notes
id PK
event FK → Event
sponsor FK → Sponsor
level FK → SponsorLevel
url_override url Nullable — event-specific landing page
logo_override image Nullable — event-specific logo variant
created_at timestamp

Unique constraint: (event, sponsor)

Speaker (Per-Event)

Speakers are scoped to a specific event (same person speaking at two events = two records).

Field Type Notes
id PK
event FK → Event
slug string URL-friendly: charity-majors
name string Charity Majors
bio text (Markdown)
image image Headshot
twitter string Optional
linkedin url Optional
github string Optional
website url Optional
created_at timestamp

Unique constraint: (event, slug)

Talk

A presentation, ignite talk, or workshop at an event.

Field Type Notes
id PK
event FK → Event
slug string URL-friendly
title string Talk title
talk_type enum talk, ignite, workshop, open-space
abstract text (Markdown) Talk description
speakers M2M → Speaker One or more speakers
youtube_url url Post-event video
slides_url url Slide deck link
created_at timestamp

Unique constraint: (event, slug)

ProgramEntry

A time slot in the event schedule. Can reference a Talk or be a standalone entry (lunch, registration, etc.).

Field Type Notes
id PK
event FK → Event
date date Which day of the event
start_time time 09:15
end_time time 09:55
entry_type enum talk, ignite, workshop, open-space, break, custom
talk FK → Talk Nullable (only for talk/ignite/workshop entries)
custom_title string For breaks, registration, lunch, etc.
comments text Optional notes (room, track, etc.)
background_color string Optional visual styling
sort_order integer For ordering within same time slot
created_at timestamp

TeamMember (Per-Event)

An organizer listed on a specific event's page.

Field Type Notes
id PK
event FK → Event
name string Display name
image image Optional headshot
employer string Optional
bio text Optional
twitter string Optional
linkedin url Optional
github string Optional
website url Optional
facebook url Optional
sort_order integer Display ordering

User (Authentication & Authorization)

Field Type Notes
id PK
email email, unique Primary identifier
name string Display name
oauth_provider enum github, google
oauth_id string Provider-specific ID
is_core_team boolean Global admin flag
is_active boolean Account enabled
created_at timestamp
last_login timestamp

EventPermission

Grants a user access to manage a specific event.

Field Type Notes
id PK
user FK → User
event FK → Event
role enum organizer, editor
granted_by FK → User Core team member who granted access
created_at timestamp

Unique constraint: (user, event)

BlogPost

Field Type Notes
id PK
slug string, unique
title string
content text (Markdown)
author string Author name
published_at datetime Nullable (draft if null)
created_at timestamp
updated_at timestamp

StaticPage

Global pages like About, Organizing Guide, Speaking, etc.

Field Type Notes
id PK
slug string, unique about, organizing, speaking, conduct
title string
content text (Markdown)
updated_at timestamp

ArchivedEvent

For pre-cutoff events served as static HTML snapshots.

Field Type Notes
id PK
slug string, unique 2009-ghent, 2013-austin
year integer
city_name string Display name
static_html_path string Path to archived HTML directory

6. System Architecture

High-Level Architecture

┌──────────────────┐              ┌──────────────────┐
│  Public Users    │              │  City Organizers  │
│  (Browsers)      │              │  & Core Team      │
└────────┬─────────┘              └────────┬──────────┘
         │ HTTPS                           │ HTTPS
         ▼                                 ▼
┌──────────────────────────────────────────────────────┐
│                  CDN / Nginx                          │
│            (reverse proxy, cache, SSL)                │
└──────┬────────────┬────────────────┬─────────────────┘
       │            │                │
       ▼            ▼                ▼
┌────────────┐ ┌─────────────┐ ┌──────────────────────┐
│  Static    │ │  Media      │ │     Django App        │
│  Archives  │ │  Files      │ │                       │
│  (HTML)    │ │  (S3 or     │ │  ┌─────────────────┐  │
│            │ │   local)    │ │  │ Public Site      │  │
│  pre-2020  │ │             │ │  │ (Django templates│  │
│  events    │ │  logos,     │ │  │  + Tailwind)     │  │
│            │ │  headshots, │ │  └─────────────────┘  │
└────────────┘ │  etc.       │ │                       │
               └─────────────┘ │  ┌─────────────────┐  │
                               │  │ Editing UI       │  │
                               │  │ (Inertia.js +    │  │
                               │  │  Vue 3 SPA)      │  │
                               │  └─────────────────┘  │
                               │                       │
                               │  ┌─────────────────┐  │
                               │  │ Django Admin     │  │
                               │  │ (core team)      │  │
                               │  └─────────────────┘  │
                               └───────────┬───────────┘
                                           │
                                  ┌────────┼────────┐
                                  │                 │
                                  ▼                 ▼
                         ┌──────────────┐  ┌──────────────┐
                         │  PostgreSQL  │  │    Redis     │
                         │  Database    │  │   (cache,    │
                         │              │  │   sessions)  │
                         └──────────────┘  └──────────────┘

How the Hybrid Rendering Works

PUBLIC SITE (read-only, server-rendered):
  Browser → Nginx → Django view → Django template → HTML response
  Example: GET /events/2026-austin/ → server-rendered HTML + Tailwind CSS

EDITING UI (interactive, SPA-like):
  Browser → Nginx → Django view → Inertia.js → Vue component (client-side render)
  Example: GET /manage/events/2026-austin/sponsors/ → Vue SPA with drag-and-drop

  On navigation within the editing UI:
  Vue component → Inertia XHR → Django view → JSON props → Vue re-renders (no page reload)

DJANGO ADMIN (core team, server-rendered):
  Browser → Nginx → Django admin → Django templates → HTML response
  Example: GET /admin/events/event/ → standard Django admin (with modern theme)

Architecture Decisions

Hybrid rendering: server-rendered public site + modern interactive editing UI. The public-facing site is server-rendered HTML via Django templates — great for SEO, works without JavaScript, fast, and simple. The organizer editing UI uses Inertia.js + Vue 3 to deliver a modern single-page-app experience (no page reloads, smooth transitions, rich interactive components) while keeping Django as the sole backend — no separate API layer, no second server process, single deployment.

Why this split? The public site is 95% read-only content — React/Vue adds zero value there and complicates SSR, SEO, and caching. The editing UI is forms, drag-and-drop, live previews, and image management — exactly where a modern JS framework earns its keep. Inertia.js is the bridge: Django views return data instead of HTML, and Vue components render it client-side. Auth, routing, validation, and business logic all stay in Django.

Monolith, not microservices. A single Django application handles public pages, the editing UI, and the API. Microservices add operational complexity that volunteer maintainers don't need. The monolith can be decomposed later if that need ever actually materializes (it probably won't).

PostgreSQL as the single source of truth. All structured data lives in PostgreSQL. No YAML files, no git repositories for content. The database is backed up regularly and can be restored trivially.

Static archives for historical events. Events before a cutoff year (suggested: 2020) are served as pre-rendered static HTML from a directory, routed by Nginx. These pages don't hit the database or application server, keeping the dynamic system focused on active and recent events. The cutoff can be moved forward over time.

Media storage. Images (logos, headshots, backgrounds) are stored either on S3 (if cost is acceptable) or on local disk behind Nginx. Either way, they're served directly by the web server / CDN, not by Django.

Why Django + Inertia.js + Vue?

Django (backend):

  1. Batteries included. Built-in admin panel, ORM, auth system, form handling, migrations. This is a LOT of functionality you don't have to build or maintain.
  2. The admin panel is 60% of the core team UI for free. Django's admin, with customization, handles most of what infrastructure maintainers need. The custom Vue-based UI is for city organizers.
  3. Huge volunteer pool. Django is one of the most widely-known web frameworks. The incoming lead maintainer is a Python developer. Finding contributors is maximally easy.
  4. Excellent ecosystem. django-allauth for OAuth, django-storages for S3, django-imagekit for image processing — these are mature, maintained packages.
  5. FOSS. BSD licensed.
  6. Proven at scale. Instagram, Mozilla, Disqus, Pinterest all built on Django. A site serving ~60-80 events/year is not going to stress it.

Inertia.js (glue layer):

  1. No REST API to build or maintain. Inertia adapts Django views to serve as data sources for Vue components. You write Django views that return props, not JSON serializers.
  2. Single deployment. The Vue app is bundled by Vite and served by Django's static files. No separate Node.js server.
  3. Django handles auth, sessions, CSRF, permissions. All the security plumbing stays server-side where it's battle-tested.
  4. Shared routing. URLs are defined in Django's urls.py — no client-side router to keep in sync.
  5. Progressive adoption. You can add Inertia to specific views without converting the whole app. Public site stays Django templates; editing UI uses Inertia+Vue.

Vue 3 (editing UI frontend):

  1. Gentler learning curve than React for occasional/volunteer contributors who aren't full-time frontend developers. Single-file components, simpler mental model, less boilerplate.
  2. Rich component ecosystem. Libraries like VueDraggable (schedule builder), TipTap (rich text editor), HeadlessUI (accessible components) cover the complex interactive needs.
  3. TypeScript optional. Contributors can write plain JavaScript or TypeScript — Vue supports both without requiring either.
  4. Strong community. Vue is the second-most-popular frontend framework and has excellent documentation.

7. Feature Requirements

Features are organized into three phases. Each phase should be deployable independently and provide standalone value.

Phase 1: Core Platform (MVP)

The minimum viable product that replaces the Hugo workflow for new events. Existing events can continue on Hugo during this phase.

1.1 Authentication & Authorization

  • OAuth login via GitHub and Google (using django-allauth)
  • User roles: Core Team (global admin) and City Organizer (event-scoped)
  • Event permissions: Core team grants organizers access to specific city/year combinations
  • Permission management UI for core team members
  • No self-registration — users are invited/granted access by core team

1.2 Event Management (Core Team)

  • Create new event: Select or create city, set year, generate slug
  • Event dashboard: List all events with status, dates, and organizer count
  • Cancel event: Set status to cancelled, update pages automatically
  • Clone event: Copy a previous year's event as a starting point for a new year (same city)

1.3 Event Editing (City Organizers)

All editing happens in the Vue-based organizer UI (/manage/events/{slug}/). No git, no YAML, no command line.

  • Event settings: Edit dates, location, description, social links, CFP/registration URLs — form-based with timezone-aware date pickers
  • Content pages: TipTap rich text editor with dual mode — WYSIWYG for non-technical organizers, Markdown input for those who prefer it. Content stored as Markdown, rendered on the public site.
  • Team members: Add/edit/remove/reorder (drag-and-drop via VueDraggable) team members with image upload (FilePond with crop/preview)
  • Navigation: Toggle which pages appear in the event nav and drag to reorder
  • Image upload: Drag-and-drop for masthead background, sharing image, team member photos — with instant preview and auto-resize
  • Preview: Live preview pane or "View public page" link to see the rendered result

1.4 Sponsor Management

  • Global sponsor directory: Searchable list of all sponsors with name, URL, logo
  • Add sponsor to event: Search the directory, select a level; or create a new sponsor if not found
  • Sponsor levels: Define custom sponsorship tiers per event (with labels, max caps, ordering)
  • Logo override: Optionally upload an event-specific logo variant for a sponsor

1.5 Public Site

  • Homepage: Upcoming events list, event map, search
  • Event pages: All page types rendered from database content
  • Event listing: Filterable by year, city, region
  • Blog: Read-only display of blog posts (core team authors via admin)
  • Static pages: About, organizing guide, speaking, code of conduct
  • URL compatibility: All existing URLs continue to work (redirects where needed)
  • SEO: Open Graph tags, sharing images, structured data
  • Responsive design: Works on mobile, tablet, desktop
  • RSS feed for blog

1.6 Static Archives

  • Serve archived HTML for historical events at their original URLs
  • Nginx routing: Requests for archived event slugs serve static files; everything else hits Django
  • Archive index: Archived events appear in the event listing with links to their static pages

Phase 2: Enhanced Editing & Operations

2.1 Speaker & Talk Management

  • Add speakers: Name, bio, headshot, social links — per event
  • Add talks: Title, abstract, type (talk/ignite/workshop), link to speaker(s)
  • Post-event enrichment: Add YouTube URL, slides URL after the event

2.2 Program/Schedule Builder

This is the most interactive component and a showcase for the Vue-based editing UI.

  • Visual schedule editor: Drag-and-drop schedule builder (VueDraggable) with a timeline/grid view per day
  • Day-based view: Tab per event day, with entries rendered as draggable cards
  • Entry types: Talk (linked to talk entity), ignite, workshop, open space, break/custom — each with distinct visual styling
  • Time slots: Set start/end times for each entry with click-to-edit or drag-to-resize
  • Live updates: Schedule changes during events publish immediately — no build cycle, no deploy. The public schedule page queries the database directly.
  • Mobile-optimized editing for day-of schedule changes — simplified list view with quick-edit for times and titles (critical for open space topic entry during the event)

2.3 Dashboard & Reporting

  • Core team dashboard: Events by status, upcoming milestones (CFP deadlines, registration opens), events needing attention
  • Event health indicators: Is the event page filled out? Does it have sponsors? Is the program complete?
  • Activity log: Track who changed what and when (audit trail)

2.4 Pretalx Integration (Optional)

  • Link event to Pretalx instance: Store the Pretalx event URL
  • Import speakers and talks from Pretalx API after CFP closes
  • Sync program: Pull schedule data from Pretalx into the schedule builder
  • One-way sync (Pretalx → devopsdays), not bidirectional

2.5 Pretix Integration (Optional)

  • Link event to Pretix shop: Store the Pretix event URL
  • Display ticket status (on sale, sold out) on the event page
  • Embed Pretix widget on registration page (Pretix supports this natively)

Phase 3: Advanced Features & Polish

3.1 Sponsor Self-Service Portal

  • Sponsors can log in, update their company info and logo
  • Submit sponsorship interest for specific events
  • View their sponsorship history across events

3.2 Speaker Self-Service

  • Speakers can log in, update their bio and links
  • Upload slides and video links post-event
  • View their speaking history across devopsdays events

3.3 Content Templates

  • City organizers can start from templates (e.g., "standard 2-day event," "single-day event")
  • Templates include default page content, sample schedule, suggested sponsor levels
  • Core team maintains templates

3.4 Notification System

  • Email notifications for core team: new event created, access requested
  • Email notifications for organizers: CFP deadline approaching, reminder to publish schedule
  • Optional Slack integration for notifications

3.5 Analytics Dashboard

  • Aggregate view of event data across years (growth, geography)
  • Per-event page view analytics (if self-hosted analytics like Plausible or Umami are adopted)

3.6 API

  • Public read-only REST API for event data
  • Enables community-built tools, widgets, and integrations
  • API documentation (OpenAPI/Swagger)

8. Tech Stack Recommendation

Backend

Component Choice Rationale
Language Python 3.12+ Largest volunteer pool, excellent ecosystem, AI-tooling friendly. Incoming lead maintainer is a Python developer.
Framework Django 5.x Batteries included (admin, ORM, auth, forms, migrations)
Database PostgreSQL 16 FOSS, robust, excellent Django support, handles the data model naturally
Cache Redis Session storage, page fragment caching, rate limiting
Task queue None initially Add Celery + Redis later if async tasks needed (email, imports)
Search PostgreSQL full-text search Good enough for the data volume; avoids Elasticsearch complexity
Glue layer Inertia.js (inertia-django) Connects Django views to Vue components without building a REST API

Frontend — Public Site (Server-Rendered)

The public-facing site that attendees, speakers, and sponsors see. Server-rendered for performance, SEO, and simplicity.

Component Choice Rationale
Templates Django templates Server-rendered, no JS build step, works without JavaScript
CSS Tailwind CSS 4.x Utility-first, modern design out of the box, excellent documentation. Replaces the aging Bootstrap 4 completely.
Interactivity Alpine.js (minimal) Lightweight sprinkles for dropdowns, modals, mobile nav. No heavy framework needed for read-only pages.
Icons Heroicons Tailwind-native SVG icon set, clean modern aesthetic
Maps Leaflet.js (FOSS) For event location maps and "events near me" — no Google Maps API key required

Frontend — Editing UI (SPA via Inertia.js + Vue 3)

The organizer-facing interface where city teams manage their events. Modern SPA experience powered by Vue 3, served through Django via Inertia.js.

Component Choice Rationale
Framework Vue 3 (Composition API) Gentler learning curve than React for volunteer/occasional contributors. Single-file components.
Routing Inertia.js (server-driven) URLs defined in Django's urls.py. No client-side router to maintain separately.
Build tool Vite Fast dev server with HMR, zero-config for Vue, outputs optimized bundles served by Django's static files.
CSS Tailwind CSS 4.x (shared with public site) Consistent design language across both surfaces
Component library Headless UI (Vue) Accessible, unstyled primitives (modals, menus, comboboxes) — styled with Tailwind
Rich text editor TipTap 2 Modern, extensible editor built on ProseMirror. Supports Markdown input/output AND WYSIWYG editing — organizers choose their preferred mode. Vue-native.
Drag-and-drop VueDraggable (SortableJS) For schedule builder (reorder program entries), team member ordering, nav ordering
Image upload FilePond (Vue adapter) Drag-and-drop image upload with preview, cropping, and progress indicators
Date/time pickers VCalendar or Flatpickr Timezone-aware date/time selection for event dates, CFP deadlines, etc.
TypeScript Optional Vue 3 supports TS natively but doesn't require it. Contributors can use JS or TS.

Admin (Core Team)

Component Choice Rationale
Admin panel Django Admin (customized with django-unfold or django-jazzmin) Free, powerful, covers 80% of core team needs. A modern admin theme makes it feel current without custom development.
Fallback Core team can also use the organizer editing UI They have global permissions, so the Vue-based editing UI works for them too — the Django admin is for bulk operations and data management

Authentication

Component Choice Rationale
OAuth django-allauth Mature, supports GitHub + Google + many others
Session management Django sessions + Redis Standard, secure, scalable. Inertia.js uses cookie-based sessions (no JWT complexity).

Infrastructure

Component Choice Rationale
Application server Gunicorn Standard Python WSGI server
Reverse proxy Nginx Static file serving, SSL termination, caching, archived event routing
Containers Docker + Docker Compose Portable, reproducible, easy for contributors to run locally
Hosting AWS EC2 (existing account) Core team already has AWS for Pretalx/Pretix
Media storage S3 or local disk + Nginx S3 if budget allows, local disk as fallback
SSL Let's Encrypt (certbot) Free, automated
Backups pg_dump to S3 on a cron Simple, reliable, restorable
Monitoring Sentry (FOSS, self-hosted) or basic logging Error tracking without SaaS cost

Development Tooling

Component Choice Rationale
Local dev Docker Compose One command: docker compose up — starts Django, PostgreSQL, Redis, Vite dev server, and Mailpit
Python formatting Ruff Fast Python linter + formatter
JS formatting ESLint + Prettier Standard for Vue/JS projects
Python testing pytest + Django test client Standard, comprehensive
JS testing Vitest Fast, Vite-native test runner for Vue components
E2E testing Playwright Browser-based testing for critical organizer workflows
CI GitHub Actions Already in use for the Hugo site
Pre-commit hooks pre-commit Lint, format, type-check on commit

Design System

The visual refresh is as important as the technical migration. The current site uses Bootstrap 4.3 (2019) with accumulated custom CSS. The new system should feel modern and clean.

Aspect Approach
Typography Inter (body) + JetBrains Mono (code). Clean, modern, excellent readability. Both FOSS.
Color palette Derive from existing devopsdays brand colors, extended with Tailwind's color scale for UI elements
Layout Responsive grid using Tailwind's built-in breakpoints. Mobile-first.
Components Headless UI primitives + Tailwind styling. No Bootstrap, no jQuery, no legacy CSS.
Dark mode Defer to Phase 2 or 3. Nice-to-have, not critical. Tailwind makes it easy to add later.
Accessibility WCAG 2.1 AA minimum. Headless UI components are accessible by default. Semantic HTML on public pages.

9. Migration Strategy

Migration is the hardest part of this project. The approach is phased, running both systems in parallel until the new one is proven.

Phase 0: Static Archive Generation (Pre-Migration)

Goal: Reduce the scope of what needs to be dynamically migrated.

  1. Choose a cutoff year (suggested: 2020). All events before this year become static archives.
  2. Generate static HTML for each pre-cutoff event by rendering the Hugo site and capturing the output per-event.
  3. Store the HTML in a directory structure that Nginx can serve directly: /archives/events/2019-amsterdam/index.html, etc.
  4. Verify every archived event's URLs resolve correctly.

This step can be done entirely within the existing Hugo infrastructure. It removes ~300+ events from the dynamic migration scope.

Phase 1: Data Migration (Hugo → PostgreSQL)

Goal: Import all post-cutoff event data into the database.

  1. Write migration scripts (Python) that parse:
    • data/events/YYYY/city/main.yml → Event, TeamMember, SponsorLevel, EventSponsor, ProgramEntry records
    • data/sponsors/*.yml → Sponsor records
    • content/events/YYYY-city/*.md → EventPage records (extract frontmatter + body)
    • content/events/YYYY-city/speakers/*.md → Speaker records
    • content/events/YYYY-city/program/*.md → Talk records
    • data/core.toml → User records with is_core_team=True
    • content/blog/*.md → BlogPost records
    • content/page/*.md → StaticPage records
  2. Copy media files: Event images, sponsor logos, speaker headshots → media storage (S3 or local)
  3. Validate the migration by comparing rendered output of the new system against the Hugo output for every migrated event.
  4. Run the migration script repeatedly during development, treating it as idempotent. Final production run happens at cutover.

Phase 2: Parallel Running

Goal: Prove the new system works without risking the live site.

  1. Deploy the new system at a staging URL (e.g., beta.devopsdays.org)
  2. Route new events (e.g., all 2027 events) to the new system while keeping the Hugo site live for existing events
  3. City organizers for new events use the web UI exclusively
  4. Core team tests all workflows: event creation, user management, content editing
  5. Public users see the new system for new events, Hugo site for existing events (Nginx routing)
  6. Duration: At least one full event cycle (an event goes from creation → CFP → program → live → archived)

Phase 3: Cutover

Goal: Retire the Hugo site.

  1. Migrate remaining Hugo events to the database (any events added to Hugo during Phase 2)
  2. Switch DNS from Netlify to the new infrastructure
  3. Set up redirects for any URL patterns that changed
  4. Keep the Hugo repo archived (read-only) as a historical reference
  5. Monitor intensively for broken links, missing content, and access issues

Migration Data Volume Estimates

Entity Approximate Count Notes
Cities ~200 Unique cities across all years
Events (dynamic) ~200-300 Post-2020 events
Events (archived) ~300+ Pre-2020 events as static HTML
Sponsors (global) ~4,000 Shared across all events
Team members ~5,000-8,000 Across all events
Event pages ~2,000-3,000 ~10 pages per dynamic event
Speakers ~3,000-5,000 Across all events with program data
Talks ~3,000-5,000 Across all events with program data
Program entries ~10,000-15,000 Including breaks, customs
Blog posts ~45
Static pages ~10
Images/media ~10,000+ files Logos, headshots, backgrounds

10. Infrastructure and Deployment

Production Environment

AWS EC2 Instance(s):
├── Docker Compose
│   ├── nginx          (reverse proxy, static files, SSL, archived events)
│   ├── django-app     (Gunicorn, the web application)
│   ├── postgres       (database)
│   └── redis          (cache, sessions)
├── /data/archives/    (static HTML for pre-cutoff events)
├── /data/media/       (uploaded images — or S3)
└── /data/backups/     (pg_dump output)

Instance sizing (estimated):

  • A t3.medium (2 vCPU, 4GB RAM) should be more than sufficient for the traffic level
  • Bump to t3.large if needed after launch
  • Consider a separate RDS instance for PostgreSQL if operational simplicity is worth the cost (~$15-30/month for db.t3.micro)

Estimated monthly cost:

  • EC2 t3.medium: ~$30/month
  • EBS storage (50GB): ~$5/month
  • S3 (media, backups): ~$5/month
  • Route 53 (DNS): ~$1/month
  • Total: ~$40-50/month (comparable to current Netlify usage)

Local Development

# Clone the repo
git clone https://github.com/devopsdays/devopsdays-web-app.git
cd devopsdays-web-app

# Start everything
docker compose up

# Public site at http://localhost:8000
# Editing UI at http://localhost:8000/manage/
# Django admin at http://localhost:8000/admin/
# Vite dev server (HMR) at http://localhost:5173 (proxied by Django)
# Mailpit (email testing) at http://localhost:8025

Requirements: Docker and Docker Compose. Nothing else. No Python installation, no Node.js installation, no database setup, no environment variable wrangling.

The docker-compose.yml includes:

  • Django app with auto-reload (Gunicorn in dev mode)
  • Vite dev server with hot module replacement for Vue components
  • PostgreSQL with a seed database (sample events for development)
  • Redis
  • Mailpit (catches outgoing emails for testing)

For frontend-only contributors:

  • Edit .vue files and see changes instantly via Vite HMR
  • No need to understand Django — the Inertia adapter handles the integration
  • Vue devtools work normally in the browser

For backend-only contributors:

  • Public site templates are plain Django templates — no JS build knowledge needed
  • Django admin works out of the box
  • Inertia views are just regular Django views that return dicts instead of render()

CI/CD Pipeline

GitHub Actions:
├── On PR:
│   ├── Lint (Ruff)
│   ├── Type check (mypy)
│   ├── Tests (pytest)
│   └── Build Docker image (verify it builds)
│
├── On merge to main:
│   ├── Build Docker image
│   ├── Push to container registry (GitHub Container Registry)
│   ├── SSH to production server
│   └── docker compose pull && docker compose up -d
│
└── Scheduled:
    ├── Database backup (pg_dump → S3)
    └── Dependency security audit (pip-audit)

Backup Strategy

  • Database: pg_dump every 6 hours to S3 with 30-day retention
  • Media files: If using local disk, rsync to S3 daily. If using S3, versioning is sufficient.
  • Configuration: All in the git repo (Docker Compose files, Nginx config, Django settings)
  • Disaster recovery: Restore from backup = docker compose up + pg_restore + copy media. Target RTO: < 1 hour.

11. Security and Access Control

Authentication

  • OAuth only — no username/password accounts. Reduces attack surface and password management burden.
  • Supported providers: GitHub and Google (via django-allauth)
  • No self-registration. A core team member must create an account invitation or grant permissions after the user's first OAuth login.
  • Session management: Server-side sessions stored in Redis. Configurable timeout (suggest: 30 days with activity, 24 hours idle).

Authorization Model

Permission Hierarchy:

Super Admin (Django superuser)
  └── Can do everything, including Django admin access
      └── Reserved for 1-2 infrastructure maintainers

Core Team (is_core_team=True)
  └── Can:
      ├── Create/edit/cancel any event
      ├── Grant/revoke EventPermissions for any user
      ├── Manage global sponsors
      ├── Edit blog posts and static pages
      ├── View all events dashboard
      └── Access Django admin (read-only for models they don't own)

City Organizer (EventPermission with role=organizer)
  └── Can:
      ├── Edit event settings for their granted events only
      ├── Manage content pages for their events
      ├── Manage team members, sponsors, speakers, talks, program for their events
      ├── Upload images for their events
      └── Cannot: create events, manage users, edit other events, access admin

Editor (EventPermission with role=editor)
  └── Can:
      ├── Edit content pages for their granted events
      └── Cannot: change event settings, manage sponsors/speakers/team

Data Security

  • HTTPS everywhere (Let's Encrypt)
  • CSRF protection (Django's built-in middleware)
  • SQL injection protection (Django ORM — no raw SQL)
  • XSS protection (Django's template auto-escaping + Content Security Policy headers)
  • Image upload validation: File type checking, size limits, image processing to strip metadata
  • Rate limiting on auth endpoints (django-ratelimit or Nginx)
  • Security headers: HSTS, X-Content-Type-Options, X-Frame-Options via Nginx

12. Integration Points

Pretalx (CFP Management)

  • Type: Optional, one-way sync (Pretalx → devopsdays)
  • Mechanism: Pretalx REST API
  • Data flow: After CFP closes, organizer clicks "Import from Pretalx" to pull accepted speakers and talks
  • Mapping: Pretalx speaker → Speaker entity; Pretalx submission → Talk entity
  • Conflict handling: Import creates new records; re-importing updates existing records matched by external ID
  • Not all events use Pretalx. The UI must support manual speaker/talk entry as the default path.

Pretix (Ticketing)

  • Type: Optional, display-only
  • Mechanism: Link to Pretix event or embed Pretix widget (iframe)
  • Data flow: Organizer enters their Pretix event URL; the system generates the embed code
  • No ticket data is stored in the devopsdays database

Google Workspace (Email)

  • Type: Reference only
  • Mechanism: Organizer email (city@devopsdays.org) is stored as a string field
  • No programmatic integration with Google Workspace

Mastodon (social.devopsdays.org)

  • Type: Link/reference only
  • Mechanism: Webfinger redirect (currently handled by Netlify redirect; move to Nginx)

Slack

  • Type: Optional future integration
  • Mechanism: Slack webhook for notifications (new event created, permissions granted)
  • Phase 3 feature

13. Risks and Mitigations

High Risk

Risk Impact Likelihood Mitigation
Volunteer burnout during build Project stalls, half-built system is worse than the current one High Phase aggressively. Phase 1 MVP must be small enough for 2-3 motivated contributors to complete. Set a realistic timeline (months, not weeks).
URL breakage on migration Broken links across the internet to 15 years of content. Inbound links from blogs, wikis, Wikipedia. High Exhaustive redirect mapping. Generate a complete URL inventory from the Hugo site. Test every URL before cutover. Keep Netlify running in parallel during transition.
Data loss during migration Missing events, sponsors, content Medium Idempotent migration scripts. Automated comparison between Hugo output and new system output. Run migration in staging repeatedly before production cutover.

Medium Risk

Risk Impact Likelihood Mitigation
Operational complexity Static site needed zero ops; a webapp needs uptime monitoring, DB maintenance, security patches Medium Docker Compose simplifies ops. Automated backups. Sentry for error monitoring. Document runbooks for common tasks.
Performance at scale Public site slower than static HTML Low-Medium Aggressive page caching (Redis or Nginx). The site's traffic is modest (~60-80 events, not millions of users). CDN for static assets.
Scope creep "While we're at it, let's also..." kills the project High This PRD exists to prevent that. Phase 1 is strictly defined. Features not in Phase 1 don't get built in Phase 1.
Security incident Database-backed site has attack surface that static HTML doesn't Low Django's security track record is strong. OAuth-only auth. No sensitive data stored (no payments, no PII beyond names/emails). Automated security scanning in CI.

Low Risk

Risk Impact Likelihood Mitigation
Django/Python version incompatibility Long-term maintenance burden Low Django LTS releases every 2 years. Pin to LTS versions. Dependabot for automated updates.
AWS cost increases Budget pressure Low Infrastructure cost is minimal (~$50/month). Could migrate to another provider; Docker makes this portable.

14. Success Metrics

Launch Criteria (Phase 1 Complete)

  • A city organizer with no git experience can create and fully populate an event page using only a web browser
  • All post-cutoff event data is migrated and renders correctly
  • All pre-cutoff events are accessible as static archives at their original URLs
  • Core team can onboard a new event in < 15 minutes
  • Zero broken inbound links from the Hugo site (verified by crawl comparison)
  • Site loads in < 2 seconds (p95) for public pages
  • At least 3 real events have been run through the full lifecycle on the new system

Ongoing Success Metrics

  • Time to first content: How long from "organizer gets access" to "event page is live"? Target: < 30 minutes (current: hours to days)
  • Support requests: Decrease in "help, I can't update my event page" messages to core team
  • Contributor count: Number of volunteers contributing to the codebase (should increase with lower barrier to entry)
  • Organizer satisfaction: Qualitative feedback from city organizers (survey after each event)
  • Uptime: 99.5%+ (allows for maintenance windows)

15. Open Questions

These items need further discussion and decision before or during implementation:

Architecture

  1. Archive cutoff year: Suggested 2020, but could be 2022 or 2023. What's the oldest event we'd want to allow editing for? The more we archive, the less we migrate dynamically, but the less historical data is queryable.

  2. Rich text vs Markdown: Resolved — TipTap 2 supports both WYSIWYG and Markdown input modes. Content is stored as Markdown for portability and clean rendering. Organizers can toggle between modes per their preference.

  3. Multi-language support: Some events have translated conduct pages (Portuguese, French). Is i18n a Phase 1 requirement, or can it be deferred?

  4. Search: Is PostgreSQL full-text search sufficient, or will users expect more sophisticated search (fuzzy matching, filters, etc.)? This affects whether we need something like MeiliSearch.

Organizational

  1. Naming and branding: Is this a new repo (devopsdays-web-app)? Does it get a project name? Should the codebase have a fun name or stay boring?

  2. Governance for the new system: Who can merge PRs to the webapp codebase? Same CODEOWNERS model as the Hugo site?

  3. Domain handling: Does the new system serve www.devopsdays.org directly, or does it sit behind a reverse proxy at a different domain during transition?

  4. Onboarding existing organizers: When the new system launches, how do we get 60+ active organizing teams to create accounts and start using it? Mass email? Slack announcement? Gradual rollout by city?

Data

  1. Sponsor versioning: The current system has versioned sponsor IDs (newrelic-before-20220808). In the new system, should sponsor logo changes create a new version (preserving history) or overwrite the current logo? The answer affects how past event pages look.

  2. Team member identity: Currently, team members are just data blobs (name + links) per event. Should they be linked to User accounts? This would enable "see all events this person helped organize" but adds complexity and requires team members to have accounts.

  3. What about events that are still on the Hugo site when we cut over? Events in the 2026-2027 range may have been created in Hugo. Do we migrate them, or require organizers to re-enter their data?


Appendix A: Current Hugo Data Structure Reference

For migration script authors, here's where everything lives in the Hugo repo:

devopsdays-web/
├── config/_default/
│   ├── hugo.yml                    # Site configuration
│   └── menus.en.yml                # Navigation menus
├── content/
│   ├── blog/                       # Blog posts (Markdown + TOML frontmatter)
│   ├── events/
│   │   └── YYYY-city/              # Per-event content
│   │       ├── welcome.md          # Landing page
│   │       ├── location.md         # Venue info
│   │       ├── contact.md          # Contact info
│   │       ├── conduct.md          # Code of conduct (+ translations)
│   │       ├── sponsor.md          # Sponsor page
│   │       ├── propose.md          # CFP page
│   │       ├── registration.md     # Registration page
│   │       ├── schedule.md         # Program page
│   │       ├── speakers.md         # Speakers listing
│   │       ├── speakers/           # Individual speaker pages (optional)
│   │       │   └── name-slug.md
│   │       └── program/            # Individual talk pages (optional)
│   │           └── talk-slug.md
│   ├── page/                       # Static pages (about, organizing, etc.)
│   └── speaking/                   # Speaking content
├── data/
│   ├── core.toml                   # Core team members
│   ├── events/
│   │   └── YYYY/
│   │       └── city/
│   │           └── main.yml        # Event configuration (dates, team, sponsors, program)
│   └── sponsors/
│       └── sponsor-slug.yml        # Global sponsor data (name, url)
├── static/
│   ├── events/
│   │   └── YYYY-city/              # Event assets (logos, images)
│   └── img/
│       └── sponsors/               # Sponsor logos (legacy location)
├── assets/
│   ├── events/
│   │   └── YYYY-city/
│   │       └── organizers/         # Team member headshots
│   └── sponsors/
│       └── A-Z/                    # Sponsor logos (current location, by first letter)
└── utilities/
    ├── add_new_event.sh
    ├── add_speakers.sh
    ├── add_sponsors.sh
    ├── add_organizers.sh
    └── add_program.sh

Appendix B: URL Mapping

Every URL from the Hugo site must continue to work. Key patterns:

Hugo URL Pattern New System Mapping
/events/YYYY-city/ Event welcome page (dynamic or archived)
/events/YYYY-city/speakers/ Speakers listing
/events/YYYY-city/speakers/name-slug/ Individual speaker
/events/YYYY-city/program/talk-slug/ Individual talk
/events/YYYY-city/location/ Location page
/events/YYYY-city/sponsor/ Sponsor page
/events/YYYY-city/contact/ Contact page
/events/YYYY-city/conduct/ Code of conduct
/events/YYYY-city/propose/ CFP page
/events/YYYY-city/registration/ Registration page
/events/YYYY-city/schedule/ Program/schedule page
/blog/YYYY/MM/DD/title/ Blog post
/about/ About page
/organizing/ Organizing guide
/speaking/ Speaking page
/sponsor/ Sponsorship info (global)
/events/ Events listing
/city/ Redirect to current year's event for that city

Appendix C: Glossary

Term Definition
City organizer A volunteer on a local devopsdays organizing team. Has access to manage their city/year's event pages.
Core team The global devopsdays leadership (~12 active + ~4 advisory members) who maintain infrastructure and enforce brand guidelines.
Event A specific devopsdays conference in a specific city in a specific year (e.g., devopsdays Austin 2024).
Ignite talk A 5-minute lightning talk with 20 auto-advancing slides (every 15 seconds). A signature devopsdays format.
Open space Attendee-driven, self-organized discussion sessions. A core (non-optional) component of every devopsdays.
Pretalx Open-source conference management tool used for CFP at talks.devopsdays.org.
Pretix Open-source ticketing platform used at tickets.devopsdays.org.
Sponsor level A tier of sponsorship (e.g., Gold, Silver, Coffee Bar). Defined per-event with custom labels and caps.