Skip to main content
← Case studies

Case study

Midway Music Hall

Live music, dance nights and lessons, with reserved seats requested from the seating chart.

Midway Music Hall had no website. Shows were found by word of mouth, and reservations came in by phone or in person at a show. The owners kept hearing from neighbors who had never known the venue was there.

I built a public site with a page for every show and a seating chart people can pick seats from, and an admin where the owners and managers run the schedule, repeating nights, lessons, seat requests, card payments, hall rentals, the inbox and the site's pictures. It launched in December 2025 on the venue's cPanel hosting as React apps over a PHP and MySQL API.

In September 2026 I moved it to Vercel, Turso and Cloudflare R2. The same React apps now run on a Next.js server that was held to 333 responses recorded from the PHP build, and 9,596 rows moved across in a cutover of about an hour.

What they wanted

They had no website, and people at shows kept telling them "I live right around the corner and had no idea you exist." They wanted to be found, to manage their own schedule, and to take seat reservations online.

The venue belongs to the same ownership group as several businesses I build for. I built its first website in late 2025 and I still run it.

At a glance

Client
A live music venue in Midway, North Carolina, with reserved table seating, a beach music series, repeating dance nights, lessons and a rental hall.
Launched
December 2025
Moved to the current platform
September 2026
Runs on
Next.js 16 route handlers on Vercel, The same React apps (React 19, Vite, Tailwind), Turso (libSQL), Cloudflare R2 for images and private files, SendGrid for outbound mail, Square hosted checkout, Cloudflare DNS, Vercel Web Analytics and Cloudflare Web Analytics

What was built

The public site

Two pages matter most: the schedule, and the show page with its seating chart. Every page goes through the server, which writes its title, description and show details before it reaches the browser, so search engines read the real schedule.

The schedule

Every upcoming show, a month at a time, marked where reserved seating is open and filterable by it. Closed dates show as a notice of their own.

A page for every show

Each show and each repeating night has its own address, with the date, doors, price, poster and an add-to-calendar file, plus event details search engines can read.

Pick seats on the chart

The seating chart shows the room as staff laid it out, with the stage and every table. Taken and held seats are grayed out, and a visitor taps the seats they want.

Seat requests and card payment

A short form for name, phone and email holds the seats for staff to confirm. On shows where the venue turns it on, the visitor can pay by card through Square.

Repeating nights and lessons

Jam nights and other regular nights are listed once each. The lessons page shows the day, times and instructor, and notes any week a class is skipped.

The Gathering Place

The venue's rental hall has its own page: room facts, a date check against the schedule, an inquiry form, and a rental application filled in and sent from the browser.

Beach series, archive and band pitches

The Carolina Beach Music series a month at a time, an archive of past shows, a form for bands that want to play, and a contact form.

Screenshots show demonstration data on a local copy of the site, set up to show everything the design can do. No customer records appear anywhere in these pages.

Midway Music Hall homepage on a desktop screen with sample shows, shown in full
The homepage from top to bottom: the photo slideshow, what is coming up, First Time Here for new visitors, and contact details with a map, shown with sample shows. Scroll inside the frame for the whole page.
Upcoming Shows page listing sample shows for the month, with reserved seating badges
The schedule a month at a time, with reserved seating marked, a filter for it, and a closed date shown as a notice of its own.
Show page for a sample band with a poster, show details and a Request seats button
A show's own page: poster, date, doors, one price, and buttons to request seats or call the box office.
Seating chart with tables around a stage, some seats grayed out and two seats selected
The seating chart with two seats chosen. Seats already reserved or held are grayed out, and the stage is drawn where it sits in the room.
Seat request form with a sample name, email and phone number and two chosen seats listed
The seat request form filled in with sample details, before it is sent. The phone number is required so staff can confirm.
The Gathering Place rental page with room details, a schedule and an inquiry form, shown in full
The Gathering Place, the venue's rental hall: the room, a date check against the schedule, the inquiry form and the rental application. Scroll inside the frame for the whole page.
Lessons page listing sample dance classes with an instructor and a skipped-week note
Dance lessons with their day, times and instructor, and a note on a week a class is skipped.
Midway Music Hall homepage on a phone with an announcement banner
The homepage on a phone, with a sample announcement banner across the top.
Upcoming Shows page on a phone with stacked sample show cards
The schedule on a phone, one show card after another.
Seating chart filling a phone screen with one seat selected
The seating chart on a phone, opened full screen with a seat chosen.

PageSpeed Insights

Median of at least 3 runs per device, 2026-10-06. Run it live ↗

Desktop

Performance
99 out of 100, Good
Accessibility
96 out of 100, Good
Best practices
100 out of 100, Good
SEO
100 out of 100, Good

LCP 871 ms · CLS 0 · TBT 15 ms

Phone

Performance
81 out of 100, Needs work
Accessibility
96 out of 100, Good
Best practices
100 out of 100, Good
SEO
100 out of 100, Good

LCP 4.3 s · CLS 0 · TBT 0 ms

What the owner runs

Behind the login

One admin for the owners, managers and box office. Each account has a role, and each person can be limited to the sections they need. The server checks both on every request.

Schedule and shows

Shows by month, each with its category, price, doors, poster, seating layout and online cutoff. Closed dates are their own entry, and played shows are put away each night.

Repeating nights

Weekly and monthly nights keep a year of dates ahead on their own. Staff can skip a date, or put a night away and bring it back.

Seat requests on the seating chart

Approve, deny, extend a hold or move seats on the map. Requests show whether the confirmation email bounced, and a roster of regulars can be seated in one action.

Square payments

Card payment is switched on by category, so it runs on only some shows. Staff can also record money taken at the door, and a report lists every Square sale.

Hall rentals

Inquiries and completed rental applications for The Gathering Place, with the application PDFs kept in private storage.

Lessons

Classes with their instructor, first class and skipped weeks. The least used module, and the one an instructor's own account can be narrowed to.

Staff inbox

Contact messages, band pitches and rental inquiries in one list, with statuses, notes and replies sent from inside the admin.

Media

Posters and photos with framing for each place they appear, and a check of what a file is used for before it can be deleted.

Staff accounts

Owner, manager and box office roles, sections chosen per person, accounts turned off rather than deleted, and password reset links.

Who did what

The audit log, written as a readable story of who changed what and when.

System checks

Checks for real problems: the nightly jobs ran, repeating dates are filled ahead, no night is listed twice, and every show taking money has a price.

Admin Events list showing a month of sample shows
The schedule in the admin, by month, with sample shows, a repeating night and a closed date.
Admin show editor for a sample show with its sections listed in a side rail
The show editor open on a sample show: category, one price, doors, poster, seating layout and online cutoff, with a rail of sections down the side.
Admin Seat Requests list with sample requests in several statuses
Open seat requests across sample shows, each with its seats and hold. Confirmed, denied and paid-at-the-door requests sit behind the status filter.
Admin Inbox listing sample messages, band pitches and rental inquiries
The staff inbox: sample contact messages, band pitches and rental inquiries in one list, with statuses and a note.
Admin Media Manager showing a grid of sample images
The media manager with sample posters and photos, each with its framing and the places it is used.
Admin Accounts list with sample staff in three roles
Staff accounts with owner, manager and box office roles, an instructor narrowed to Lessons, and one account turned off.
Admin Who did what page listing recent sample changes in plain sentences
Who did what: the audit log told as a story of the sample changes made while setting up these screens.
Admin Seat Requests on a phone with each sample request as a card
Seat requests on a phone, shown as cards so the box office can work them from the floor.

What it runs on

Two platforms, one site

The first version ran on shared cPanel hosting. The site now runs on the current platform, with the move recorded in the write-up.

Version 1, December 2025

Hosting
GoDaddy cPanel shared hosting, Apache and PHP 8.3
Database
MySQL (MariaDB 10.11) on the same host
Deploys
A staged ZIP built locally, checked, then uploaded
Images
Cloudflare R2 from May 2026
Email
SendGrid for outbound mail
DNS
Cloudflare, proxied to the cPanel host

Today, since September 2026

Hosting
Vercel, deployed on every merge to main
Database
Turso (libSQL), with a separate preview database
Images and files
Cloudflare R2, public images on a CDN subdomain and rental files in a private bucket
Payments
Square hosted checkout, a sandbox on previews
Outbound email
SendGrid, send-only key, tagged by site, no tracking
Mailboxes
Mail records unchanged by the move; staff read mail in their own inboxes
Nightly jobs
Vercel cron: retention, putting played shows away, filling repeating dates
DNS
Cloudflare, DNS-only to Vercel
Analytics
Vercel Web Analytics and Cloudflare Web Analytics, public pages only
Way back
The PHP build and MySQL left in place on cPanel

What it does now

Reservations now come in online, and the staff say every part of running the venue is easier.

  • •The owners and managers use the admin for everything, from the schedule to the inbox. Lessons is the part they use least.
  • •Many older patrons still reserve by phone or in person at shows, and staff handle those alongside the online requests.
  • •Online seat reservation with card payment works, and the venue turns payment on for some shows.
  • •Maintenance is mostly hands-off. I step in when something breaks or needs to change.

Next

  • A Google Calendar link, so people can follow the schedule from their own calendar as well as the site.

“People tell me they live right around the corner and had no idea we existed. Now they find the schedule, pick a table and pay before they get here. Running the place got easier.”

Owner, Midway Music Hall

The venue and what it needed

Midway Music Hall is a live music venue in Midway, North Carolina. It puts on shows with reserved table seating, runs a Carolina Beach Music series, hosts repeating nights such as jam nights, teaches dance lessons, and rents out a separate hall called The Gathering Place for private events. The venue belongs to the same ownership group as several other businesses I build for, including Midway Mobile Storage and Thunder Road Bar and Grill.

When I started, the venue had no website at all. The owners kept hearing the same line from people who came to a show for the first time: "I live right around the corner and had no idea you exist." They hear it less now.

What they wanted came in three parts. People nearby had to be able to find the venue and see what was on. The staff had to be able to manage the schedule themselves, show by show, without calling anyone. And the venue wanted to take seat reservations online, against the real tables in the room, so a reservation made at midnight would be waiting for staff in the morning.

What was there before

There was no site. People learned about shows from the venue itself and from each other. Reservations came in two ways: a phone call to the box office, or a word with staff in person at a show.

That way of working has not gone away, and it should not. Many of the venue's patrons are older and prefer to reserve by phone or at the door. The system had to make online reservations possible without making the phone and the door second-class. Staff see every request in one place, however it arrived.

Version 1: the cPanel build

The Node start

The repository began on October 22, 2025. The first version was a React app, built with Create React App, over a small Node and Express API talking to MySQL. The very first commit already had the core of what the venue needed: seat requests, a seating chart, a history of layout changes, an admin panel and a form for band suggestions.

Through November the public side took shape. On November 27 a single-page version of the site went together with a section for the 2026 Beach Bands series, along with a guide for deploying on GoDaddy behind Cloudflare. On November 30 and December 1 the seating work landed: a visual layout editor where staff place tables of different shapes, position the stage and size it, plus a media manager and a slideshow at the top of the homepage.

The switch to PHP

The venue's hosting was a GoDaddy cPanel account. That kind of hosting is built to run PHP and MySQL, and a long-running Node process on it is the odd one out. In mid-December I rewrote the API in PHP. The PHP API was converted on December 12, and on December 16 I made it the only backend and removed the Node one. The React app stayed the same throughout, since it only ever talked to the API.

The site launched in December 2025. In the same weeks the admin gained an audit log (December 14), and seat requests gained database locking and transactions (December 17), so two people asking for the same seat at the same moment could not both get it.

Why a schedule system and not a calendar

The obvious shortcut was a Google Calendar embedded in a page. I built a schedule system instead, because a show at this venue is much more than a calendar entry. A show has a category, a price, a door time, a poster, a seating layout, a cutoff after which online requests close, and a switch for card payment. Seat requests belong to a specific show and a specific set of seats on that show's layout. Repeating nights need a year of dates ahead, with the option to skip one. Closed dates need to show as closures. None of that fits in a calendar event, and a calendar cannot hold a reservation against a table.

A calendar is still useful for patrons who want the schedule on their own phone, so a Google Calendar link is planned as an extra way to follow it. The schedule itself stays in the system.

Growing through 2026

The cPanel build grew steadily through 2026, driven by what the staff ran into:

  • •February 5: seat request emails, and PayPal as a first payment option.
  • •March 3: an announcement banner and popup the staff control, and signing in by email as well as username.
  • •March 15 to 18: tiered pricing, multi-day events and door times for each date.
  • •March 28 to April 14: workflows for recurring series.
  • •April 3: Square checkout for seat requests.
  • •April 6: branded error pages and a maintenance mode.
  • •May 22 to 24: the public site and the admin split into two separate apps, and Create React App replaced with Vite.
  • •May 25: admin system checks.
  • •May 27: images moved to Cloudflare R2.
  • •June 7: an optional online payment fee.
  • •June 18 to 20: the rental workflow for The Gathering Place.
  • •July 19: PayPal removed, leaving Square as the only provider (finished in PR #22 on August 8).

Until August 2026 I committed straight to the main branch. From August 3 every change went through a pull request with checks, and the August work shows what that made possible. PR #16 added three roles: owner, manager and box office. PR #21 added a ledger of payment webhooks and a payment report. PR #40 fixed show times in the admin, which had been read in the browser's time zone, so a 6 PM show could read 2 PM on a machine set to UTC; the same PR put the frontend tests into CI, where they had never run. PR #43 let the venue set a time after which online requests close, and PR #44 added a roster of regulars. PR #55 fixed a test that built "already past the cutoff" in a way that, on a Saturday, meant today, so CI failed every Saturday morning.

At the end of the cPanel era the site ran on PHP 8.3 on shared GoDaddy hosting, with MariaDB 10.11, Apache routing, Cloudflare in front, images on R2, mail through SendGrid and payments through Square. A deploy was a staged ZIP, built and checked on my machine, then uploaded.

Why the site moved, and what changed

By September 2026 every site in the ownership group was moving to the same footing: Vercel for hosting, Turso for the database and Cloudflare R2 for files. That brings deploys on every merge, preview copies with their own data, and no shared server of mine to patch. Midway Music Hall was the fourth to move, after Bow Wow's Dog Spa on September 13, Midway Mobile Storage on September 15 and Thunder Road on September 16, and it had live reservations and live payments in it.

A port held to a contract

I did not want a rewrite that behaved "about the same". On September 16 I built a harness that drove the PHP build through real sessions as a visitor, a box office account, a manager and an owner, and recorded every request and response (PR #82). That gave 333 recorded steps, plus 574 cases checking that the small helpers produced the same results in both languages. The new Next.js server had to replay all of them. Where I wanted the new build to behave differently, the difference became a file with a written reason. A recording is never re-recorded just to make a test pass.

The same day I ported the public API (PR #83), the admin API (PR #84) and set up the platform with a rehearsed data move (PR #85). The React apps came across unchanged.

Recording the old build closely turned up 28 bugs in production, and I wrote each one down. A few of them:

  • •The public list of events included promoters' phone numbers.
  • •There was no rule for password length.
  • •Every "Add to calendar" link returned a 404.
  • •The calendar files mangled every letter n.

18 questions about which behavior to keep were answered and recorded that day, and the port followed the answers.

Four clocks down to two

Time was the subtlest problem. MariaDB on the host wrote its own timestamps in Mountain time. PHP wrote venue time on some paths and UTC on others. Between them, seat holds lapsed about three hours late, and "this month" in the admin was wrong for three hours at every month's end.

The new database has two kinds of time and no others. A moment in time is stored in UTC and marked with a Z. A time on the venue's own clock, such as a show's start, is stored without an offset. The database's own CHECK rules refuse anything else. The server tests run under four different time zones, so a test that only passes in Eastern time fails in CI.

Locks without row locks

The PHP build protected seats with 15 places that locked database rows while checking them. libSQL, the database behind Turso, has no row locks. Each of those places became a write transaction that takes the database's write lock before its first read. A second visitor trying at the same moment waits, retries, and gets the normal "those seats are taken" answer instead of an error.

Square calls can take many seconds, and nothing should hold the write lock that long. The step that starts a card payment is split around two short transactions, with the call to Square in between.

The bug that ended other people's bookings

Days after the move, PR #158 fixed a serious bug that came with the new database client. Version 0.18 of the libSQL client changed its reconnect step to close every open transaction on the connection. When one visitor's request hit lock contention and reconnected, it could tear down another visitor's booking in the middle. The fix reconnects only when no transaction is in flight. To prove the guard mattered, I removed it and watched five tests fail.

Moving the data

The export ran inside one read-only snapshot over a utf8mb4 connection, because five tables were stored as Latin-1 while holding Windows punctuation such as curly quotes. I rehearsed the whole move on a throwaway Turso database first. Two bad rows needed named corrections: a price of minus one cent, and a date in the year 0026.

The cutover ran on September 17, from about 1:10 to 2:10 in the morning Eastern. I took a backup, put up the maintenance page, exported, loaded 9,596 rows and checked them against the export, moved DNS, and ran 22 checks against the live server. I then compared the old and new databases twice, at the end of the window and 40 minutes later, and both comparisons came back empty. The preview database was refreshed from the new production copy.

What the new footing made possible

A few of the changes made on the new footing stand out.

  • •The stale-tab notice (PR #215, September 24). Staff reported the admin "breaking" on their phones. It was a deploy replacing the app's files under a tab that had been open for hours. The admin now checks for a newer version, tells the person, and offers a Reload button. It never reloads for them, and a failed file load is worded differently from a real error. This became the standard for every app I run.
  • •One price per show (PR #230, September 25). Shows had separate advance and door prices. When I measured, no show charged two different prices; every apparent difference was one box left empty. Measuring also found a money bug: an advance price of exactly zero beside a real door price was offered for card payment at $0.00. It affected three shows, none of which had payment turned on. Each show now has one price, asked once.
  • •Nights listed twice. The first date of a repeating night was itself a dated show, so a jam night could appear twice on the schedule. I fixed the cases that existed, and a system check named nights_listed_twice now catches it by title and date.
  • •Holds that match how staff work (PR #187). Staff had been extending open holds by hand. When I measured, 94.6 percent of 1,215 confirmed requests had settled within seven days, so a new request now holds its seats for a week, never past the show, and staff are warned a day before a hold lapses (PR #188).

The public site, page by page

The public site is a React app. Every page goes through the Next.js server first, which writes the page title, description, sharing tags and structured data, and since PRs #255 and #266 the real schedule and show content too, so search engines read the actual listings.

Homepage

The homepage is written for someone who has never been (PR #131). It opens with a slideshow of photos, a short introduction, and a spotlight on what is coming up. A "First Time Here" section answers the questions a newcomer has, with a strip showing how payment works at the venue. A banner or popup the staff control can announce anything they need to, and visitors can dismiss it. The page ends with contact details and a map.

Schedule

Every upcoming show, a whole month at a time (PR #239). Shows with reserved seating are marked, and a filter shows only those (PR #149). A date the venue is closed shows as a notice of its own instead of a show card (PR #135). Each card links to the show's page.

Show pages

Every show and every repeating night has its own address (PR #130). The page shows the poster, date, doors and price, who can come, a Request seats button when seating is open, and a button to call the box office. Published upcoming shows offer an add-to-calendar file. The server writes structured data describing the event, so a search engine can show it as an event.

The seating chart

The seating chart opens on the seat step and uses the whole screen (PR #178). It draws the room from the layout version the show uses, with the stage and every table. Seats already reserved or held are grayed out. The visitor taps the seats they want, and the chart keeps a running count.

Once a show's cutoff passes, online requests close, and the chart says so and points people to the box office. The cutoff is set per weekday and can be changed for a single show.

The reservation form

After choosing seats the visitor gives a name, a phone number and an email address. The phone number is required so staff can confirm. The email is where the confirmation goes, and the form says so. There is room for a note.

On submit, the server checks the seats and records the request in one write transaction, so two people cannot get the same seat. The seats are held for staff to confirm. If card payment is on for that show's category and Square is set up, the visitor can pay right away or later from the link in their confirmation email. One Square payment covers the whole request, up to six seats.

Repeating nights, lessons and the beach series

The repeating nights page lists each regular night once instead of every date (PR #132). The lessons page shows each class's day and times, taken from its first class, and its instructor, and notes any week a class is skipped (PRs #274, #275, #278). The Carolina Beach Music series has its own page, a month at a time.

The Gathering Place

The rental hall has a page of its own. It covers the room, checks a date against the schedule, and offers a short inquiry form. When the venue turns it on, the page also offers the rental packet and a rental application that is filled in right in the browser and sent as a PDF to private storage (PR #113).

Everything else

An archive of past shows, a form for bands that want to play, a contact form, privacy and terms pages that describe what the site actually collects, a payment return page that asks the server whether a payment went through, branded error pages, and a sitemap built from the schedule. Old addresses for closed dates answer that the page is gone for good, so search engines drop them.

What the owner runs

The admin is a second React app. The owners and managers use it for everything; lessons is the part they use least. The box office uses the parts its role allows.

Roles and sections

There are three roles: owner, manager and box office. On top of the role, each account can be narrowed to the sections that person needs. An instructor, for example, is a box office account narrowed to Lessons (PRs #205 to #207). The server enforces both the role and the sections on every request, so hiding a menu item is never the only protection. Accounts are turned off instead of deleted, so history still reads correctly (PR #95), and anyone can ask for a password reset link (PR #98).

Front of house

  • •The dashboard shows what needs doing today and what is on this week (PR #104).
  • •Seat requests can be approved, denied, extended, held until the show or moved to other seats on the map (PR #139). Staff can record money taken at the door and how it was paid (PRs #196, #209). A request whose confirmation email bounced is marked in red (PR #204).
  • •Regulars is a roster of people who come often, with their usual seats for each layout (PR #105). It suggests regulars from past bookings (PR #106), can follow a particular act instead of every show (PRs #159, #160), and lets staff seat a regular before they have paid (PR #240).

The schedule

  • •Events is the schedule by month. The show editor covers category, price, doors, layout, the picture and its framing, recurrence, and snapshots of the seating that can be restored. A closed date is its own kind of entry (PR #137), and played shows are put away each night.
  • •Recurring Nights and Lessons have their own sections, with series dates, skipped dates, and a way to put a series away and bring it back (PRs #244, #245).
  • •Seating Layouts is the visual editor for the room: tables of many shapes, the stage, undo and redo, and arrow keys (PR #119). A new version is recorded only when the map actually changes (PR #120). It refuses to delete a layout a show uses and offers to move those shows (PR #126). It is meant for a tablet or larger and says so on a phone.
  • •Event Categories hold the defaults a new show starts with: layout, door time, price, description, inbox address and whether card payment is on.

Inbox, website and payments

  • •The inbox holds contact messages, band pitches and rental inquiries in one list (PR #108), with statuses and notes (PR #110). Staff can reply from inside the admin, and the reply is recorded (PR #273). Rentals lists completed applications above inquiries. The Email screen shows every address the site sends to (PR #272).
  • •Site Content edits the banner and homepage text. The media manager handles posters and photos with framing for each place a picture appears, and asks what a file is used for before it can be deleted (PR #123).
  • •The payment report lists Square sales and separates online payments from sales on the venue's own terminal (PR #88). Payment settings lead with whether online payment works at all, in plain words (PR #183).

Maintenance

System Checks confirms the stores are reachable and the email setup is complete, and runs checks for real problems: the nightly jobs ran, repeating dates are filled a year ahead, lessons are on the website, no night is listed twice, every show taking money has a price, and upcoming shows have pictures. Who Did What tells the audit log as a story instead of a table of codes.

The whole admin works at phone width, with a menu drawer and tables shown as cards (PRs #192 to #194).

Infrastructure

  • •Domain and DNS: a Cloudflare zone. The bare domain points at Vercel and www redirects to it. The CDN subdomain for images and the mail records were left as they were.
  • •Hosting: a Vercel project in the JAMARQ Digital team. A merge to main deploys production. Builds are skipped for changes outside the deployable paths.
  • •Database: Turso, with separate production and preview databases. The schema has 38 tables with strict types, money kept in whole cents, and the two kinds of time described above. Each schema change is a dated SQL file that I run by hand on preview and production. Nothing changes the database on deploy.
  • •Files: two Cloudflare R2 buckets. Public images sit behind a CDN subdomain, and rental applications and uploads in progress sit in a private bucket. Files too large for a normal request go straight from the browser to R2 through a signed link that expires in an hour. Images are resized on the server with sharp.
  • •Email: SendGrid, on one shared account. Every message carries the site's tag, a plain-text part and no tracking, and the key can only send. Bounces on seat request confirmations come back through a signed webhook. Server errors are emailed as alerts (PR #271).
  • •Payments: Square hosted checkout, confirmed by a signed webhook. Previews use Square's sandbox.
  • •Nightly jobs: three Vercel cron jobs in the early morning. One clears old audit entries, sessions and throttle rows. One puts played shows away. One fills repeating nights and lessons a year ahead.
  • •Previews: every preview has its own database, its own storage buckets, its own mail key with mail off, and Square's sandbox.
  • •Analytics: Vercel Web Analytics and Cloudflare Web Analytics, on public pages only. The admin and payment pages are not counted.
  • •Security: a session cookie with idle and absolute timeouts, a check on the origin of every admin change, sign-in lockout after repeated failures, salted hashes in place of visitor addresses, hidden trap fields on the public forms, and limits on rental submissions.
  • •CI: GitHub Actions on my self-hosted runners, running the contract replay, the server tests and the old PHP suite.

Decisions worth explaining

Staff confirm every request, paid or not

A paid request does not confirm itself. The venue seats people by judgment as well as by the chart, and regulars have their usual seats. Payment marks a request as paid, and a person still says yes. The same holds for requests that come in by phone and at the door.

Card payment by category

Online payment is switched on one category of shows at a time (PR #189). The venue takes card payment online on some shows and leaves the rest to the box office and the door, where most of its card sales still happen on its own Square terminal. The settings page says plainly whether online payment can work at all before it offers any switch.

Holding a card at the moment of booking was planned as a second stage and shelved on September 23. The current flow holds the seats first and takes payment after.

One way to classify a show

Shows used to carry free-form tags as well as a category. Counting by tags gave confident wrong answers, such as only three beach shows when there were eighteen. The tags column is gone, and the category is the only classification (PRs #224, #238).

The PHP build stays

The PHP code and its MySQL schema stay in the repository as the reference the new server is tested against, and CI still runs the PHP test suite against MariaDB. The cPanel deploy and its database are still in place, with a backup taken before the cutover, as the way back if it were ever needed.

Local runs cannot touch production

A local run of the site writes files to the local disk even if storage keys happen to be in the environment. Files go to R2 only from production or from Vercel. Mail sends only when the app is production, sending is switched on, and a key is set. A developer machine can exercise every feature without a file reaching the live bucket or an email reaching a patron.

Alerts that actually alert

The documentation promised that server errors would be emailed, and the code only logged them. PR #271 made the code match the promise. The same habit led to PR #253, where a band pitch now records whether its notification email went out, so a lost message is visible in the inbox.

What it does now

The site is live at midwaymusichall.net. People find the venue, see the schedule, pick seats on the chart and send their requests at any hour. Reservations now come in online, and the staff say every part of running the venue is easier.

Day to day:

  • •The owners and managers use the admin for the whole operation: the schedule, repeating nights, seat requests, regulars, rentals, the inbox and the site's pictures. Lessons is the module they use least.
  • •Many older patrons still reserve by phone or in person at shows. Staff work those alongside the online requests, on the same seating chart.
  • •Online seat reservation with card payment works. Payment is on for some shows, and the rest are paid at the venue.
  • •Hall rental inquiries and applications arrive through The Gathering Place page and land in the admin.

Ongoing support

Maintenance is mostly hands-off unless something breaks.

  • •Dependabot opens dependency updates. Minor and patch updates merge on their own once every check passes, and larger ones wait for me.
  • •A weekly scheduled security sweep checks the dependencies. The security gate blocks on real advisories, with one written exception for a build tool that expires on its own date (PR #267).
  • •Every change runs the contract replay, the server tests and the PHP suite before it can merge.
  • •Schema changes go to the preview database first, then production, by hand.
  • •The next addition is a Google Calendar link, so patrons can follow the schedule from their own calendar as well as the site. The schedule system stays the place where shows are made.

Services this shows

Want something like this for your business?

Send the current site, the problem, and what should become easier. You will hear back within one business day.