Recognition Media · 2027 Platform Rebuild

Rebuild Architecture

Recognition Media is not an awards platform. It is a factory for awards programs — and the architecture should say so.

Date 30 July 2026 Status Draft — internal review Scope Target architecture & technology stack
Scope — target architecture and technology stack for the 2027 platform rebuild.
Out of scope — the CDM support engagement, which runs on separate assumptions. Timeline and cost estimates; §9 provides the work segmentation those depend on.
Sources — AIT/RM Current Platform & Functionality DEMO (22 Jul, 1h31m — platform walkthrough with the client) · AIT – Recognition Media Platform Assessment · RM Platform — Complete Technical Analysis [Vendor Copy] · AIT/RM Q&A notes (28 Jul) · #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.

DEFINE INTAKE ADJUDICATE AMPLIFY FULFIL RECORD seasons · categories pricing · calendar entries payment juries · scoring rounds public voting trophies · certificates tickets the winners archive 1997 → present transient — one season cumulative — compounds
Fig. 1 — The awards pipeline. Each program runs some subset of the six stages.
  • Webby runs all six, with two judging rounds and a live event.
  • Time runs DEFINE → INTAKE only.
  • 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.

It is calendar-driven end to end

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.

Its output is permanent

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.

ConceptWhy it works
Season cloning as the primary creation pathClone 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 deadlineMarketing says Friday; the system actually closes Sunday night. A deliberate, revenue-positive mechanic — and not something you would invent from first principles.
Backdoor entryProgram shows as closed publicly, but a private link still accepts entries from people the team is actively courting.
Per-media-type early closeDocumentaries 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 separationOne 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 creditsSubmit 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 registrationAt 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

FailureWhat it costs today
Configuration lives in code and schemaPropertyCategoryData 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 columnsSixteen-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 waysAspNetUser, 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 tenWinner.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-scopedA returning entrant re-enters everything every year, described by the client as the worst part of the experience.
No in-platform analyticsConversion and abandonment rates were requested for the ROI case and could not be produced.
19 property records for 9 programsSignalJudging, CollisionJudging and TimeJudging exist purely to model judging portals. Dead programs can’t be hidden.
Five payment processors; proprietary store and checkoutStripe, 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

4.6M
PV votes / 16 days
≈289k per day
14,000
Peak entries / season
Webby & Telly
~3,000
Peak entry day
≈2,000 concurrent
Zero
Downtime tolerance
entries & store
1 hr
Downtime tolerance
public voting
18
Storefronts
9 entry + 9 store
1997
Permanent record
begins
Days
New program
onboarding target
The asymmetry that drives topology

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. Compose the pipeline, don’t fork it. A program enables the stages it uses. There is one implementation of each stage.
  6. 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.
  7. 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

Tenant Recognition Media — one today; modelled so a partner-operated instance stays possible ProgramGroup new Webby Group · AIVA Group · Telly · Signal · Partner Awards Program Webby · Lovie · Anthem · Davey · W3 · Communicator Telly · Signal · Time · Magellan Season one instance of a program — its own calendar and catalogue MediaType CategoryType Category entries attach here 1 : n 1 : n 1 : n 1 : n ProgramGroup is the scope for cross-program login, judging continuity and credits claiming — never all of RM
Fig. 2 — Tenancy. The group level does not exist today and is the boundary that matters.

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 · Containment Season seasonNumber year programId Calendar timezone phases[] ordered validated as a whole Phase kind · order scope: season | mediaType | category « value object » Deadline publicDate enforcedDate bypassToken? 1 : 1 1 : n 0 .. 1 No identity, no lifecycle of its own — it exists only as part of the Phase that holds it. The visible-vs-enforced mechanic and the backdoor link stop being special cases and become two fields and a token on one object. A Phase scoped to a media type is how documentaries close early. B · Phase sequence registration early-entry standard-entry extended-entry closed ↑ each carries a Deadline judging-rnd-1 judging-rnd-2 public-voting winners-announced store-open phase.entered Every transition emits an event. Nothing downstream polls a date — judging opens, the ballot opens and the store activates on the event. Pricing attaches to phases, so “extended costs $395” is calendar data, not three price columns per category.
Fig. 3 — The season calendar. A Deadline is a value object held by a Phase, not a date column on the season row.
  • A deadline is a value object, not a date column — carrying publicDate, enforcedDate and 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.entered drives 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

A · Structure Person / Organization Work title · media · URLs customFields Credit role · person? · unbounded Entry state resolvedPrice Category Award 0 .. 1 per entry AwardLevel 1 : n submits 1 : n one per category 0 .. 1 1 : n n : 1 n : 1 0 .. 1 optional claim per-program vocabulary — Silver · Bronze · People’s Voice B · Entry lifecycle — one state field, legal transitions, no flag bag draft submitted paid processed assigned scored shortlisted finalist awarded Submissions owns Judging owns Winners owns Public-voting eligibility becomes a query over state and program configuration — replacing twelve independent booleans where setting the wrong one silently drops an entry from the ballot.
Fig. 4 — Work, entries and outcomes. The lifecycle band shows which service owns each transition.

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.

Highest-leverage decision in the document

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.

Configuration — admin-managed, stable Tenant ProgramGroup new Program Theme Season Calendar MediaType Phase CategoryType « value object » Deadline Category AwardLevel 1 : n 1 : n 1 : n 1 : 1 1 : n 1 : 1 1 : n 1 : n 0 .. 1 1 : n 1 : n Participation — accumulates Entry Award Work Review Vote Credit Person Organization n : 1 entered into 1 : n 0 .. 1 1 : n n : 1 n : 1 1 : n 1 : n 0 .. 1 0 .. 1 n : 1 juror n : 1 voter — low assurance Credit is unbounded and optionally claimable — not ten fixed columns on the winner row.
Fig. 5 — Domain model. Dashed borders mark the value object and the separately-deployed voting entities.

05Topology

5.1 How the boundaries are chosen

Load profile alone is too thin a basis. A boundary earns its place against six tests.

TestQuestion
AudienceIs this a distinct user population with its own surface and auth posture?
CohesionDoes it own a cluster of data and rules the rest of the system mostly doesn’t touch?
CadenceDoes it change on its own schedule, and can a team own it end to end?
LoadIs the traffic shape materially different?
Blast radiusShould a failure here be prevented from reaching there?
LongevityDoes 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.

RM staff Entrants Jurors The public Public · WordPress Program Admin tenancy · lifecycle calendar · catalog config · users · CS .NET internal · low volume Submissions & Commerce work · entry · credits cart · pricing · checkout .NET zero downtime · revenue Judging clusters · assignment criteria · scoring rounds · ballots .NET own calendar window Public Voting ballots · votes aggregation rate limiting Go (candidate) 4.6M / 16 days · own store Awards & Winners the record, 1997→ public API gallery .NET read-dominant · CDN Shared platform Identity managed OIDC · RBAC Media upload · encoding · delivery Notifications transactional email Event bus config · projections · analytics Persistence PostgreSQL — one cluster schema per service · no cross-schema access Public Voting store separate from day one OpenSearch gallery search Bought Shopify merch only Stripe per-program accounts Headless CMS copy · labels · links Salesforce event-driven sync Snowflake events · reporting
Fig. 6 — Service topology. Dashed border marks the one service with its own datastore from day one.
Service · audienceWhy 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.
This decomposition is not novel to RM

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.
On the database

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

The average is the wrong tool

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.
Pre-scale rather than react

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.

The rule that makes the topology work

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.

RM staff Entrants Jurors Public Public · WordPress Program Admin tenancy · calendar catalog · config Submissions work · entry · credits cart · checkout Judging assignment · scoring rounds · ballots Public Voting ballots · votes aggregation Awards & Winners the record, 1997→ public API · entitlements config phase entry order scores outcomes votes pv winners award published E V E N T B U S Snowflake every event · reporting Salesforce entrants as leads Notifications → Customer.io receipts · recovery · announcements Synchronous dependencies — called by services and frontends Identity OIDC · person · org all services + frontends Media upload · encode · CDN Submissions · Judging · Winners Stripe payment · tax · invoicing Submissions Shopify merch catalogue store frontend OpenSearch gallery index Winners Headless CMS copy · labels · links all frontends Two paths worth naming Judging ──▶ Media jurors cannot review if encoding is unavailable Store frontend ──▶ Winners ──▶ Shopify eligibility resolved in the domain, never by Shopify synchronous request / response asynchronous event
Fig. 7 — Information flow. Services never call each other synchronously; all inter-service data moves through the bus as events, and each consumer holds its own read projection.
Event catalogue
PublisherEventConsumers
Program Adminconfig.changedSubmissions, Judging, Voting, Winners — local caches
Program Adminphase.enteredSubmissions (open/close entry), Judging (open round), Voting (open ballot), Winners (publish), Notifications
Program Adminphase.ending-soonInfrastructure pre-scaling (§5.5)
Submissionsentry.submitted · entry.paidJudging (assignable projection), Salesforce (lead), Snowflake
Submissionscart.abandonedNotifications (recovery), Snowflake
Submissionsorder.completedNotifications (receipt), Snowflake
Judgingentry.scored · entry.shortlistedSnowflake
Judgingentry.finalistVoting (ballot projection), Winners
Judgingentry.awardedWinners (the record), Notifications
Public Votingvote.castSnowflake, aggregated
Public Votingpv.winner.determinedWinners
Awards & Winnersaward.publishedNotifications, OpenSearch index, Snowflake
Shopify (webhook)merch.order.completedWinners (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.

And chosen honestly — including the peak

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.

ServiceRuntimeReasoning
Program Administration.NETBusiness-rule and configuration dense. Shares model code with Submissions.
Submissions & Commerce.NETWhere the ported business rules concentrate — pricing resolution, eligibility, entry lifecycle. The hardest logic to recover, and where continuity is worth most.
Judging.NETThe densest rules in the system. An assignment algorithm balancing eight constraints is a correctness problem, not a throughput one.
Public VotingGo (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 patternRead-dominant and CDN-fronted. Defensible either way; not worth a second runtime on its own.
FrontendsTypeScript — React and Next.jsReact 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.
Policy

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

LayerRecommendationReasoning
DatabasePostgreSQL — one cluster, schema per serviceJSONB suits the typed configuration store; strong indexing for entry queries; no licensing. Public Voting gets its own store.
Data accessEF Core, code-first, migrations in repoToday’s schema changes happen out-of-band via Red Gate and a tracking sheet. Migrations in version control are a prerequisite for phased cutover.
EventsManaged brokerCarries config propagation, entry projections and analytics.
IdentityManaged OIDCIncludes the social login entrants already use.
CMSHeadless, consumed directly by frontendsEvery string, label and outbound link on platform pages.
Merch storeShopify, headless storefront§7.2 — gating needs validating.
PaymentsStripe, per-program accountsAccounts are to be siloed for audit; to be confirmed.
SearchOpenSearchGallery search and autocomplete.
MediaS3 + CloudFront + Imgix; encoding provider TBCSee §10.
EmailCustomer.ioConfirmed retained.
CRMSalesforce, event-driven syncAIT wants the recent Salesforce investment preserved. Direct sync rather than an opaque middleware layer.
AnalyticsDomain events → Snowflake, alongside Segment§8.
Hosting / CIAWS containers with autoscaling · GitHub ActionsPublic 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 assumesEntry submission actually does
A catalogue of products that persistsA category tree re-cut every season — the single biggest editorial change year to year
The buyer chooses the variantThe price tier is decided by when checkout happens, not by any buyer choice
Price is an attribute of the SKUPrice is a function of (category, calendar phase, entrant status), resolved at cart evaluation
A line item is a productA line item is an entry — a Work bound to a Category, carrying media, URLs and credits
Cart contents are stableA 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
What forcing it would cost

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 priceOverride with a reason code, not a parallel SKU. This is what the current IsFree flag 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

The capability that settles it

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.

CapabilityProviderReplaces / fixes
Checkout UI, embedded and themed per programPayment Element / embedded CheckoutThe proprietary checkout the client describes as dated. Maintained, accessible, mobile-first, localised — and it inherits new payment methods without additional work
Address capture, autocomplete, validationAddress ElementThe zip-code validation failures driving CS tickets, and the missing copy-billing-to-shipping
Wallets — Apple Pay, Google Pay, LinkStripeOne-tap checkout. Directly attacks the returning-entrant problem alongside persistent identity
Saved payment methodsStripeReturning entrants stop re-entering card details
Sales tax and EU VATStripe TaxRetires TaxJar. Matters for Lovie
Discount codes and promotionsCoupons / promotion codesThe entry discounting they want and have never had. The domain decides eligibility; Stripe applies the mechanics
Invoicing for check and wireStripe InvoicingToday this is manual. Proper invoices, payment tracking, reconciliation
Recurring billingStripe BillingSignal’s bespoke subscription layer, currently custom for one property
Receipts, payment history, self-serviceCustomer PortalCS load
Fraud screeningRadarSits 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:

  1. One clear action per step. The two calls to action at the foot of the cart were not distinguishable during the walkthrough.
  2. Collapse the entry list. Summary rows that expand, replacing the endless vertical scroll that appears once someone enters more than a handful of pieces.
  3. 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.
  4. Copy billing to shipping. Raised directly as a gap.
  5. Server-side cart persistence for everyone, not a 24-hour browser cache for anyone who hasn’t registered.
  6. Wallets and saved cards for returning entrants, on top of persistent identity.
  7. Automated abandoned-cart recovery. They already do this manually off the quick-registration email; events plus Customer.io make it automatic.
  8. 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.

Why this is not a nice-to-have

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.

First thing to build

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.

TierContentsTreatment
Full fidelityWinners 1997→present · people and organisations · current and recent entries · in-flight orders · order historyMigrate completely. The winners record is both a brand asset and a live API contract.
Reduced fidelityHistorical orders · historical credits · judging historyMigrate through the Person/Organization extraction and dedup pass.
Archive onlyAbandoned carts · logs · retired programs · duplicate *Judging recordsCold 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.

SegmentSizing note
00Discovery: judging + public votingBlocks defensible estimates on the two biggest unknowns
01Tenancy, program lifecycle, configuration storeFoundation
02Calendar engine + phase eventsEverything downstream is driven by it
03Identity, RBAC, Person / Organization / CreditIncludes dedup strategy
04Configuration extraction from the legacy modelLargest pure-analysis task — 114 columns, 30+ flags, 9 property branches, each needing a keep/retire/promote decision
05Content service + CMSHighest ticket-volume relief, low complexity
06Catalog: media types, categories, pricing, cloning
07Submissions + entrant experience
08Commerce: entry checkout + Stripe
09Store on ShopifyIncludes entitlement gating and personalisation
10JudgingVery high, least understood
11Public voting serviceScale-driven; own datastore
12Winners service + public API + galleryDo first — precedes everything, unblocks WordPress cutover
13Event stream + reporting
14Data migration, all tiersSpans the programme, not a phase
15Characterisation harness + QAPrerequisite for cutover

10Open questions

For Recognition Media

  1. 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.
  2. 02Salesforce sync. The current middleware’s data flows are undocumented, including on RM’s side. Needs mapping before a replacement is committed to.
  3. 03Season calendar, 18 months. Outstanding, and it gates migration window planning.
  4. 04Stripe per-program accounts. Confirm whether legal and audit genuinely require siloing; it shapes the payment integration.
  5. 05Lovie and GDPR. Moved .eu.com and relocated to the US; likely out of EU residency scope, unconfirmed.
  6. 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.
  7. 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).
  8. 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

  1. 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.
  2. 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.
  3. 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

  1. Build a factory for awards programs, not an awards platform. Nine today, ~11 in five years, each a configured variant of one pipeline.
  2. Model the calendar as the control plane — typed phases, validated transitions, events. Deadlines carry a visible and an enforced date.
  3. Program configuration is data with a schema, an audit trail and an admin UI.
  4. Entry state is a lifecycle; award levels are per-program vocabulary. No boolean flag bags.
  5. People and organisations are the durable spine. Credit is first-class and unbounded from day one — it is what unblocks Webby Credits and the Directory.
  6. The winners record from 1997 is a product, with one service and one public API serving every program.
  7. 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.
  8. 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.
  9. Runtime is per-service, capped at two. .NET where the business rules concentrate; a lightweight runtime for Public Voting on elasticity grounds, not throughput.
  10. Strangler by program — smallest first, Webby last, Winners service first.
  11. Write characterisation tests before porting. Nothing is inherited.