Case study · Founder & sole engineer
Renovation Defenders
A learning and commerce platform for renovation professionals — safety training, products, tools, and community in one place.
Six product domains that are normally six separate products, sharing one platform spine. I designed, built, and operate all of it, plus the Flutter app.
- Backend Laravel 12 · MySQL on RDS
- Frontend Blade · Bootstrap 5
- Mobile Flutter — iOS & Android
- Payments Stripe · In-App Purchase
- Live renovationdefenders.com
Renovation Defenders — one platform, six product domains
Any one of these is normally its own product. They share a single spine so they also share identity, billing, admin, and storage.
Clients
-
Web application
Laravel 12 with Blade and Bootstrap 5. Three distinct layout systems — public, authenticated, and admin — selected by view component.
-
Flutter mobile app
iOS and Android, talking to a Sanctum-authenticated REST API. Ships In-App Purchase, including the refund path.
Product domains
- Video courses S3-hosted lessons, per-user assignment, module structure, automatic progress tracking.
- E-commerce Product catalog, cart, and Stripe checkout with a CSRF-exempt webhook endpoint.
- Price estimator A multi-step wizard that generates a branded PDF quote at the end.
- Publication Admin-managed articles with JSON-LD structured data and a generated sitemap.
- Video contest User-submitted entries with voting and moderation.
- Admin back office Categorised FAQ, content management, and user administration.
Shared platform spine
- Auth & roles (admin middleware)
- Stripe billing & webhooks
- S3 media storage
- PDF generation
- Security-header & cache middleware
- Elastic Beanstalk deploy
55 migrations, 128 routes, one coherent data model. The interesting constraint was inherited: the database uses uppercase table and column names throughout, so the entire Eloquent layer, validation rules, and mobile API accommodate it rather than fighting it.
Why six domains in one product is the hard part
Individually, none of these is exotic. A course player, a storefront, a quote generator, a blog, a contest, an admin panel — each is a well-trodden problem with good libraries behind it.
The difficulty is that they all have to agree. One identity, so a user who bought a product and was assigned a course is the same person in both. One permission model, so the admin panel can moderate contest entries and manage catalog items without a second auth system. One storage story, so a course video and a product image do not live in two different places with two different access rules. One deploy, so a change to the estimator cannot take down checkout.
The temptation is to bolt each domain on as it is requested, and the result is a codebase with six half-shared conventions. The alternative is to decide the spine first — identity, roles, storage, payments, admin — and require every new domain to sit on it. That is the decision this platform is built around, and it is why the sixth domain took less time than the third.
The inherited constraint
The database uses uppercase table and column names throughout — USERS,
EMAIL, BLOG_POSTS. That is not a convention I would choose,
and it is not one I could change without a migration touching every table in a live
system.
So the whole application accommodates it: Eloquent attribute access, validation rules, and the mobile API contract all speak uppercase. With one deliberate exception — Laravel's built-in password-reset repository queries its result set by lowercase keys, so that one table keeps an uppercase name with lowercase columns. It looks like an inconsistency until you know why, which is exactly the kind of thing that belongs in the project's documentation rather than in someone's memory.
Working with a constraint you did not choose, and documenting why, is most of what maintaining real software actually is.
Two clients, one contract
The Flutter app is not a wrapper. It authenticates through Sanctum, tracks video progress against the same records the web player writes, and carries its own monetization path through In-App Purchase — including refunds, which is the part that gets skipped until a customer asks for one.
Keeping web and mobile honest to the same API is what the written handoffs are for. When the mobile client needs something the API does not yet provide, that request is a document with a rationale and a proposed contract — not a message that gets lost.