Delivered remotely for a business based in Nashville, United States. Names withheld by agreement.
Project Overview
Industry: Wedding and special-event services — local services marketplace and lead generation.
Type of solution: A two-sided web marketplace comprising a public consumer site, a vendor self-service portal, and an administrative back office for staff moderation and account management.
Business context: Wedding service procurement is intensely local, time-bounded and fragmented across dozens of unrelated trades. A couple is a one-time buyer with no repeat relationship and no way to compare unfamiliar suppliers. The suppliers are small independent businesses for whom a directory listing produces attention but not intent, and for whom advertising spend is hard to justify without knowing who is actually interested. Both sides needed something more concrete than a listing.
General users: Consumers planning a wedding; vendor businesses across several dozen service categories; and platform staff who moderate offers and manage accounts through a back office with per-module access rights.
General purpose: To give consumers a searchable set of real, redeemable offers from vendors that serve their city, and to give vendors a measurable stream of qualified enquiries generated by the offers they publish.
The Business Challenge
A listing is not an offer. Directory-style listings put businesses in front of consumers but give the consumer nothing to compare and the vendor nothing to measure. The product had to be built around concrete, limited, redeemable offers so that both sides had something specific to act on.
Interest had to become contactable intent. A page view is worthless to a small business. The platform needed an explicit moment at which a browsing consumer becomes a named enquiry — and that enquiry had to arrive with enough context (what they want, where they are, when the event is) that a vendor can pick up the phone the same day.
Relevance is bounded by geography and date. An offer is only useful if the vendor serves the consumer's city and is available for the consumer's date. Both had to be first-class parts of the data model and the search experience, not filters bolted on afterwards.
One offer, many cities. A vendor's service area rarely matches a single location. Offers had to target an arbitrary set of cities, and editing that set had to reconcile additions and removals cleanly rather than replacing everything each time.
Two ways to limit an offer. Some promotions run out by volume, others by date. The two constraints are mutually exclusive but share a single offer record, so authoring, validation and display all had to switch behaviour on the chosen model.
Quality control at the front door. A marketplace whose value proposition is trustworthy offers cannot publish them unreviewed. Offers needed a status lifecycle with staff moderation before going live, without making publication feel slow to the vendor.
The vendors are not technical. Deal authoring, image management, multi-city targeting and enquiry follow-up had to be usable by a small-business owner with no training and no support call.
The transaction completes offline. The deal is redeemed in person, not in a checkout. The platform needed a mechanism to bridge the online offer and the offline redemption.
The platform had to be cheap to run and hard to break. A marketplace serving small businesses cannot carry a heavy operational footprint. The architecture had to run predictably on modest hosting, with as little that could go wrong as possible.
Our Approach
Make the offer the unit of the marketplace. Rather than modelling businesses with promotions attached, we modelled offers as the primary entity — each with its own category, savings, imagery, redemption code, targeting and constraint model, belonging to a vendor. Search returns offers; the vendor profile is what the consumer reads after something has already caught their attention.
Design the save action as the conversion event. Saving an offer writes an explicit link between a consumer and an offer, and that link surfaces on the vendor's side as an enquiry carrying the consumer's name, contact details, city, event date and the code of the offer that attracted them. One consumer gesture, one qualified lead, no forms in between.
Capture the event date once and reuse it everywhere. The event date is captured against the consumer account and carried into every enquiry the consumer generates. It is the single most useful qualifier a vendor can receive, and asking for it once means never asking again.
Model city targeting as an explicit relationship. Offers map to cities through a dedicated association, and saving an edited offer reconciles the stored set against the submitted one — adding what is new, removing what has been dropped — rather than deleting and rewriting.
Give the vendor a real authoring tool. The vendor portal covers offer creation and editing, per-offer image galleries with drag-and-drop upload, thumbnailing, grouping and reordering, multi-city and category targeting, presentation styling, and an enquiry list per offer with export for use in whatever the vendor already runs their business on.
Bridge online and offline with a redemption code. Every offer carries a short code the consumer quotes to the vendor in person. It is how a marketplace settles a transaction it does not process.
Validate everything at the boundary, on the server. Every field of every request is checked against a length limit and either a permitted character set or a complete enumeration of allowed values, before session loading and before any database work. Requests that do not match a known operation are treated as hostile and raise an alert.
Keep the dependency surface near zero. The application uses no framework and no package manager. The AJAX transport, multi-select controls, date picker, tab strip, gallery, upload handling and image CAPTCHA are all either purpose-built or self-contained. There is no build step and no dependency graph to break — which suits a platform that has to run inexpensively and predictably for years.
The Solution
Deal-centric search. Consumers choose a service category and a city — selected through a cascading country, state and city control — and see the live offers available to them, each with imagery, headline savings, vendor name and location.
Deal detail with vendor context. Each offer page carries the full description, gallery imagery, the redemption code, the vendor's own description, website and social presence, and the consumer's actions: save it, message the vendor, or comment.
Saved offers as enquiries. Saving an offer adds it to the consumer's account and simultaneously registers a qualified enquiry against the vendor, complete with contact details, city, event date and originating offer code.
Vendor enquiry list and export. Vendors see every consumer who saved each of their offers, and can export the list for use outside the platform.
Deal authoring with two constraint models. Vendors create offers with a title, description, category, savings, redemption code and presentation styling, and choose either a redemption-count limit or a fixed expiry date.
Multi-city targeting. A single offer can be published into any set of cities the vendor serves, edited freely, with the stored targeting reconciled on save.
Per-offer image galleries. Drag-and-drop upload with automatic thumbnail generation, image grouping, reordering and deletion, all from the vendor portal.
Moderated publication. Offers carry a status and are reviewed by staff before becoming visible, so what consumers see has been checked.
Vendor profiles. Company description, website and social handles, presented alongside every offer the vendor publishes.
Direct messaging and comments. Consumers can message a vendor from an offer page and leave comments, giving the marketplace a conversational surface alongside the transactional one.
Full account lifecycle. Registration with email verification, distinct account states, lockout after repeated failed sign-ins, unattended self-service recovery through security questions, escalation on repeated recovery failures, and password reset that re-keys the account's encrypted data.
Consumer and vendor from one account model. Both sides share a single authentication path and account record, distinguished by type, with vendor-specific attributes held separately — one login system, two products.
Administrative back office. Staff manage accounts, moderate offers and administer the platform through a back office with a per-module access matrix controlling read, write, create and delete rights per staff member.
Responsive delivery. Width- and orientation-aware layouts, device-width viewport handling, client-side device detection and self-hosted fonts, so the site works on the phone the consumer is actually holding.
Key Features
Deal-based marketplace model The marketplace trades in concrete, limited, redeemable offers rather than business listings, giving consumers something specific to compare and vendors something specific to measure.
Save-to-enquiry conversion A single consumer gesture converts interest into a named, contactable enquiry delivered to the vendor with contact details, location, event date and the originating offer code.
Event date as a first-class attribute Captured once against the consumer account and carried into every enquiry, giving vendors the qualifier that matters most in a date-driven industry.
City-level offer targeting Offers publish into an arbitrary set of cities through an explicit association, with add/remove reconciliation on every edit.
Dual redemption constraint models Volume-limited and date-limited offers share one record shape, with authoring, validation and display switching on the model the vendor chooses.
Vendor self-service portal with galleries Deal authoring, drag-and-drop image upload with thumbnailing, grouping and reordering, targeting, and enquiry management — usable without training.
Enquiry export Vendors export their enquiry lists for use in whatever tools they already run their business on, rather than being locked into the platform.
Moderated offer lifecycle Offers pass through staff review before publication, protecting the credibility the marketplace depends on.
Unattended account recovery Lockout, security-question recovery and escalation to suspension work without staff involvement — necessary for consumers who will use the platform once.
Field-level encryption with re-keying Sensitive account data is encrypted under a per-account key, and every affected field is re-encrypted under a newly generated key whenever the password is reset.
Technical Architecture
Presentation layer. A single bootstrapper resolves each requested page to a template within the requesting account's own content directory, falling back to a guest set for anonymous visitors, and performs token substitution before serving the HTML. Per-account theming and content variation are achieved without a template engine.
Client layer. Each page pairs a template with a JavaScript module built on a hand-written XHR wrapper. Purpose-built components cover multi-select, tabs, date selection, modals, galleries, uploads and device detection. Responsive CSS handles width and orientation; fonts are self-hosted.
Command endpoints. Every request carries an operation and a target. The matching server endpoint dispatches on that pair, with an explicit terminal branch that treats any unrecognised combination as hostile and raises an operational alert. Responses are structured documents with a success/failure envelope, parsed and rendered by the client.
Validation layer. Before session loading and before any data access, every submitted field is checked against a maximum length and either a permitted character set or a complete enumeration of allowed values. Enumerated fields — category, account type, redemption model — are validated against the full permitted set, so unknown values cannot reach the data layer.
Shared services layer. A common include provides session loading and re-verification, database connection with intent-appropriate credentials, validation, output escaping, encryption and decryption, mail composition and delivery, thumbnail generation, email verification and centralised error, exception and shutdown handling.
Data layer. A relational database holding accounts, vendor attributes, offers, offer-to-city targeting, saved offers, comments, contacts and notes. Read paths and write paths connect as different database users, so a read operation is physically incapable of writing.
Media layer. Uploaded imagery is written to per-vendor, per-offer directories with thumbnails generated on upload and served as static files directly by the web server.
Reference data layer. Country, state and city data are served from a bundled dataset rather than an external geocoding service, keeping geography selection fast and dependency-free.
Back office. An existing open-source PHP business-management application administers staff, accounts and configuration on the same database, extended with a self-installing customer-accounts module that creates its own tables, registers itself and provisions access rights.
Infrastructure layer. Web-server configuration enforces HTTPS, applies cache-control policy and controls directory index resolution; a scheduled task handles periodic maintenance; operational alerts are delivered by email with a local log.
Flow: Responsive browser client → command endpoint with allow-list validation → shared services (session, encryption, mail, media) → relational database with intent-separated credentials → static media and reference data served by the web server → moderated publication → enquiry delivered to the vendor
Technology Stack
| Category | Technology |
|---|---|
| Server language | PHP |
| Architecture | Server-rendered pages with an XHR command-endpoint layer; no framework |
| Database | MySQL via the native driver, largely with prepared statements |
| Database access model | Separate read-only and read-write credentials selected per operation |
| Encryption | libsodium authenticated encryption for sensitive account fields |
| Image processing | Server-side thumbnail generation from uploaded imagery |
| File upload | Drag-and-drop upload with chunked server-side handling |
| Purpose-built MIME mail composition with HTML templates and delivery logging | |
| Anti-abuse | Image CAPTCHA (configurable), allow-list request validation, lockout and alerting |
| Client scripting | Vanilla JavaScript with a hand-written XHR wrapper; jQuery for the vendored upload component |
| Client components | Purpose-built multi-select, tab strip, date picker, modal, gallery, cookie and device-detection modules |
| Front end | Responsive CSS with width and orientation media queries, device-width viewport, self-hosted web fonts, web app manifest |
| Wire format | Structured documents with an explicit success/failure envelope |
| Reference data | Bundled country, state and city dataset driving cascading geography selection |
| Web server | Apache with forced HTTPS, rewrite rules and cache-control policy |
| Scheduling | Cron-driven maintenance task |
| Back office | An existing open-source PHP business-management application with a per-module access matrix, extended with a self-installing module |
| Dependencies | No package manager and no build step; third-party components vendored and self-contained |
Technical Challenges & Solutions
| Challenge | Our Approach |
|---|---|
| Turning browsing interest into something a small business can act on | The save action writes an explicit consumer-to-offer relationship that surfaces on the vendor side as a qualified enquiry carrying contact details, city, event date and the originating offer code — one gesture, one lead, no intermediate form. |
| Relevance bounded by both geography and a fixed date | Both were made first-class: city targeting is an explicit association between offers and cities, and the event date is captured once against the consumer account and carried into every enquiry rather than being re-collected. |
| One offer needing to appear across an arbitrary, editable set of cities | Targeting is stored as a set of associations, and saving an edited offer reconciles stored against submitted — adding what is new and removing what has been dropped — instead of deleting and rewriting the set on every save. |
| Two mutually exclusive ways to limit an offer sharing one record | Volume-limited and date-limited constraints are held in separate fields with exactly one populated, and the authoring UI, server validation and public display all switch on the selected model, so an offer can never carry a contradictory limit. |
| Publishing only offers that have been checked | An explicit status lifecycle with staff moderation, enforced in the queries that drive public search and listing so that unmoderated offers are unreachable rather than merely hidden. |
| Untrusted input reaching a system with no framework validation layer | Allow-list validation of every field at the boundary — maximum length plus either a permitted character set or a complete enumeration — executed before session loading and before any data access, with unrecognised operations treated as hostile and alerted on. |
| Protecting sensitive account data without a key-management service | Field-level authenticated encryption under a per-account key held encrypted under a master key, with every affected field re-encrypted under a freshly generated key whenever the password is reset. |
| Consumers who use the platform once and will not contact support | Unattended account recovery: lockout after repeated failed sign-ins, self-service unlock through security questions, and escalation to suspension on repeated recovery failure, all with automated notification. |
| A transaction that completes in person, not in a checkout | A short redemption code carried on every offer, quoted by the consumer to the vendor — the bridge between an online marketplace and an offline sale. |
| Running reliably and cheaply for years without an operations team | A near-zero dependency surface with no framework, package manager or build step, static media and reference data served directly by the web server, and platform configuration handled at the web-server layer. |
Security & Reliability
Transport security. HTTPS is enforced at the web-server layer for every request, with cache-control policy applied to dynamic pages.
Input validation at the boundary. Every field of every request is validated against a maximum length and either a permitted character set or an explicit enumeration of allowed values, before session loading and before any database access. Requests that do not correspond to a known operation are rejected and raise an alert.
Parameterised data access. Data operations use prepared statements with bound parameters across the majority of paths.
Least privilege at the database. Read-only and read-write credentials are chosen per operation, so read paths cannot write regardless of what reaches them.
Encryption of sensitive account data. Authenticated encryption protects sensitive fields, keyed per account, with the account key itself held encrypted and rotated on password reset.
Account protection. Distinct account states, lockout after repeated failed sign-ins, self-service recovery through security questions with escalation to suspension on repeated failure, email verification, and configurable image CAPTCHA on sensitive forms.
Administrative access control. Back-office staff hold explicit per-module read, write, create and delete rights rather than blanket administrative access.
Operational alerting. Malformed requests, script execution failures and mail delivery failures raise alerts to a staff address with a local delivery log, so problems are noticed on a platform that carries no monitoring stack.
Small dependency surface. With no package manager and no framework, the code that runs in production is the code that was written for it — a meaningful reduction in supply-chain exposure for a long-lived, lightly staffed platform.
Scalability & Performance
Stateless request handling. Session state lives in cookies and the database rather than in application memory, so instances carry no local state.
Read/write separation built in. Because read and write paths already connect as different database users, introducing a read replica is a configuration change rather than a code change.
Static media served directly. Uploaded imagery and pre-generated thumbnails are served as files by the web server, keeping image delivery entirely off the application path.
Thumbnails generated once at upload. Listing pages never resize images at request time.
Reference data served from files. Country, state and city selection reads from a bundled dataset rather than querying the database or calling an external service on every interaction.
A light client. No framework payload, self-hosted fonts and purpose-built components keep the initial load small — which matters most on the mobile connections consumers actually browse from.
Platform concerns at the platform layer. HTTPS enforcement, cache-control policy and index resolution are handled by web-server configuration rather than in application code.
Deliberately modest operational footprint. No cache tier, queue or search service — an architecture matched to a platform that must run predictably and inexpensively for years rather than one sized for traffic it does not have.
Business Outcomes
- The marketplace trades in offers, not listings, giving consumers something concrete to compare and vendors something concrete to measure.
- Consumer interest becomes a contactable enquiry at a single, well-defined moment, with the context a small business needs to follow up the same day.
- Vendors receive enquiries qualified by location and event date, rather than impressions they cannot act on.
- Vendors manage their own presence end to end — offers, imagery, targeting and enquiry follow-up — without needing platform staff.
- Enquiries leave the platform freely, exportable into whatever tools a vendor already uses.
- Only reviewed offers reach consumers, protecting the credibility the marketplace depends on.
- Consumers who use the platform once can still recover their own accounts without contacting support.
- The platform runs on a small operational footprint, appropriate to a business serving independent local vendors.
Why it worked
Two-sided marketplaces fail on the same question every time: what, precisely, is the moment a browsing visitor becomes something the supply side can act on? Get that wrong and you have built a directory — plenty of traffic, no value on the supply side, and no reason for anyone to pay. Get it right and everything else in the product is in service of one clear mechanic.
Our team builds for that clarity. We made the offer, not the business, the unit of the marketplace, so that both sides had something specific to act on. We designed the save action to be the conversion event and made sure the resulting enquiry arrived carrying location, event date and the offer that prompted it — because a lead without context is a lead a small business will not chase. We modelled the two constraints an offer can genuinely have, rather than forcing one shape onto both. And we matched the architecture to the operational reality: a platform serving independent local vendors has to run cheaply, predictably and for a long time, which is a design constraint worth respecting rather than an excuse for a heavier stack.
Our teams work across PHP and relational data modelling, marketplace and lead-generation product design, self-service portals for non-technical business users, media handling and moderation workflows, and applied cryptography for account data — with the judgement to decide which parts of a product deserve engineering depth and which are better kept simple enough to survive without a maintenance team.
Final Summary
A couple planning a wedding buys around twenty unrelated services, once, from small local businesses they have never heard of, against a date that cannot move. The businesses on the other side are independent operators with no marketing function, for whom attention is worthless and a qualified enquiry is everything. A directory serves neither.
Our team built a marketplace around the offer rather than the listing. Vendors publish concrete discounts — limited either by redemption count or by expiry date — targeted at the cities they actually serve, with their own imagery, description and a redemption code that bridges the online offer and the in-person sale. Offers pass staff moderation before publication. Consumers search by category and city, having given their event date once, and see real offers from vendors who can serve them.
The mechanic that makes it work is the save. When a consumer saves an offer, the platform registers a qualified enquiry against that vendor carrying the consumer's contact details, city, event date and the code of the offer that attracted them — exportable, actionable, and delivered without a form standing in between. Around that sit the things a self-service marketplace needs: vendor galleries with drag-and-drop upload, multi-city targeting that edits cleanly, direct messaging and comments, a full account lifecycle with unattended recovery, encrypted account data with re-keying on password reset, and validation at the boundary of every request.
It runs on a deliberately small architecture — server-rendered PHP, a purpose-built client, no framework and no build step — because a platform serving independent local businesses has to work every day for years without an operations team behind it. That constraint shaped the engineering, and the product is better for having taken it seriously.