Version: 1.1 (Draft — hybrid frontend architecture revision) Date: 2026-04-10 Author: Matty Stratton, with research assistance from Claude Status: Draft for Review
- Executive Summary
- Problem Statement
- Goals and Non-Goals
- User Personas
- Data Model
- System Architecture
- Feature Requirements
- Tech Stack Recommendation
- Migration Strategy
- Infrastructure and Deployment
- Security and Access Control
- Integration Points
- Risks and Mitigations
- Success Metrics
- Open Questions
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.
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:
- Fork the repository (~11GB, including 4.1GB of git history)
- Install Hugo locally (version-sensitive, platform-specific issues common)
- Clone their fork, create a branch
- Edit YAML data files and Markdown content files (following specific naming conventions)
- Run Hugo locally to preview changes
- Commit, push, and open a pull request
- Wait for a core team member to verify their identity and merge
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.
- 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
| 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 |
| 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 |
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."
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."
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.
The data model is derived from 15+ years of the existing Hugo site's content structure. Entity names and relationships are battle-tested.
┌──────────────┐
│ CoreTeam │
│ Member │
└──────┬───────┘
│ manages
▼
┌──────────┐ has many ┌──────────────┐ has many ┌──────────────┐
│ City │◄──────────►│ Event │◄──────────►│ EventPage │
└──────────┘ └──────┬───────┘ └──────────────┘
│
┌────────────────┼────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ TeamMember │ │EventSponsor │ │ProgramEntry │
│ (per-event) │ │ (junction) │ │ │
└──────────────┘ └──────┬───────┘ └──────┬───────┘
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ Sponsor │ │ Talk │
│ (global) │ │ │
└──────────────┘ └──────┬───────┘
│
▼
┌──────────────┐
│ Speaker │
│ (per-event) │
└──────────────┘
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 |
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 | 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)
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)
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) |
| string | Optional | |
| created_at | timestamp | |
| updated_at | timestamp |
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)
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)
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 |
| string | Optional | |
| url | Optional | |
| github | string | Optional |
| website | url | Optional |
| created_at | timestamp |
Unique constraint: (event, slug)
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)
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 |
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 |
| string | Optional | |
| url | Optional | |
| github | string | Optional |
| website | url | Optional |
| url | Optional | |
| sort_order | integer | Display ordering |
| Field | Type | Notes |
|---|---|---|
| id | PK | |
| 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 |
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)
| 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 |
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 |
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 |
┌──────────────────┐ ┌──────────────────┐
│ 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) │
└──────────────┘ └──────────────┘
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)
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.
Django (backend):
- 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.
- 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.
- 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.
- Excellent ecosystem.
django-allauthfor OAuth,django-storagesfor S3,django-imagekitfor image processing — these are mature, maintained packages. - FOSS. BSD licensed.
- 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):
- 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.
- Single deployment. The Vue app is bundled by Vite and served by Django's static files. No separate Node.js server.
- Django handles auth, sessions, CSRF, permissions. All the security plumbing stays server-side where it's battle-tested.
- Shared routing. URLs are defined in Django's
urls.py— no client-side router to keep in sync. - 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):
- Gentler learning curve than React for occasional/volunteer contributors who aren't full-time frontend developers. Single-file components, simpler mental model, less boilerplate.
- Rich component ecosystem. Libraries like VueDraggable (schedule builder), TipTap (rich text editor), HeadlessUI (accessible components) cover the complex interactive needs.
- TypeScript optional. Contributors can write plain JavaScript or TypeScript — Vue supports both without requiring either.
- Strong community. Vue is the second-most-popular frontend framework and has excellent documentation.
Features are organized into three phases. Each phase should be deployable independently and provide standalone value.
The minimum viable product that replaces the Hugo workflow for new events. Existing events can continue on Hugo during this phase.
- 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
- 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)
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
- 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
- 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
- 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
- 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
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)
- 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)
- 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
- 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)
- Sponsors can log in, update their company info and logo
- Submit sponsorship interest for specific events
- View their sponsorship history across events
- Speakers can log in, update their bio and links
- Upload slides and video links post-event
- View their speaking history across devopsdays events
- 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
- 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
- Aggregate view of event data across years (growth, geography)
- Per-event page view analytics (if self-hosted analytics like Plausible or Umami are adopted)
- Public read-only REST API for event data
- Enables community-built tools, widgets, and integrations
- API documentation (OpenAPI/Swagger)
| 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 |
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 |
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. |
| 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 |
| 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). |
| 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 |
| 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 |
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. |
Migration is the hardest part of this project. The approach is phased, running both systems in parallel until the new one is proven.
Goal: Reduce the scope of what needs to be dynamically migrated.
- Choose a cutoff year (suggested: 2020). All events before this year become static archives.
- Generate static HTML for each pre-cutoff event by rendering the Hugo site and capturing the output per-event.
- Store the HTML in a directory structure that Nginx can serve directly:
/archives/events/2019-amsterdam/index.html, etc. - 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.
Goal: Import all post-cutoff event data into the database.
- Write migration scripts (Python) that parse:
data/events/YYYY/city/main.yml→ Event, TeamMember, SponsorLevel, EventSponsor, ProgramEntry recordsdata/sponsors/*.yml→ Sponsor recordscontent/events/YYYY-city/*.md→ EventPage records (extract frontmatter + body)content/events/YYYY-city/speakers/*.md→ Speaker recordscontent/events/YYYY-city/program/*.md→ Talk recordsdata/core.toml→ User records withis_core_team=Truecontent/blog/*.md→ BlogPost recordscontent/page/*.md→ StaticPage records
- Copy media files: Event images, sponsor logos, speaker headshots → media storage (S3 or local)
- Validate the migration by comparing rendered output of the new system against the Hugo output for every migrated event.
- Run the migration script repeatedly during development, treating it as idempotent. Final production run happens at cutover.
Goal: Prove the new system works without risking the live site.
- Deploy the new system at a staging URL (e.g.,
beta.devopsdays.org) - Route new events (e.g., all 2027 events) to the new system while keeping the Hugo site live for existing events
- City organizers for new events use the web UI exclusively
- Core team tests all workflows: event creation, user management, content editing
- Public users see the new system for new events, Hugo site for existing events (Nginx routing)
- Duration: At least one full event cycle (an event goes from creation → CFP → program → live → archived)
Goal: Retire the Hugo site.
- Migrate remaining Hugo events to the database (any events added to Hugo during Phase 2)
- Switch DNS from Netlify to the new infrastructure
- Set up redirects for any URL patterns that changed
- Keep the Hugo repo archived (read-only) as a historical reference
- Monitor intensively for broken links, missing content, and access issues
| 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 |
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.largeif 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)
# 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:8025Requirements: 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
.vuefiles 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()
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)
- Database:
pg_dumpevery 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.
- 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).
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
- 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
- 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.
- 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
- Type: Reference only
- Mechanism: Organizer email (
city@devopsdays.org) is stored as a string field - No programmatic integration with Google Workspace
- Type: Link/reference only
- Mechanism: Webfinger redirect (currently handled by Netlify redirect; move to Nginx)
- Type: Optional future integration
- Mechanism: Slack webhook for notifications (new event created, permissions granted)
- Phase 3 feature
| 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. |
| 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. |
| 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. |
- 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
- 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)
These items need further discussion and decision before or during implementation:
-
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.
-
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. -
Multi-language support: Some events have translated conduct pages (Portuguese, French). Is i18n a Phase 1 requirement, or can it be deferred?
-
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.
-
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? -
Governance for the new system: Who can merge PRs to the webapp codebase? Same CODEOWNERS model as the Hugo site?
-
Domain handling: Does the new system serve
www.devopsdays.orgdirectly, or does it sit behind a reverse proxy at a different domain during transition? -
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?
-
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. -
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.
-
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?
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
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 |
| 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. |