The Team

Petty Theft Row is a men's slowpitch softball team in New York City. We've been playing in rec leagues around the city since spring 2023.

Tech Stack

I'm Andrew. The site started as a roster and a schedule, and crept outward from there.

It's Elixir and Phoenix LiveView, on SQLite. I picked Elixir because I like writing it. The pattern matching and pipes click for me, and you end up with small functions that chain together without feeling over-engineered. OTP is laughable firepower for a softball site, but it earns its keep on the scheduled jobs that pull our league schedule from SportNinja and write up the weekly news roundup.

LiveView does most of the heavy lifting. When someone enters a score or RSVPs for a game, every open browser sees it. No websocket code, no client state to manage. The dithered photos, ASCII weather sparklines, and drag-and-drop field positions are small JS hooks bolted onto LiveView mounts.

SQLite because we don't have write contention, the whole database fits in a pocket, and backups are a nightly file copy to S3. Postgres would be infrastructure for its own sake.

The frontend is a strict monospace grid in plain CSS. Character widths, fixed line heights, no border radius. I forked Iosevka into a custom font so the box-drawing characters line up at every size. Tailwind handles the utilities. I'm not a designer, but with one font and a tight grid there aren't many decisions left to make.

Deploys are blue-green. GitHub Actions builds a mix release, rsyncs the tarball to a single Debian VPS, and Ansible swaps Traefik to the new slot once health checks pass. OpenTelemetry ships spans out to Honeycomb so I can see what the BEAM is doing when something goes sideways.

Operations

deploy (on push to main)
│
├─ 1. GitHub Actions CI
│     compile, assets, mix release
│
├─ 2. rsync tarball to VPS
│
├─ 3. Ansible playbook
│     ├─ stop inactive slot
│     ├─ extract release
│     ├─ run migrations
│     └─ start new slot
│
├─ 4. health check GET /healthz
│
├─ 5. swap Traefik route
│     update dynamic YAML config
│
└─ 6. stop old slot
      done ── zero downtime

db backups (daily 06:00 UTC)
│
├─ 1. sqlite3 .backup on VPS
├─ 2. gzip + stream to S3
└─ 3. prune backups, keep 30

Architecture

       ┌────────────┐
       │  Browsers  │
       └─────┬──────┘
             │ HTTPS
             ▼
┌────────────────────────────┐
│  VPS (Debian)              │
│                            │
│  ┌──────────────────────┐  │
│  │  Traefik :443        │  │
│  │  Let's Encrypt TLS   │  │
│  └──────────┬───────────┘  │
│             │              │
│  ┌──────────┴───────────┐  │
│  │  blue/green deploy   │  │
│  └────┬────────────┬────┘  │
│       │            │       │
│  ┌────┴─────┐ ┌────┴────┐  │
│  │  :4001   │ │  :4002  │  │
│  │  blue    │ │  green  │  │
│  │  Phx+LV  │ │  Phx+LV │  │
│  └────┬─────┘ └────┬────┘  │
│       └──────┬─────┘       │
│              │             │
│  ┌───────────┴──────────┐  │
│  │  SQLite              │  │
│  │  ecto_sqlite3        │  │
│  └──────────────────────┘  │
│                            │
└─────────────┬──────────────┘
              │
    ┌─────────┼─────────┐
    ▼         ▼         ▼
┌────────┐┌───────┐┌────────┐
│  S3    ││ Brevo ││ Open-  │
│  media ││ email ││ Meteo  │
│        ││ + SMS ││ wx API │
└────────┘└───────┘└────────┘