Pickleball at Sea
Pickleball at Sea is a small business that organizes doubles and singles pickleball tournaments on cruise ships and at resort properties, and the site I wrote for it handles player registration, bracket generation, scoring, photo galleries, and the event calendar. The organization runs two tournaments a year with roughly twenty-four teams in each, and the site replaces a workflow that used to rely on a stack of spreadsheets and a handful of shared email accounts.
The business
Pickleball at Sea organizes both doubles and singles tournaments, and its two currently scheduled events are an Eastern Caribbean cruise aboard the Norwegian Luna in October of 2026 and a week at Sandals Royal Curaçao in May of 2027, for which twenty rooms have been secured. Each event hosts twenty-four doubles teams, twenty-four singles players, and roughly a dozen alternates and spectators who travel with them.
The site itself handles event information, player registration, photo galleries, and a hall of fame. Actual travel bookings are handled through TravelTamers as a Nexion Travel Group affiliate, which keeps the tournament software focused on the tournament and leaves the commerce where the commerce already worked.
Architecture
The application is a straightforward Express 5 server with EJS templates rendered on the server, and it uses SQLite in WAL mode with foreign-key enforcement turned on. I chose SQLite over Postgres here because the dataset is small, the write load is negligible, and a single file backed up nightly by the same Docker volume as the rest of the container is simpler to reason about than a network service would be. The session store is SQLite-backed for the same reason.
Project Structure server.js Express app entry point entrypoint.sh Docker: auto-seeds DB + bootstraps admin data/database.js SQLite schema — 10 tables data/seed.js Event + FAQ seeder (INSERT OR REPLACE) lib/email.js Google OAuth → SMTP fallback → console middleware/auth.js Player auth (requireLogin, loadPlayer) middleware/rateLimit.js In-memory rate limiter routes/ 12 Express routers views/ EJS templates layouts/ main.ejs (public), admin.ejs (admin panel) partials/ navbar, footer
Features
- Player Registration — bcrypt-hashed credentials, duplicate prevention, capacity race conditions handled in transactions. Players register interest, manage profiles, and track event status.
- Admin Panel — Full admin interface for managing events, players, brackets, and content. Auto-bootstrapped admin user on first Docker run via entrypoint script.
- Email System — Nodemailer with Google OAuth service account for reliable delivery. Falls back to SMTP, then to console logging. Confirmation emails, event updates, registration notifications.
- Security — Timing-safe CSRF via crypto.timingSafeEqual. Rate limiting: 60/min global, 5/15min on auth endpoints. HSTS, X-Frame-Options, Content-Type-Options, Permissions-Policy headers.
Deployment
Docker container behind Traefik on the same VPS that runs the Travel Tamers ecosystem. Auto-SSL via Let's Encrypt, health check endpoint at /health, persistent SQLite volume. The entrypoint script handles database seeding and admin bootstrapping — deploy once, everything initializes automatically.
"24 teams. One ship. Zero spreadsheets. Everything from registration to brackets to post-tournament photos runs through the same codebase."