Skip to main content
← Case studies

Case study

Bow Wow's Dog Spa

Comfort-first dog grooming for Winston-Salem and the Triad.

Bow Wow's Dog Spa had a Facebook page and nothing else. I built the shop a full website with an appointment request flow, a photo gallery, a services list, a catalog of what's on the shelves, and an admin the owners run themselves.

The first version ran on PHP and MySQL on cPanel and went to production in August 2026 behind a holding page. In September 2026 I moved it to Next.js on Vercel with a Turso database, Cloudflare R2 for photos and paperwork, and SendGrid for notifications. Every recorded behavior of the old system was replayed against the new one before the move.

The site is live at its domain. The owners are still adding their photos, services and prices, so the public sees a holding page with the shop's contact details until they switch the full site on from the admin.

What they wanted

The owners of Midway Music Hall had only a Facebook page for their grooming shop and needed a real website where customers could see the services, look at the dogs and ask for an appointment.

Bow Wow's has the same owners as Midway Music Hall. I already build and look after the group's websites, so the shop's site was built the same way.

At a glance

Client
A dog grooming spa serving Winston-Salem and the Triad in North Carolina.
Launched
August 2026
Moved to the current platform
September 2026
Runs on
Next.js 16 on Vercel, The same React public site and admin, Turso (libSQL) database, Cloudflare R2 for photos and paperwork, SendGrid for notifications, Vercel Web Analytics

What was built

The public site

One homepage made of sections the owners can reorder, hide or give their own photo, with short addresses like /services and /booking that open the page at the right place.

Appointment requests

Customers pick services and the number of dogs, choose a date and an open time, and add their details, their vet and any vaccination paperwork. The time is held while they finish, and the shop confirms or declines the request.

Services and starting prices

Each service shows a short summary, how long it takes, a starting price and a note on breed or weight. Prices can be hidden while the owners settle them.

Gallery

A photo wall of groomed dogs, before-and-after pairs, the shop and the boutique, with each photo opening full size.

In the shop

A browsable catalog of what the shop sells, in categories, with optional prices and stock labels. There is no checkout; customers buy in person.

Reviews, FAQ and policies

Featured customer quotes with a link to the shop's Google reviews, a full FAQ and the shop's policies, all written in one voice.

Contact form

Messages are stored in the admin and send the shop an email notification. Hidden spam traps and rate limits keep out automated junk.

Holding page

One switch replaces the whole site with a page carrying the shop's phone, email, hours and a staff sign-in link. While it is on, the site's data stays private too.

Staff preview

Signed-in staff can open the real site behind the holding page and turn sections on and off one at a time to see how it will look.

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.

Bow Wow's Dog Spa holding page with the logo, a short message, call and email buttons, hours and address
The holding page the public sees today: the new logo, a call button with the shop's phone number, an email button, the hours and the address.
Bow Wow's Dog Spa homepage on a desktop screen, shown in full with sample content
The full homepage behind the holding page, from top to bottom, shown with sample services, photos, products and reviews. Scroll inside the frame for the whole page.
Services and starting prices section listing sample grooming services
Services and starting prices, with how long each visit takes and a note on breed or weight. Shown with sample services and placeholder prices.
Request an appointment form at step one with a sample service selected and a dog count control
Step one of an appointment request: pick the services and the number of dogs, before any time is chosen or anything is sent. Shown with sample services.
Gallery section with a grid of sample dog photos and a before-and-after pair
The photo wall, with a before-and-after pair beside single photos, each opening full size. Shown with sample photos.
In the shop section listing sample products in categories
In the shop: a catalog of what is on the shelves, by category, with no cart or checkout. Shown with sample products.
Contact form filled in with a sample name, email, phone number and message
The contact form filled in with sample details, before it is sent.
Bow Wow's Dog Spa homepage hero on a phone
The homepage on a phone, opening on the request-an-appointment button.
Request an appointment form on a phone showing sample services to choose from
The same appointment request on a phone, with the services as large tap targets.

PageSpeed Insights

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

Desktop

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

LCP 494 ms · CLS 0 · TBT 0 ms

Phone

Performance
95 out of 100, Good
Accessibility
100 out of 100, Good
Best practices
100 out of 100, Good
SEO
100 out of 100, Good

LCP 2.4 s · CLS 0 · TBT 0 ms

What the owner runs

Behind the login

A dark admin in the shop's colors, grouped by job: bookings, website, inbox and maintenance. Owners and staff each see only the parts their role allows.

Booking requests and schedule

A queue of requests by status, with confirm, decline and edit, a preview of the email the customer will get, manual bookings for phone calls, attachments and CSV export. The schedule sets weekday times, closed dates, how long a held time lasts, and a switch to pause online booking.

Text and sections

Business details, hours and links, the copy of every homepage section, which sections show, their order and their photos, the legal pages, and the holding-page switch.

Photo library

Upload a photo once and use it anywhere. Each photo shows where it is used, can be placed on the site from its card, and has a focal point so every crop keeps the dog in frame.

Gallery

Choose which photos appear on the photo wall and in the before-and-after areas, and in what order.

Services

Services with durations, price labels and breed or weight notes, ordered by dragging and hidden when not offered.

Reviews

The customer quotes featured on the site, with their star ratings.

Shop catalog

Categories and items with optional prices and stock labels. It is a catalog only, with no cart or payments.

Contact inbox

Every message from the contact form, marked handled once someone has answered it.

Activity history

An audit log of who changed what, and when.

Four roles

Super admin, manager, scheduler and content editor. A scheduler sees only bookings; a content editor sees only the website. Only a super admin can manage accounts, and nobody can lock everyone out.

Admin booking requests queue with sample requests in several statuses
Booking requests by status, with the filters and counts at the top and confirm, decline and edit on each request. Shown with sample requests. Scroll inside the frame for the whole page.
Admin booking schedule with weekday appointment times and a pause online booking switch
The booking schedule: the times customers can choose on each weekday, closed dates, how long a held time lasts and the switch that pauses online booking. Shown with sample Monday to Thursday times. Scroll inside the frame for the whole page.
Admin services list with sample grooming services, durations and price labels
Services with durations, price labels and breed or weight notes, put in order by dragging. Shown with sample services.
Admin photo library grid of sample photos
The photo library: a photo is uploaded once, shows where it is used and can be placed anywhere on the site. Shown with sample photos.
Admin Text and Site Info page with business details and section settings
Text & Site Info, where the owners edit business details and hours, the holding-page switch, and each homepage section's wording, visibility, order and photo.
Admin contact inbox with sample messages waiting for a reply
The contact inbox: every message from the contact form, with a filter for the ones still needing a reply. Shown with sample messages.
Admin activity history listing recent sample changes with the account and time of each
Activity history: who changed what, and when, here recording the sample setup.

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, August 2026

Hosting
cPanel shared hosting
Database
MySQL (MariaDB) on the host
Deploys
ZIP archives uploaded to the host
Photos
Uploads stored on the host
Email
SendGrid notifications

Today, since September 2026

Domain and DNS
Cloudflare DNS, apex and www on Vercel
Hosting
Vercel, deployed on every merge to main
Database
Turso, with a separate preview database
Storage
Cloudflare R2: a public bucket for photos, a private one for paperwork
Email
SendGrid sends notifications to the shop's existing inbox; no mailboxes at the domain
Analytics
Vercel Web Analytics on public pages only

What it does now

In use

  • •The domain serves a holding page with the shop's phone, email and hours and a staff sign-in link. The full site and the admin run behind it.
  • •The owners are being set up on the admin. Their photos and text are not in yet.
  • •Turning the full site on is a switch in the admin. Online booking opens with a second switch once services and prices are confirmed.
  • •When the full site is on, booking requests and contact messages land in the admin and send the shop an email.

Next

  • The owners add their photos, services and prices, then turn off the holding page and open online booking.
  • Google Calendar sync is built and switched off. It can be connected when the owners want the shop calendar's busy times blocked online.

“We went from a Facebook page to a site where people can book, see the gallery and shop, with the admin ready for us to fill in as we go.”

Owner, Bow Wow's Dog Spa

The business and what it needed

Bow Wow's Dog Spa is a dog grooming shop serving Winston-Salem and the Triad in North Carolina. It has the same owners as Midway Music Hall, and I already build and look after the websites for that ownership group. When the owners asked for a site for the grooming shop, they were asking for something they had never had: a real website of their own.

What they needed was simple to say. Customers should be able to find the shop, see what it offers and roughly what it costs, look at dogs the shop has groomed, and ask for an appointment without a phone call. The owners should be able to change all of that themselves, from their phones if need be, without waiting on me.

Grooming has a few details a generic booking widget does not handle well, and they shaped the build from the start:

  • •An appointment is a request. The shop decides whether a time works for that dog, that coat and that temperament, so nothing is booked until a person confirms it.
  • •Many customers bring more than one dog, and two dogs take longer than one.
  • •The shop needs vaccination records, and customers have them as photos or PDFs on their phones.
  • •The owners are busy grooming. Anything they have to do in the admin has to be short and plain.

What was there before

Before this project, Bow Wow's had a Facebook page and nothing else. That page did a fair job of showing photos and posting updates, but it could not list services in a way a customer could scan, it had no way to take an appointment request with the details the shop needs, and the shop did not own it. Anyone searching for a groomer in the area found a social profile, and every question turned into a phone call or a message thread.

There was no old website to migrate, no content to carry over and no domain history to clean up. That made the starting point clean, and it meant every word and photo on the site would eventually have to come from the owners or be written fresh.

Version 1: the cPanel build

The repository starts on January 3, 2026, with the first working pieces already in place: a PHP API, a React public site, a separate React admin, branded email templates, privacy and terms pages and a full favicon set. By March 19 the full public site and admin worked end to end. On April 20 I added the shop catalog and the groundwork for calendar sync.

The first version was built the way I built the group's other sites at the time:

  • •Two React apps built with Vite: the public site and the admin. They share nothing at runtime except the API.
  • •A PHP 8.3 API that answered every request the two apps made, covering content, services, bookings, photos, the inbox and accounts.
  • •MySQL (MariaDB) on a cPanel shared host, with numbered migration files for every schema change.
  • •Apache routing through .htaccess and a small index.php that served the right page shell for each address.
  • •Photos stored on the host, with image variants produced by PHP's GD library.
  • •SendGrid for outgoing email.
  • •ZIP archives for deploys, built from a staging folder by a script so nothing from development could ship by accident.

Production did not serve the full site right away. From May 3 the domain showed a standalone placeholder page, marked so search engines would not index it, while the real site stayed in local testing. Through May and June the work was hardening: a standard Playwright test suite (May 17), my shared PHP backend kit brought in with an admin System Checks page (May 25), a secret scanner, spam traps on every public form, and guards on the deploy archives.

On August 4 the backend was converged fully onto that shared kit (#40, #41). Bow Wow's was the first site to make that move, and it became the worked example for how to converge an existing site. I wrote the tests first: a suite of 29 tests against a real database proved the site behaved the same before the old code was removed.

The first week of August was a client-ready pass (#38). The admin went dark and into the shop's colors (#48), the sidebar was grouped by job, lists became orderable by dragging, and every homepage section gained its own photo. Then I built the pieces that let the site go to production before the owners' content existed:

  • •A launch switch that replaces the whole public site with a holding page (#82).
  • •Withholding the site's data while that page is on, so the content cannot be read through the API either (#83).
  • •A staff sign-in link on the holding page (#86).
  • •Online booking held closed for launch (#51).

The full application replaced the placeholder in production in August 2026, with the holding page on. Later that month I added publishing section by section with a staff preview of the real site (#95), stopped seed migrations from resetting settings the shop owns (#97), installed the legal copy (#98, #99), and guarded the account screens so nobody could lock every admin out (#100). On August 31, server errors started sending an alert (#108).

On September 13, just before the move, migration 022 (#134) rewrote the public copy in one voice. I had found that the text in production was early development wording rather than anything the owners had written, so I replaced it with copy that sounds like a grooming shop: an FAQ of eleven questions and seven plain policies. Each change was guarded by the exact wording it replaced, with backups, so a sentence the owners had edited would be left alone.

Why the site moved, and what changed

In September I moved the ownership group's sites off cPanel and onto one platform: Next.js on Vercel, with Turso for the database. I recorded the decision as an architecture decision for the whole fleet. The reasons were practical. A change should deploy when it merges, without a ZIP upload. Every change should get a preview copy of the site to check before it goes live. Each site should run the same way, so fixing something once fixes it everywhere.

Midway Town Center went first as a pilot, but it had no production data. Bow Wow's was the first site in the group to leave cPanel with real data on it: staff accounts, settings, services, the schedule and the audit log. The cutover happened on September 13, 2026.

The new build was four pull requests, all merged on September 13:

  • •#135 laid the foundation: the Next.js app, the database layer and the configuration.
  • •#136 ported the public API and all 47 admin routes.
  • •#137 added the page shells, the upload limits and the data-move tool.
  • •#140 fixed the export from MariaDB and recorded the cutover.

The two React apps did not change. The Next.js app builds them and serves them, and its route handlers answer the same API calls the PHP code used to answer. Underneath, almost everything moved:

  • •MySQL became Turso, a hosted SQLite-compatible database. The schema is one file of 22 strict tables, and every timestamp must be stored as UTC.
  • •Photos moved to Cloudflare R2. Image variants are now made with sharp, under the same file names the PHP build used, so existing links kept working.
  • •PHP sessions became signed cookies.
  • •Rate limits that lived in files moved into database tables.
  • •The maintenance flag file became an environment setting.
  • •Vaccination paperwork now uploads straight from the browser to a private R2 bucket. That gets around the size limit Vercel puts on a request body, which is smaller than the 8 MB the form allows.

The move was done without freezing the site. A freeze would only have broken the holding page the public was seeing, so instead I checked the live database for edits just before DNS moved. The export first failed on the production MariaDB version, where OFFSET is a reserved word, after passing a rehearsal on MySQL 8. I fixed it (#140), cleared the target, ran a dry run, then applied and verified. The whole sequence had been rehearsed on a throwaway Turso database first. The holding page and paused booking carried across, so the public saw nothing change.

The PHP build stays in the repository. It is the reference the new build was tested against, and it is the rollback: the old host still has the code and a database backup, and pointing DNS back would restore it.

The public site, page by page

The public site is one homepage made of sections. The owners set the order, and each section can be hidden or given its own photo, either beside the text or behind it under a dark overlay. Short addresses open the homepage at the right section, so a link to /services, /booking, /gallery, /products, /reviews, /about, /contact, /faq or /policies goes straight to it. Each address gets its own page title and canonical address, and the sitemap lists only pages the site actually serves (#173, #174).

Home

The opening section carries the shop's name, a short line about comfort-first grooming and buttons to request an appointment or call. Below it, a section on what a visit is like explains how the shop treats nervous dogs, puppies and seniors. Location, hours and the serving area sit near the bottom with the footer.

Services

Each service shows its name, a short summary, how long it takes, a starting price and a note on breed or weight. The owners can hide prices across the board while they settle them. The Services section ships hidden, because the starting prices were drafts. Turning off the holding page cannot publish a price nobody has confirmed.

Request an appointment

The booking form loads only when someone opens it (#121), so the homepage stays light. It works in four steps:

  1. Pick one or more services and the number of dogs. The form adds up how long the visit will take.
  2. Choose a date and an open time. Times come from the shop's weekly template, minus closed dates and times already taken. Picking a time holds it for this customer while they finish.
  3. Enter the owner's details, each dog's name, breed and weight, and the vet's name and phone. Vaccination paperwork can be uploaded as a PDF or photo, or summarized in a note.
  4. Review and send. The page says plainly that this is a request and the shop will confirm it.

If the owners pause online booking, the section shows their pause message and the phone number in place of the form. The Booking section also ships hidden until the owners turn it on.

A photo wall of groomed dogs, before-and-after pairs, the shop itself and the boutique. Any photo opens full size in a lightbox (#71).

In the shop

The catalog lists what the shop sells, by category, with optional prices and a stock label such as in stock or ask in the shop. There is no cart and no checkout. Customers buy at the counter, and the page saves them a trip to ask whether something is on the shelf.

Reviews

Featured quotes from customers with star ratings, and a link to read every review on Google. The rating and count are entered by hand in the admin. The site does not call Google for them.

About, contact, FAQ and policies

The about section tells the shop's story in the owners' voice once they supply it. The contact form takes a name, email, phone and message. The FAQ answers the questions groomers hear every day, and the policies cover late arrivals, cancellations, matted coats and vaccinations.

Privacy, terms and status pages

The privacy and terms pages stay public even while the holding page is on (#93), and the privacy page was updated to match exactly what the site collects (#176). Errors and unknown addresses get branded status pages, and a missing page answers with a real 404 that search engines will not index.

The holding page and the staff preview

While the holding page is on, it replaces the entire site. It shows a short message from the shop, the real phone number, email, hours, address and serving area, and a Staff Sign In link (#82, #86). The site's data is withheld at the API too, so nothing on the full site can be read until the owners choose to show it (#83).

Signed-in staff can open the real site with ?preview=1. A strip across the top lets them switch each section on and off to see how the page will look before the public sees it (#95). The preview uses the admin's existing sign-in cookie, so there is no preview link or token that could leak.

What the owner runs

The admin lives at /admin. It is dark, in the shop's colors, with a sidebar grouped by the job at hand: Dashboard, Bookings, Website, Contact Inbox, Maintenance and Account.

Dashboard

Today's numbers, recent activity and quick actions. While the holding page is on, a notice at the top says so before anything else. A launch checklist scores how much of the site's content is ready, and a panel lists every section that is currently hidden from the public.

Booking requests

Every request arrives in a queue with a status: waiting for confirmation, confirmed, declined, canceled, expired or completed. From a request the owners can confirm, decline or edit it, with a preview of the email the customer will receive. They can add notes, extend or release a held time, download the paperwork from private storage, and delete a request when a customer asks. A manual booking form covers appointments made by phone. The whole queue exports to CSV.

Booking schedule

The weekly template sets which times each weekday offers and how many dogs each time can take. Date overrides close the shop for a holiday or open an extra day. The owners also set how long a held time lasts, how long an unconfirmed request waits before it expires, and a switch that pauses online booking with a message of their choosing.

Google Calendar

A page to connect the shop's Google Calendar, so busy times there are blocked on the booking form and confirmed appointments are written back. It is built and tested, and switched off in production until the owners want it.

Text and site info

Business details, hours, social links, the Google review link and rating, and the copy for every section. This is also where the owners choose which sections show, put them in order, set each section's photo and style, and turn the holding page on or off.

Each has its own screen. Services carry durations, price labels and notes. Reviews carry the quote, the name and the stars. The gallery chooses which photos appear and in what order. Products hold categories and items with prices and stock labels. Lists are ordered by dragging (#54).

Photo library

One library for every photo on the site. Each photo's card shows where that photo is used, and a photo can be placed into a section or the gallery straight from its card (#138, #139). Every photo has a focal point, so when a slot crops it wide or tall the dog stays in frame (#118, #122, #126). Photos over 4 MB are scaled down in the browser before they upload.

Contact inbox

Every contact form message, kept in the database as well as emailed. Messages can be marked handled, so two people do not answer the same one (#81).

Maintenance and accounts

System Checks confirms the database, storage and email are reachable and can send a test email. Activity History is the audit log. Admin Accounts, open only to super admins, adds and removes the people who can sign in. Change Password signs out every other session on that account.

Infrastructure: domain, DNS, hosting, email, storage, analytics

Domain and DNS

DNS runs on Cloudflare. The bare domain points to Vercel, and www redirects to it. Both records are DNS-only, so Vercel handles the certificates. The records for the old host's control panel stay pinned to that host. The mail-related records (MX, SPF, DMARC and SendGrid's sending records) were left untouched at cutover, so nothing about email changed on the day.

Hosting and deploys

The site is a Vercel project with its root in the web folder, running on Node 22. A merge to main deploys to production. Every pull request builds a preview, and since September 30 those previews have their own Turso database, their own R2 buckets with a copy of the public photos, and their own SendGrid key. A preview can never write to production data or email a real customer.

Every change passes one required CI job before it can merge. It runs a secret scan, a workflow linter, a dependency advisory gate, the PHP test suite against a real MySQL service, the frontend tests and builds, a maintainability audit, a bundle-size budget, a schema check, type checking, the Next.js test suite, that suite again in three time zones (#147), and a Playwright smoke test (#111, #157). Dependency updates arrive through Dependabot, and minor and patch updates merge on their own once that job passes.

Database

Turso holds every table. The schema is a single file, applied by hand and checked in CI, and it refuses any timestamp that is not UTC. The code refuses to start against a local database file when it is running on Vercel, so a misconfigured deploy fails loudly.

Email

Email is sending only, through SendGrid. When a customer sends a booking request or a contact message, the shop gets a notification at the inbox it already used. The reply-to points at the shop, so a customer who replies reaches a person (#94). Customer receipts, confirmations and declines go out when the owners act in the admin. Every message is tagged with the site's name (#160), and the tracking pixel is off for these messages (#170). The SendGrid key can send mail and do nothing else.

There are no mailboxes at the domain. Nothing receives mail there today, which is fine because every submission is also stored in the admin. Adding inbound mail before the old hosting account closes is an open decision for the owners. DMARC reports come to me, so I see if anyone tries to send mail as the shop.

Storage

Cloudflare R2 holds two buckets. The public one serves the site's photos from the shop's own CDN subdomain. The private one holds vaccination paperwork and is reached only through signed-in admin download routes. Paperwork lands first in a pending area that clears itself after a day, so an abandoned upload does not linger.

Analytics and alerts

Vercel Web Analytics runs on the public pages only (#175). The admin is not tracked. Any server error sends me an alert, throttled so a bad minute does not become a flood of email (#108).

Decisions worth explaining

The contract-tested port

Moving a working system to a new platform is where things quietly break. I did not write a fresh spec for the new build. I recorded what the PHP system actually did and held the new build to that.

The recordings live in the repository as golden files. There are 57 public requests, made once with the site open and booking on and once with the site held and booking paused, each with the database rows it wrote. There are 118 edge cases for how the old code cleaned up input. And there is a 156-step admin session that signs in as each of the four roles and works through the admin. The test suite replays all of it through the new route handlers on every change. At the merge of #136 that was 497 tests across 19 files, plus 62 schema checks.

Where the new build behaves differently on purpose, the difference is written down in a list beside the tests and in a table in the README. Most of those rows are bugs the port fixed:

  • •A request for more than one dog held a time sized for one dog, so every multi-dog request failed.
  • •A public booking could set fields meant for staff and hide a real request as a test.
  • •A customer's name went into the staff email without escaping.
  • •Database error text could reach the browser.
  • •Changing a password left other sessions signed in.
  • •Two calendar sync runs at once could create the same event twice.

The clocks needed their own care. The old database's clock ran on Mountain Standard Time, while PHP wrote UTC, and neither stored a time zone. The data move converted each column from the clock that actually wrote it. The new schema rejects any time that is not UTC with a Z, the API shows times on the shop's clock, and the tests run under Tokyo, Berlin and Los Angeles time as well as the shop's own to catch anything that leans on the machine's zone.

The booking request with a time hold

A grooming appointment is a request, and that decision runs through the whole flow. When a customer picks a time, the site creates a hold on it, 30 minutes by default and sized by the number of dogs. The hold keeps someone else from taking the same time while the first customer types out their dog's details and finds their vaccination records. If they finish, the request is saved as waiting for confirmation and the hold becomes part of it. If they wander off, the hold expires and the time opens again.

The shop then confirms or declines. A request nobody answers expires after a set number of hours, so a customer is not left wondering. Customers never see a confirmed booking until a person has said yes.

Two people can pick the same time in the same second. The old system used a MySQL row lock for that. In the new build, the check and the insert happen inside one write transaction that takes the database's write lock up front. A test sends five holds for the same time at once, and exactly one wins (#136).

The paperwork upload is tied to the hold. The browser asks for a signed upload address bound to that hold, sends the file straight to private storage, and the server checks the file's first bytes to confirm it really is a PDF or image before it is attached to the request.

Roles

The shop is small, but more than one person may help with it. There are four roles:

  • •A super admin can do everything, and is the only role that can manage accounts.
  • •A manager can do everything except manage accounts.
  • •A scheduler sees the booking requests and the schedule.
  • •A content editor sees the website screens, the photo library, the catalog and the contact inbox.

Every admin request goes through the same three checks in order: the request must come from the site's own origin, the signed-in account is read fresh from the database, and the role must allow that section. Changing a password or removing an account takes effect on the next request, because each account carries a session counter that invalidates old cookies. Sign-in attempts are throttled after five failures in fifteen minutes. The account screens will not let anyone remove or demote the last super admin (#100).

Retention

The site collects names, phone numbers, dog details and vaccination records, so I built a way to get rid of them. A single request can be deleted when a customer asks, along with its attachments (#84). The owners can also set a retention window of 6, 12, 24 or 36 months, or none. Only finished requests (declined, canceled, expired or completed) are ever removed, so no setting can delete an upcoming appointment. There is no scheduled job for this. The window is applied when the booking screen opens, at most once a day, and each clean-up is written to the activity history. The window itself is kept out of the public settings feed.

Launch is a switch in the admin

The owners can open the site when they are ready, without asking me for a deploy. The holding page, the paused booking and the hidden Services and Booking sections are all settings. Turning the holding page off shows the sections that are ready and keeps the draft prices hidden. Opening booking is a second, separate switch.

What it does now

The site is live at bowwowsdogspa.com. As of October 5, 2026, the domain serves the holding page with the shop's phone, email, hours and a staff sign-in link, and online booking is paused. The full public site and the admin run behind it on the new platform.

The owners are still being set up on the admin. Their photos, services, prices and text are not in yet. When they are, the owners will turn the holding page off from Text and Site Info and open booking from the Booking Schedule. From then on, booking requests and contact messages land in the admin and send the shop an email.

Google Calendar sync is built and switched off. Once the owners want the shop calendar's busy times blocked on the booking form, connecting it is a sign-in from the admin.

There are no traffic or booking numbers to report yet, because the public has not seen the full site. I will add an honest outcome here when there is one.

Ongoing support

I look after the site the same way I look after the group's other sites: mostly hands-off unless something breaks. Server errors alert me. Dependency updates arrive weekly, and the safe ones merge themselves once the full CI job passes. A weekly security check runs against the tooling. When a fix is needed, it goes through a pull request, gets a preview with its own database, and deploys when it merges.

The repository's documentation describes the stack that actually runs. In late September I audited every document, marked each one with the era it describes, and ran the local setup from the README end to end to prove it worked (#164). Whoever picks this up next, including me, starts from instructions that are true.

When the owners turn the full site on, the work shifts to helping them settle into the admin: placing their photos, confirming prices and answering the first booking requests.

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.