Rebuild Architecture
Recognition Media is not an awards platform. It is a factory for awards programs — and the architecture should say so.
Out of scope — the CDM support engagement, which runs on separate assumptions. Timeline and cost estimates; §9 provides the work segmentation those depend on.
#opp-recognition-media-platform-rebuild
01What the system is
Recognition Media is not an awards platform. It is a factory for awards programs.
Nine live programs today, roughly eleven within five years, each with its own brand, calendar, category taxonomy, pricing, jury model and revenue mix. The client’s own framing is the Alphabet analogy: RM is the holding entity, and each program must feel like an independent organisation with no RM branding anywhere. Partners like Time and Magellan buy a subset of the machinery and put their own name on it.
So the product is not any one awards program. The product is the ability to define, launch, run and retire program variants — in days rather than quarters. Every architectural decision below follows from taking that literally.
The awards pipeline
Every program is the same six-stage pipeline, and each runs some subset of it.
- Webby runs all six, with two judging rounds and a live event.
- Time runs
DEFINE → INTAKEonly. - Most programs run everything except ticketing, with a single judging round.
- Public voting is typically switched on in a program’s second year, once there is an audience.
Two properties of this pipeline drive the architecture more than anything else.
Nothing in an awards program happens because a user clicked something. It happens because a date passed — entry opens, early pricing expires, the final deadline hits, judging round one opens, voting opens, winners are announced, the store opens. The calendar is the control plane.
Entries are transient; they belong to one season and are largely irrelevant twelve months later. The winners record is cumulative and runs back to 1997. It is the brand’s credibility, its SEO, the substrate for the future Directory, and the reason people pay to enter. It compounds.
02Evidence from the current platform
Evidence, not argument. Three things worth extracting: what the existing system gets right, what it gets wrong, and the numbers it has to handle.
What it gets right — carry these forward
These emerged from eleven years of running real awards. A rebuild that loses them would be a downgrade.
| Concept | Why it works |
|---|---|
| Season cloning as the primary creation path | Clone last season, adjust categories, live in ~15 minutes. Categories are the main year-on-year change; everything else is deliberately stable, because a hallmark of a credible award is that the dates don’t move. |
| Visible vs. enforced deadline | Marketing says Friday; the system actually closes Sunday night. A deliberate, revenue-positive mechanic — and not something you would invent from first principles. |
| Backdoor entry | Program shows as closed publicly, but a private link still accepts entries from people the team is actively courting. |
| Per-media-type early close | Documentaries and podcast seasons close earlier because they take longer to judge. A good argument for deadlines being per-scope rather than per-season. |
| Work vs. entry separation | One piece of work entered into several categories creates several entries — the client calls this the “double dip.” The right decomposition, and also how the revenue works. |
| Deferred credits | Submit without the full credit list and add it later, because the person filing at 11pm before deadline is often a junior coordinator who doesn’t have it. This is reported as a major reason entrants rate the platform well. |
| Progressive registration | At the second entry, capture an email. Converts abandoned carts. Described by the client as unambiguously valuable. |
| Category recommendations | “People who entered Activism also entered Charitable & Nonprofit.” Real revenue driver, currently powered by co-entry frequency. |
What it gets wrong
| Failure | What it costs today |
|---|---|
| Configuration lives in code and schema | PropertyCategoryData carries 114 columns and 30+ boolean flags; nine worker classes carry hardcoded if (propertyID == X) branches. Adding a media type, changing an icon, editing page copy or enabling a module for a partner is a vendor release. Figures quoted for a single iteration: two to three months and $100k. |
| Entry lifecycle is a bag of booleans | ~12 independent flags on the entry row (Winner, Honoree, Nominee, Finalist, PublicVotingWinner, ShortList, Feature, HideInGallery…). Set the wrong one and entries silently vanish from public voting — the documented number-one source of confusion. |
| Dates are scattered nullable columns | Sixteen-plus date fields on the season row, read defensively by unrelated code. Stale round-end dates silently produce an impossible window and a ballot with zero entries. |
| Identity is fragmented three ways | AspNetUser, UserReviewer, PVUser — entrant, judge and voter are different people as far as the system is concerned. There are no store accounts at all; the cart is lost on exit. |
| Credits are free text, capped at ten | Winner.Credit1Name … Credit10Name. Creators can’t see their own history — raised directly in the walkthrough — and the credited-creator population is unreachable for marketing. |
| Accounts are season-scoped | A returning entrant re-enters everything every year, described by the client as the worst part of the experience. |
| No in-platform analytics | Conversion and abandonment rates were requested for the ROI case and could not be produced. |
| 19 property records for 9 programs | SignalJudging, CollisionJudging and TimeJudging exist purely to model judging portals. Dead programs can’t be hidden. |
| Five payment processors; proprietary store and checkout | Stripe, Authorize.Net, PayFlow, Amex direct, WorldPay. The checkout is described by the client as not resembling how anyone checks out in 2026. |
No tests; secrets in config; CORS * | Zero automated tests across ~1,900 files and 129 tables. |
The numbers
≈289k per day
Webby & Telly
≈2,000 concurrent
entries & store
public voting
9 entry + 9 store
begins
onboarding target
Entries and public voting have opposite profiles — steady transactional writes with zero downtime tolerance, versus a massive cacheable read spike with an hour of slack. Today they share a deployment, which means a voting surge can take down the revenue path.
03Design principles
- The program is data. Anything that differs between programs, seasons or categories is configuration with a schema, an audit trail and an admin UI — never a code branch, never a column added by a developer.
- The calendar is the control plane. Phases and transitions are modelled explicitly and emit events. No feature reads a raw date and decides for itself what phase it is in.
- People and organisations are the durable spine. Seasons come and go; the identity graph accumulates. Model it as the long-lived core, not a per-season artifact.
- The permanent record is a product. The winners archive gets a first-class service and a public API, because nine WordPress galleries, SEO and the future Directory all depend on it.
- Compose the pipeline, don’t fork it. A program enables the stages it uses. There is one implementation of each stage.
- Buy the commodity, build the domain. Commerce, CMS, identity, search and tax are solved problems. The awards domain is not, and it is what RM sells.
- Draw boundaries around audiences and domain cohesion, then check them against load and blast radius. Never around the org chart.
04The core model
4.1 Tenancy
ProgramGroup does not exist today and is the boundary that matters. The stated goals — cross-program login views, judging continuity, credits claiming, future memberships — are all scoped within a group and explicitly never across all of RM. Judges also recur within a group.
Tenant is a rename: the root was previously called Organization, which collided with Organization meaning an entrant company (§4.4). Two different things needed two different names.
Program lifecycle is explicit (draft / active / dormant / retired), which is how dead programs disappear from the admin without a developer.
4.2 The program calendar
A Season owns a Calendar: an ordered set of typed phases with transitions.
- A deadline is a value object, not a date column — carrying
publicDate,enforcedDateand an optional bypass token. The visible-vs-enforced mechanic and the backdoor both become properties of a deadline rather than special cases scattered through the code. - Phases can be scoped below the season — per media type, per category. This is how documentaries close early, as a general capability rather than a bespoke feature.
- Transitions emit domain events.
phase.entereddrives everything downstream: opening judging, activating the store, sending the announcement. Nothing polls dates. - Calendars are validated as a whole. A season with overlapping or unreachable phases fails at save time. The entire class of bug where stale round dates silently produce an empty ballot stops being possible.
Pricing attaches to phases, so “extended deadline costs $395” is a property of the calendar rather than three separate columns per category.
4.3 Work, entries and outcomes
Two deliberate changes from today:
Entry state is a lifecycle, not flags. One state field with legal transitions, replacing twelve independent booleans. Public voting eligibility becomes a query over state and program configuration rather than “does this property require Finalist or Winner OR Nominee?” — which is currently per-property tribal knowledge.
Award outcomes are a per-program taxonomy. Telly’s Silver and Bronze, Webby’s Winner and People’s Voice Winner, Anthem’s nonprofit and for-profit tracks are all AwardLevel records owned by the program, not hardcoded labels with FirstPlaceLabel columns as a half-measure.
4.4 The identity graph
One Person, one login, OIDC. Role grants scoped to (group | program | season) — a real permission model rather than four tiers reported as functionally indistinguishable in practice.
Organization here means an entrant company — an agency, PR firm or brand — and is a canonical entity with alias handling. “RGA New York” and “RGA” are the same agency; this has to be solved during migration anyway, so the registry is built once and kept. The tenancy root is Tenant (§4.1), deliberately a different name.
Credit is a first-class relationship between a Work and a Person — unbounded, optionally claimable.
- The direct answer to the question raised about creators losing their work history.
- Unblocks Webby Credits, already in beta with ~100 users, whose entire purpose is converting credited creatives with no email on file into reachable contacts.
- Unblocks the Webby Directory (2029), which is by definition a database of credited creators.
- The data degrades every year it stays as free text in ten fixed columns.
Directory and Memberships are outside the initial scope, and this should still be built now. It is a junction table; the cost is trivial and the option value is not.
Public voters are the same Person type at a lower assurance tier with a separate storage profile — millions of records RM has deliberately decided not to push into the marketing stack on cost grounds.
4.5 The permanent record
A Winners service owning the canonical awards record from 1997 to present, with a versioned public API.
Today the Webby gallery is built into the platform and the other eight are WordPress pulling an API — an accident of history that makes Webby a special case forever. Instead: one service, one API, and a gallery front end any program can enable. Whether a program renders through the platform or through WordPress becomes a delivery choice, not an architectural fork.
This service is also the natural home for the entitlements the store needs — what did this person win, and what may they therefore buy — which is what keeps store access control out of Shopify.
4.6 Multi-brand delivery
Brand is data: theme tokens, assets, copy, domain and email templates, resolved per program. One rendering pipeline, no per-brand code, no RM branding anywhere.
RM keeps editorial control of brand assets — partners have explicitly said they don’t want that responsibility — so the theming admin is an RM-side tool, not a partner-facing one. A neutral default theme exists so a new program is presentable on day one.
4.7 The model in one view
Configuration above the rule, participation below. Everything above is stable and admin-managed; everything below accumulates as people take part. The two meet at exactly two points — an entry references a category, and an award references an award level.
05Topology
5.1 How the boundaries are chosen
Load profile alone is too thin a basis. A boundary earns its place against six tests.
| Test | Question |
|---|---|
| Audience | Is this a distinct user population with its own surface and auth posture? |
| Cohesion | Does it own a cluster of data and rules the rest of the system mostly doesn’t touch? |
| Cadence | Does it change on its own schedule, and can a team own it end to end? |
| Load | Is the traffic shape materially different? |
| Blast radius | Should a failure here be prevented from reaching there? |
| Longevity | Does it need to outlive, or precede, the rest of the system? |
A candidate needs several of these, not one. Applied honestly, the tests say no as often as yes — see §5.3.
5.2 Services
Five domain services, plus shared platform concerns.
| Service · audience | Why it splits |
|---|---|
| Program Administration RM staff — program managers, customer service | Distinct internal audience of tens of users. Owns tenancy, program lifecycle, the calendar, catalog, pricing, configuration, users and permissions. Highest privilege in the system and the highest rate of change. Physical separation lets the whole admin plane sit behind network controls and SSO the public surfaces never get — the structural answer to “everyone can access the user manager and delete people.” |
| Submissions & Commerce Entrants — agencies, PR firms, creators | The revenue path. Zero downtime tolerance, ~3,000 entries and ~2,000 concurrent users on a deadline day. Owns work, entries, credits, cart, checkout. Isolated so nothing else can take it down. |
| Judging Jurors | Distinct invited population that never pays and never sees the rest of the platform. Owns the densest rules in the system — clusters, assignment, criteria, scoring, rounds, executive ballots — and a data cluster (17 tables today) nothing else writes. Runs in its own calendar window, on its own cadence, with a mobile-first surface. It is also the least understood module, so an isolated blast radius during discovery and rebuild is worth a lot. |
| Public Voting The public | 4.6M votes in sixteen days against an otherwise modest baseline. One hour of downtime tolerance against zero for entries. Read-dominant and cacheable against write-heavy and transactional. Dormant most of the year. Queue-based ingestion, CDN-cached ballots, its own datastore. |
| Awards & Winners Public, WordPress, crawlers, future Directory | The permanent record, 1997→present. Read-dominant, heavily crawled, consumed by eight WordPress galleries through a public API. Mostly append-only once a season closes. Must outlive everything else and must be stood up first — §9 puts it in front of legacy data so the galleries cut over once rather than nine times. That plan only works if it is genuinely separate. |
It is how the business already talks about itself. The current platform ships seven separate frontends, and the subdomain structure already reads entries.*, judging.*, voting.*, store.*. The frontends are already split along these lines; only the backend isn’t.
5.3 What does not split
A framework that only says yes is not a framework.
- Entry checkout stays inside Submissions. Cart items are entries, pricing comes from category plus calendar phase, and eligibility is rule-driven. Splitting it puts a network boundary and a distributed transaction across the revenue path. Payment is delegated to Stripe; the checkout itself is not a separate service.
- Customer service is a role, not a service. CS works on submissions data and program configuration. It needs serious UX attention inside Program Administration — it was the tool most heavily exercised during the walkthrough — but its own deployable would just be a second window onto someone else’s data.
- Content is bought, not built. A headless CMS consumed directly by each frontend. No service of ours in the middle.
- Reporting is a sink, not a service. Events land in Snowflake; operational dashboards live in Program Administration.
- Nominee Center folds into Submissions or Awards depending on where discovery lands.
5.4 Reconciling this with team size
Five coarse services aligned to bounded contexts is not microservices. The standard objection — a small team drowning in operational overhead — applies to dozens of fine-grained services with chatty synchronous calls and distributed transactions. It does not apply here. Each of these is substantial, owns its data, and has a clear reason to exist.
The costs are real and worth naming:
- Configuration has to reach everyone. Program Administration is the sole writer and publishes change events; every service keeps a local read cache. Eventually consistent, which is correct — season configuration does not change sub-second.
- Entry data is needed downstream. Judging needs assignable entries, Voting needs finalists, Winners needs awarded entries. Each owns a read projection fed by events from Submissions, holding only the fields it needs — not a shared table and not a synchronous call.
- Six deployables need CI, observability and on-call. Correlation IDs and distributed tracing from day one, not retrofitted.
- A badly cut seam costs more than a monolith. Mitigated by the fact that these boundaries are evidenced three times over — by audience, by the existing frontend split, and by the existing data clusters.
Each service owns its schema, no cross-schema access. For a team this size, start with one Postgres cluster, separate schemas, boundaries enforced in code and review — operational cost near a monolith’s, with a clean physical split available later if any service outgrows it. Public Voting is the exception and gets its own store from the start, because its load and availability profile is the reason it exists as a service.
One further argument for this shape: Memberships and the Directory become additive. Both are future initiatives consuming Identity and Winners. Under this topology they are new services against stable contracts. Under a monolith they are surgery.
5.5 Absorbing the voting peak
4.6M over 16 days is an average. Voting is deadline-driven, and deadline-driven participation is heavily back-loaded — a large share of the total lands in the final day, and a large share of that in the final hours, amplified by the “voting closes tonight” mail RM will certainly send.
Working the back-loading through, the write path peaks somewhere in the low hundreds per second. Even that is not the hard part. The hard part is reads. Every voter loads ballots and browses nominee media, so a final-day surge of several times the average user count produces millions of page and asset requests against video and image content. Vote writes are a rounding error next to that.
Four consequences:
- Accept votes, don’t process them. Queue-based ingestion: validate cheaply, acknowledge immediately, aggregate asynchronously. The queue absorbs the spike so nothing downstream has to scale with it. This is what makes the final hour a non-event rather than the annual outage.
- Votes must be idempotent per
(voter, nominee, round). A spike means retries and double-taps, and duplicate votes are a credibility problem, not just a data problem. - Aggregate by stream, never by counting rows. Tallies come from the aggregation pipeline. Counting a multi-million-row table during the close is exactly the wrong thing — the current 50-minute statistics cache is a blunt version of this instinct.
- Serve ballots and nominee media from the edge with short TTLs. Rate limiting and bot defence belong at the edge too, since that is where abuse concentrates during a close.
Autoscaling responds to load that has already arrived, which on a sixteen-days-then-nothing profile means the first minutes of the surge are the worst-served. But the close time is known — it lives in the program calendar. phase.ending-soon can drive deterministic pre-warming ahead of the deadline. A direct dividend of treating the calendar as the control plane rather than a set of date columns.
Load-test against the modelled peak, not the average — and get the real distribution first (§10).
5.6 Information flow
Two planes, deliberately separated.
No service makes a synchronous call to another service. Synchronous paths run only frontend → its own service, or service → a shared or bought dependency. Everything between services travels as events, and each consumer holds its own read projection — Judging never queries Submissions for an entry, it holds the assignable-entry projection it built from entry.paid.
Event catalogue
| Publisher | Event | Consumers |
|---|---|---|
| Program Admin | config.changed | Submissions, Judging, Voting, Winners — local caches |
| Program Admin | phase.entered | Submissions (open/close entry), Judging (open round), Voting (open ballot), Winners (publish), Notifications |
| Program Admin | phase.ending-soon | Infrastructure pre-scaling (§5.5) |
| Submissions | entry.submitted · entry.paid | Judging (assignable projection), Salesforce (lead), Snowflake |
| Submissions | cart.abandoned | Notifications (recovery), Snowflake |
| Submissions | order.completed | Notifications (receipt), Snowflake |
| Judging | entry.scored · entry.shortlisted | Snowflake |
| Judging | entry.finalist | Voting (ballot projection), Winners |
| Judging | entry.awarded | Winners (the record), Notifications |
| Public Voting | vote.cast | Snowflake, aggregated |
| Public Voting | pv.winner.determined | Winners |
| Awards & Winners | award.published | Notifications, OpenSearch index, Snowflake |
| Shopify (webhook) | merch.order.completed | Winners (purchase history), Snowflake |
06Stack
6.1 Runtime is a per-service decision
Once the topology is five services, a single runtime row in a table is misleading. Runtime should be chosen per service against that service’s actual profile.
Voting is heavily back-loaded (§5.5), so the daily average badly understates real demand. But working the back-loading through, the write path still lands in the low hundreds per second at the worst minute — unremarkable for any mainstream runtime, and close to irrelevant once ingestion is queue-based, at which point the write path is validate-and-enqueue. The genuinely hard parts of the peak are edge caching, idempotency and stream aggregation: architecture, not runtime.
The argument for a lightweight runtime on Public Voting is therefore elasticity and cost — and the peakiness strengthens it: the service must scale hard and fast ahead of a known close, then back to near nothing for months. Small, fast-starting, cheap-to-run instances suit that shape. The same reasoning applies more weakly to Awards & Winners, which is read-dominant and sits behind a CDN doing most of the real work.
| Service | Runtime | Reasoning |
|---|---|---|
| Program Administration | .NET | Business-rule and configuration dense. Shares model code with Submissions. |
| Submissions & Commerce | .NET | Where the ported business rules concentrate — pricing resolution, eligibility, entry lifecycle. The hardest logic to recover, and where continuity is worth most. |
| Judging | .NET | The densest rules in the system. An assignment algorithm balancing eight constraints is a correctness problem, not a throughput one. |
| Public Voting | Go (candidate) | Justified on elasticity and cost rather than throughput. Small, self-contained surface: ballots, votes, aggregation, rate limiting. Low rule density, so little is lost by leaving the shared model. |
| Awards & Winners | .NET, or Go if PV proves the pattern | Read-dominant and CDN-fronted. Defensible either way; not worth a second runtime on its own. |
| Frontends | TypeScript — React and Next.js | React for admin, judging and submissions. Next.js SSR for gallery, voting and entry landing — removes the prerender dependency and lifts SEO on an archive going back to 1997. |
Cap production at two backend runtimes. Every additional runtime is a permanent tax on hiring, tooling, CI, observability and on-call — paid forever, in exchange for a one-time gain. Two is enough to put a lightweight runtime where elasticity genuinely pays without fragmenting the team.
On Rust specifically — it would be the right call if this were a genuine high-throughput ingestion problem. At tens of writes per second it buys efficiency the workload does not need, against a materially smaller hiring pool. Worth revisiting only if vote volumes grow by an order of magnitude, or if fraud detection becomes CPU-bound.
On staffing being neutral — it is neutral on RM’s side, since there is no in-house engineering team and no incumbent stack to protect. It is not neutral on the delivery side: whoever maintains this bears the polyglot cost, and RM has no capacity to absorb it themselves. That argues for the same cap, for a different reason.
The residual argument for .NET across the rule-dense services stands on one specific ground: eleven years of business rules exist only as C#, they are the hardest input in the whole project to recover, and people who can read the old system while writing the new one are valuable throughout the rebuild. That is a transition argument with a shelf life, not a permanent one.
6.2 Shared layers
| Layer | Recommendation | Reasoning |
|---|---|---|
| Database | PostgreSQL — one cluster, schema per service | JSONB suits the typed configuration store; strong indexing for entry queries; no licensing. Public Voting gets its own store. |
| Data access | EF Core, code-first, migrations in repo | Today’s schema changes happen out-of-band via Red Gate and a tracking sheet. Migrations in version control are a prerequisite for phased cutover. |
| Events | Managed broker | Carries config propagation, entry projections and analytics. |
| Identity | Managed OIDC | Includes the social login entrants already use. |
| CMS | Headless, consumed directly by frontends | Every string, label and outbound link on platform pages. |
| Merch store | Shopify, headless storefront | §7.2 — gating needs validating. |
| Payments | Stripe, per-program accounts | Accounts are to be siloed for audit; to be confirmed. |
| Search | OpenSearch | Gallery search and autocomplete. |
| Media | S3 + CloudFront + Imgix; encoding provider TBC | See §10. |
| Customer.io | Confirmed retained. | |
| CRM | Salesforce, event-driven sync | AIT wants the recent Salesforce investment preserved. Direct sync rather than an opaque middleware layer. |
| Analytics | Domain events → Snowflake, alongside Segment | §8. |
| Hosting / CI | AWS containers with autoscaling · GitHub Actions | Public voting autoscaling is manual today. |
07Commerce
There are two commerce surfaces and they are not the same problem.
Conflating them is the most expensive mistake available in this rebuild.
7.1 Entry commerce — build it
Entry submission looks like a shopping cart and is not one.
The tempting mapping is: each category becomes a product, each deadline tier becomes a variant, the entrant checks out with several variants in the cart. It is a reasonable first instinct and it does not survive contact with the domain.
| Retail commerce assumes | Entry submission actually does |
|---|---|
| A catalogue of products that persists | A category tree re-cut every season — the single biggest editorial change year to year |
| The buyer chooses the variant | The price tier is decided by when checkout happens, not by any buyer choice |
| Price is an attribute of the SKU | Price is a function of (category, calendar phase, entrant status), resolved at cart evaluation |
| A line item is a product | A line item is an entry — a Work bound to a Category, carrying media, URLs and credits |
| Cart contents are stable | A cart can cross a deadline boundary mid-session and legitimately reprice |
| Upsell is merchandising | “Have you considered these categories” is a domain recommendation over the category graph |
| Fulfilment ships a thing | “Fulfilment” is an entry entering the judging pipeline |
Webby alone runs to several hundred categories. Across nine properties on staggered season calendars that is thousands of products created and archived every year by a sync pipeline sitting directly on the revenue path. Pricing would live in two systems and the category tree — which Program Administration owns — would be the source of truth in neither. Variants would encode a temporal condition rather than a buyer choice, so the storefront would have to force the selection rather than offer it. And a cart left open across a deadline transition would be wrong in whichever system failed to reprice.
So build it — but build far less than “build it” implies.
What gets built
Only the part that is genuinely ours:
- A pricing resolver, not a price column.
resolvePrice(category, phase, entrantStatus)evaluated when the cart is read. The category tree stays the single source of truth. - Cart lines reference entries.
CartLine = { entryId, resolvedPrice, priceBasis: { phase, priceSetId, resolvedAt } }. - Re-resolve at checkout and snapshot the basis onto the order line for audit. If the phase moved while the cart sat open, surface it explicitly rather than silently charging either price.
- Comped and reduced-price entries are a
priceOverridewith a reason code, not a parallel SKU. This is what the currentIsFreeflag and reduced-price categories become. - Eligibility rules — which categories this entrant may enter, given media type, program and season.
That is a few hundred lines of domain logic, not a commerce platform.
What Stripe already provides
Line items can be constructed per transaction rather than drawn from a stored catalogue. The price is resolved at cart evaluation and the computed lines — name, description, amount — are passed to Stripe at the moment of checkout. There is no catalogue to create, sync or archive. That single capability removes the objection above while still providing a maintained checkout.
| Capability | Provider | Replaces / fixes |
|---|---|---|
| Checkout UI, embedded and themed per program | Payment Element / embedded Checkout | The proprietary checkout the client describes as dated. Maintained, accessible, mobile-first, localised — and it inherits new payment methods without additional work |
| Address capture, autocomplete, validation | Address Element | The zip-code validation failures driving CS tickets, and the missing copy-billing-to-shipping |
| Wallets — Apple Pay, Google Pay, Link | Stripe | One-tap checkout. Directly attacks the returning-entrant problem alongside persistent identity |
| Saved payment methods | Stripe | Returning entrants stop re-entering card details |
| Sales tax and EU VAT | Stripe Tax | Retires TaxJar. Matters for Lovie |
| Discount codes and promotions | Coupons / promotion codes | The entry discounting they want and have never had. The domain decides eligibility; Stripe applies the mechanics |
| Invoicing for check and wire | Stripe Invoicing | Today this is manual. Proper invoices, payment tracking, reconciliation |
| Recurring billing | Stripe Billing | Signal’s bespoke subscription layer, currently custom for one property |
| Receipts, payment history, self-service | Customer Portal | CS load |
| Fraud screening | Radar | Sits alongside existing bot protection |
Embedded rather than hosted, because nine programs must feel like independent organisations and a recognisably third-party checkout page works against that. Confirm the current feature set and pricing against Stripe’s documentation during discovery — this list is the shape of the answer, not a contract.
Checkout experience — what changes for the entrant
The current flow was demonstrated failing in several specific ways. Fixes, in rough order of value:
- One clear action per step. The two calls to action at the foot of the cart were not distinguishable during the walkthrough.
- Collapse the entry list. Summary rows that expand, replacing the endless vertical scroll that appears once someone enters more than a handful of pieces.
- Surface the deadline in the cart. A countdown and an explicit “prices rise when the standard deadline passes.” This turns the repricing edge case into a conversion lever — the deadline structure already drives their revenue, and the checkout currently hides it.
- Copy billing to shipping. Raised directly as a gap.
- Server-side cart persistence for everyone, not a 24-hour browser cache for anyone who hasn’t registered.
- Wallets and saved cards for returning entrants, on top of persistent identity.
- Automated abandoned-cart recovery. They already do this manually off the quick-registration email; events plus Customer.io make it automatic.
- Mobile-first throughout. AIT lists it as a must-have and the current flow is desktop-first.
7.2 Merchandise — buy it
The winners’ store is a genuinely different problem and a good fit for Shopify: a small catalogue of physical goods per season, real shipping, real inventory, real tax, real fulfilment. It retires the FedEx rate integration, TaxJar, the PDF generation service and a large proprietary checkout.
Two things need validating in discovery before this is committed:
- Entitlement gating. You may only buy the trophy for the category you actually won. That is not native Shopify — it needs a headless storefront querying entitlements from the Winners service and rendering only eligible products. Personalisation (award level, category, two engraving lines) maps cleanly onto line-item properties, but combined with gating it means a custom storefront rather than the stock one.
- Nine stores. Nine properties means nine storefronts, with the licensing and administration that implies. Worth pricing against a Shopify Plus arrangement.
If gating proves awkward in discovery, the fallback is a custom storefront over a commerce engine with Stripe plus a tax and shipping service — losing the admin and fulfilment tooling but keeping full control of eligibility. Decide this on evidence, not in the SOW.
Do not carry ten years of historical catalogues in as live products. Archive them; drive “your wins” from the Winners service; let the store hold a current catalogue. This answers a concern raised directly and removes what was named as a major migration cost.
7.3 Everything else
Buy CMS, identity, search, email and tax. Build the program model, calendar, submissions, entry commerce, judging, public voting and the winners record. That list is the actual product.
08Data and reporting
Domain events from day one — phase.entered, entry.started, category.selected, cart.abandoned, checkout.step.completed, payment.failed, award.published, vote.cast — landed in Snowflake alongside the existing feeds.
RM currently cannot report conversion or abandonment, which is why those figures could not be produced for the ROI case. It also means the rebuild can be evaluated against the outcome AIT names first: higher entry conversion.
In-platform operational dashboards for program managers — entries by category, revenue pacing against deadline phases, judging completion — so daily numbers don’t require the BI team.
Partner data access needs a real answer. The current control is a 50-row export throttle, and the Architizer episode — a partner who took the entry data and launched a competing award — is why anyone cares. Scoped, audited, rate-limited API access with row-level tenancy enforcement.
09Getting there
Strangler by program, which matches the phased rollout the client has already asked for.
Sequence. A fall 2027 program first — Communicator or another AIVA property: modest volume, single judging round, no live event, no native gallery. Webby last, because it carries every stage of the pipeline plus the live event and the highest volume. Cutover windows come from the 18-month season calendar, still outstanding.
Route at the edge by subdomain. entries.communicatorawards.com to the new platform, entries.webbyawards.com to the old, both serving the same winners API.
Stand up the Winners service first. It is the public contract eight WordPress galleries already consume. Put it in front of legacy data early so those galleries cut over once rather than nine times, and so the permanent record is under new management before anything else moves.
| Tier | Contents | Treatment |
|---|---|---|
| Full fidelity | Winners 1997→present · people and organisations · current and recent entries · in-flight orders · order history | Migrate completely. The winners record is both a brand asset and a live API contract. |
| Reduced fidelity | Historical orders · historical credits · judging history | Migrate through the Person/Organization extraction and dedup pass. |
| Archive only | Abandoned carts · logs · retired programs · duplicate *Judging records | Cold archive, customer-service lookup only. It has been accepted that the oldest data may be CS-accessible rather than surfaced in accounts. |
Build the safety net before porting. There are no inherited tests, no known-issues list, and the handoff is likely to be light. Characterisation tests against the legacy API, written before the corresponding logic is rebuilt, so new behaviour can be diffed against live. This is the mechanism that makes phased cutover verifiable, and the concrete case for the dedicated QA capability the client has asked for.
Segmentation for estimation
Rough dependency order. Sizing figures are out of scope here; this table is the structure they attach to.
| Segment | Sizing note | |
|---|---|---|
| 00 | Discovery: judging + public voting | Blocks defensible estimates on the two biggest unknowns |
| 01 | Tenancy, program lifecycle, configuration store | Foundation |
| 02 | Calendar engine + phase events | Everything downstream is driven by it |
| 03 | Identity, RBAC, Person / Organization / Credit | Includes dedup strategy |
| 04 | Configuration extraction from the legacy model | Largest pure-analysis task — 114 columns, 30+ flags, 9 property branches, each needing a keep/retire/promote decision |
| 05 | Content service + CMS | Highest ticket-volume relief, low complexity |
| 06 | Catalog: media types, categories, pricing, cloning | |
| 07 | Submissions + entrant experience | |
| 08 | Commerce: entry checkout + Stripe | |
| 09 | Store on Shopify | Includes entitlement gating and personalisation |
| 10 | Judging | Very high, least understood |
| 11 | Public voting service | Scale-driven; own datastore |
| 12 | Winners service + public API + gallery | Do first — precedes everything, unblocks WordPress cutover |
| 13 | Event stream + reporting | |
| 14 | Data migration, all tiers | Spans the programme, not a phase |
| 15 | Characterisation harness + QA | Prerequisite for cutover |
10Open questions
For Recognition Media
- 01Encoding provider. Media encoding is mission-critical — if it fails, judges cannot review and the gallery breaks — but it is absent from the technical analysis integrations table. Confirm the current arrangement and whether to carry it or move to AWS MediaConvert.
- 02Salesforce sync. The current middleware’s data flows are undocumented, including on RM’s side. Needs mapping before a replacement is committed to.
- 03Season calendar, 18 months. Outstanding, and it gates migration window planning.
- 04Stripe per-program accounts. Confirm whether legal and audit genuinely require siloing; it shapes the payment integration.
- 05Lovie and GDPR. Moved
.eu→.comand relocated to the US; likely out of EU residency scope, unconfirmed. - 06Judging model per program. Which programs use clusters, how executive rounds differ, what the conflict-of-interest rules actually are. Needed before segment 10 can be sized.
- 07Hourly vote and traffic distribution for the 2026 voting period. Segment should have it. The daily average is useless for sizing; what matters is the shape of the final 48 hours, the peak hour, and the read-to-write ratio, in order to load-test against something real rather than a guess (§5.5).
- 08Whether live vote tallies are shown to the public during voting. It changes the read architecture substantially, and the current 50-minute statistics cache suggests the answer is “sort of.”
For the delivery team
- 09Judging is the largest estimate risk. Not demoed, “requires deeper discovery” in AIT’s own assessment, rated very high migration difficulty, and carrying the most intricate logic in the system — an assignment algorithm balancing completion count, availability, language, category grouping, score proximity, conflict of interest, device compatibility and a consecutive-review cap. No number is defensible without its own session.
- 10Configuration extraction is the hidden schedule risk. Eleven years of accreted flags across nine programs. Under-scoping this is the most likely way the estimate goes wrong.
- 11Runtime choice is a staffing decision. §6 recommends .NET on the strength of business-rule recovery, not sentiment. If the ten-year maintenance story points elsewhere, the architecture is unaffected — worth settling deliberately rather than by default.
11In brief
- Build a factory for awards programs, not an awards platform. Nine today, ~11 in five years, each a configured variant of one pipeline.
- Model the calendar as the control plane — typed phases, validated transitions, events. Deadlines carry a visible and an enforced date.
- Program configuration is data with a schema, an audit trail and an admin UI.
- Entry state is a lifecycle; award levels are per-program vocabulary. No boolean flag bags.
- People and organisations are the durable spine.
Creditis first-class and unbounded from day one — it is what unblocks Webby Credits and the Directory. - The winners record from 1997 is a product, with one service and one public API serving every program.
- Five services drawn on audience and cohesion — Program Administration, Submissions & Commerce, Judging, Public Voting, Awards & Winners — with shared identity, media and events. One Postgres cluster, schema per service, to start.
- Two commerce surfaces, two answers. Build entry commerce — price is a function of category and calendar phase, not a SKU attribute. Buy the merch store.
- Runtime is per-service, capped at two. .NET where the business rules concentrate; a lightweight runtime for Public Voting on elasticity grounds, not throughput.
- Strangler by program — smallest first, Webby last, Winners service first.
- Write characterisation tests before porting. Nothing is inherited.