Allen Kenoyer Glass
Custom Software Development
- Next.js 16 (App Router)
- React 19
- Supabase (Postgres, Auth, Storage)
- Google Calendar Service-Account Sync
- Row-Level Security
- Tailwind CSS v4 + shadcn/ui
- Resend Email Delivery
- JSON-LD Structured Data
- Vitest + Playwright

The rebuilt Allen Kenoyer Glass site — custom Next.js front end with a self-serve class CMS behind it.
Allen Kenoyer Glass is a stained-glass studio in Lawndale, California that had been running on an aging WordPress install for years. The objective was narrow and explicit: get the studio off WordPress, and rebuild only the content management it actually uses. A general-purpose CMS and its plugin stack had been carrying a site that publishes two kinds of thing — a class schedule and a studio announcement. 10xDev rebuilt the property as a custom Next.js 16 application: a fourteen-route public marketing site, and behind it a purpose-built admin that manages those two content types and nothing else.
Only the CMS the Studio Actually Needed
Most of the old site was content that changes on the order of years — service pages, portfolio, the patterns catalog — and none of it needed a database or an editor behind it. That content ships as code and renders statically; the patterns catalog is a typed module, and adding a pattern is a commit, not a form. Scope was locked in writing at the start: no e-commerce and no online class registration, both of which the studio handles in person. What survived the cut is exactly what changes weekly and only the studio can author — the class schedule and the announcement banner — so the admin has two sections, no page builder, no theme layer, and no plugin surface to keep patched. Everything else in the build went into making those two things genuinely good.
A Content Model That Matches Real Classes
A studio schedule does not fit a flat list of events. The data model is three tiers: classes holds the stable course template — name, category, skill level, prerequisite, tuition and supply fees, capacity; cohorts is one specific run of that course, tagged multi_session or single_session; and cohort_sessions is one row per actual date. A five-week beginner course is one class, one labeled cohort, and five session rows. Visibility falls out of the data rather than a publish checkbox — a class appears on the public site when it has at least one session that has not yet ended, so a finished course takes itself down and adding a date is what puts it back up. Every datetime column is timestamptz in UTC and rendered through a single hardcoded studio timezone, so no display ever drifts across a daylight-saving boundary.
One-Way Google Calendar Sync
The studio manager wanted to keep the public Google Calendar her students already check, but not to maintain it by hand alongside the site. The integration is strictly one-way: the database is the source of truth and the calendar is a downstream mirror, one event per session row. Writes go through a dedicated Google Cloud service account granted writer access to her calendar — no OAuth refresh token sitting there waiting to be silently revoked. The propagation layer is a pure, unit-tested resolver: hand it a cohort’s after-state and it recomputes each session’s chronological ordinal and emits the create, update, or delete op for every event. That is how moving a single date correctly re-pushes the sibling titles whose “Class 3 of 5” numbering just shifted.
Failure handling is DB-wins. A Google outage never blocks admin work — the database write commits, and a failed push flags the row with sync_status and sync_error so the dashboard can offer a retry. Retries are idempotent: the executor fetches the stored gcal_event_id before deciding whether to create or update, so an event deleted on the Google side falls back to a clean create instead of erroring forever. A permanent Sync all action in the admin exists as the heavy-weight recovery tool for when state drifts.
The Admin Dashboard
The dashboard is the actual product of the rebuild. It is gated by Supabase Auth with session refresh in middleware and a server-side admin guard on every protected route, using the modern publishable/secret key format rather than the legacy service-role JWT. Classes get a full editor — the class form, a cohort manager for adding runs and their individual session dates, a derived status pill showing whether a class is currently visible to the public, and per-session sync indicators. Announcements are a single markdown body edited in place, where saved state is public state, surfaced on the site through a header modal. Row-level security policies back the whole surface, so authorization is enforced in Postgres rather than only in the application.
Public Forms, Hardened in Postgres
Three intake forms — general contact, custom-design inquiry, and repairs — share one pipeline: a Zod schema, React Hook Form on the client, submission through a Server Action, and delivery via Resend. Two spam layers sit in front of it. A honeypot field rides along in every schema and resolves success-shaped when tripped, so bots get no signal. Behind that is a sliding-window per-IP rate limit implemented in the database: the raw-IP counter lives in a private schema the Data API never exposes, reachable only through a SECURITY DEFINER function that returns a bare boolean and nothing else, with the implicit PUBLIC execute grant revoked and re-granted only to the two roles the form action actually runs as.
Leaving WordPress Without Losing Rankings
Migration was treated as its own engineering problem. The app sets trailingSlash: true to preserve WordPress’s canonical URL form, so every legacy URL resolves in a single 301 hop rather than a redirect chain. The redirect set is built in code and unit-tested against the mapping table of record, emitting a literal 301 rather than the 308 that Next produces from permanent: true, with no catch-all to the homepage — unmapped paths are left to 404 instead of sending a soft-404 signal. Legacy content was extracted to markdown and hand-converted, and every image was migrated into a single public-read Supabase Storage bucket by idempotent scripts, then served through next/image for optimization.
Structured Data Built From Site Config
The SEO layer is hand-rolled and typed against schema-dts, so a malformed graph is a compile error rather than something discovered in Search Console. The studio emits a LocalBusiness composite typed as ArtGallery and EducationalOrganization, carrying the full address, geocoordinates, opening hours, price range, and social profiles — all sourced from the same site-config module the visible page renders from, so the entity signal can never disagree with the page. Each class detail page emits Course markup drawn from its own row. The generated sitemap reuses the exact visibility function the public pages use, so it can never list a class the site does not show.
Built From a Decision Log
The build ran off a written decision log rather than accumulated convention: twenty-one ADRs covering framework, hosting, auth, data modeling, image pipeline, forms, calendar integration, redirects, and schema markup, each locked before the code that realized it. Work was executed as phased task files with declared dependencies, and quality is gated in CI by a single pnpm check — ESLint, Prettier, a strict TypeScript pass, and Vitest — with Playwright suites covering admin auth, class and cohort management, announcements, and every public form.
The Result
WordPress is gone, and nothing was rebuilt that did not need to be. The studio manager updates one place: a class change appears on the public site and on the Google Calendar her students check within the same save, and the announcement banner is hers to edit. There are no plugins to update, no theme to break on a core release, and no admin surface she has to navigate around to reach the two screens she uses. The site itself is a fast, statically-rendered Next.js property on Vercel, and every legacy URL lands in a single hop.