Internal — technical team session · 22 September 2026
The respondent
platform, rebuilt.
Four routes out of the three-channel estate — from a fortnight of cleanup to a full rebuild. What each one costs, what it fixes, and what it breaks.
Before we start
What's evidence here, and what isn't
Measured directly
A read-only sweep of the production AWS account on 21 September: every EC2 instance, EBS volume, snapshot, S3 bucket, DynamoDB table and Lambda function. Nothing was changed. Bucket sizes come from CloudWatch, not estimates.
Estimated from list prices
Cost Explorer is denied to the credentials available, so every euro figure is public on-demand pricing applied to observed inventory — not your actual bill. The relative shape is reliable; the absolute total is not. Rik is bringing the real numbers to the session.
Inferred
RDS, ElastiCache and load balancers can't be listed with these permissions. They were reconstructed from CloudWatch metric dimensions and network interfaces. That's 26% of the cost estimate and the single largest uncertainty — if the two big databases are Multi-AZ, that line doubles.
Read from the source
upublish, core-system, chatapp, webapp, mobapp, botkit-service and the sync layer were read directly, with a read-only token. Claims about how the fan-out works cite file and line. Nothing was pushed or modified.
Several things everyone believes going in turn out to be wrong. That's the useful part of this deck — not the options.
The headline
Three premises worth correcting before we choose anything
“Half our AWS cost is document storage”
All fifteen S3 buckets together hold 52 GB and cost about $4 a month — 0.17% of the bill. The bucket that looks like the document store, upload.upinion.com, is completely empty.
“Storage is the problem”
Storage is 31% of spend — but it's block storage under the CouchDB sync tier, not documents in S3. Two 2 TB disks plus their backups are ~$625/month. Right instinct, wrong object.
“Unwinding the documents is a huge job”
CouchDB is not our system of record — it's a derived copy projected from MySQL, which already holds assignments and answers as normalised rows. The Chatapp already runs on them. The destination exists; only mobapp still needs the documents.
Why this matters for the session: if we optimise storage we'll save single-digit dollars. The money is in developer-days and always-on compute. That changes which option is worth picking.
Current state
Forty instances, one region, two architectural generations
| Layer | What's there | Note |
|---|---|---|
| Respondent channels | Native app · botkit · Chatapp | Three clients, three codebases |
| Sync tier | master-usync CouchDB, 2 × 2 TB | 91.5% full · RHEL 7 |
| Fan-out | uPublish + queue workers | Materialises per-respondent docs |
| Messaging | botkit-service-v3 | Single instance, no redundancy |
| Data | 7 RDS · 3 ElastiCache · 13 DynamoDB | DynamoDB holds 15 MB total |
| Edge | 7 load balancers · 3 CloudFront | 3 still Classic ELB |
| Staging | rc-* — 14 EC2 + 3 RDS | Full mirror, running 24/7 |
| Newer stack | rte-platform-* Fargate + DynamoDB | A migration already half-started |
The shape of the problem
Forty machines for about seventeen services, almost all of them t4g.micro or t4g.small. The fleet is wide and shallow — very little compute, a great deal of operational surface. Every service is a single instance except three.
Two generations, mid-migration
Beanstalk-era EC2 services sit alongside a modern serverless stack. Nobody finished the move. Every option below is partly a question of which generation wins.
Cost shape · estimated
Where the money actually goes
The single biggest line
Two 2,000 GB disks on the CouchDB sync cluster: $440/month, plus ~$185/month to snapshot them weekly. That's $625/month — 27% of everything — spent replicating the sync corpus.
Free money, no rebuild needed
- Staging mirror running 24/7 — ~$170/mo
- gp2 → gp3 on 4,496 GB — ~$99/mo, no performance loss
- 248 GB of disk on stopped machines — ~$26/mo
- A load balancer serving a stopped instance — ~$20/mo
- A database replica with zero connections in 48h — ~$13/mo
No orphaned volumes and no stray Elastic IPs — the classic waste isn't here. Somebody has been tidying.
The reframe
Documents cost us four dollars a month. The sync database costs six hundred and twenty-five.
The rebuild question was never “where do we put the documents”. It's whether we keep replicating 1.8 TB of CouchDB to every device.
And the mechanism matters: the cost driver is document count, not document bytes. Under CouchDB's default storage engine, every document's metadata and key has to stay permanently resident in RAM, on every replica. RAM is the most expensive byte in AWS. That's how a workload whose raw payload is about 10 GB a year can eat a quarter of the bill.
From the source, not from inference
The hard part is already done, and nobody wrote it down
CouchDB is a copy, not the truth
MySQL is the system of record. question_push already is the assignment table; answer already is the answer table. The document store is a derived read-model projected on top. Telling detail: there is no backup job for CouchDB anywhere, while MySQL and Redis both have one — somebody already treats it as disposable.
The Chatapp is already on the target architecture
chatapp fetches normalised rows over REST and posts answers back as flat rows. webapp never reads question or answer documents at all. Of the three clients, only mobapp is genuinely coupled to the fan-out.
And the merge function really is never used
There is no conflict handling anywhere — no 409 handling in any service. webapp logs _conflicts to Sentry in 18 places with the message “needs to be solved!” and resolves nothing. The equivalent checks in mobapp are commented out.
The shape of the fan-out
One CouchDB database per user, named u<id>, holding roughly 2 documents per question plus one. Rebuilding a single user's database routinely exceeds 2,000 documents and can take close to an hour.
Seven per-respondent fields — status, priority, index, viewed and answered dates — are flattened into the question content itself. The code has a mechanism to keep them separate and never calls it.
The one genuinely hard piece
A single CouchDB reduce function is the only definition anywhere of “which question does this respondent see next, and how far through are they”. It is minified JavaScript inside PHP string literals, with no tests and no second implementation to check against, and mobapp consumes its output directly. It can't be ported — only rebuilt and re-verified.
What this changes: the rebuild is less “design a new architecture” and more “finish migrating onto the one we already built”. That is a much better position than we thought we were in — but see slide 16, because it also means Options B and C are not as independent as they look.
Not a platform option — found while reading, routed separately
Two credential problems that need owners this week
These have nothing to do with which option we pick. They surfaced during the code read and would be wrong to leave in a research document.
Live credentials are in git history
RSA private keys, Apple production push certificates, and a Google Cloud service-account private key are committed to repositories.
Rotation is the remedy, not deletion — removing a file from the working tree leaves it in history. Every clone anyone has ever taken still contains them.
The Chatapp signing secret is public
chatappSecret is committed in cleartext, is identical across dev, RC and production, and is shipped inside the client bundle — so it is readable by anyone who opens the app.
On the face of it that means respondent login links can be forged. Worth confirming quickly, because if it holds it is a personal-data exposure, not just a hygiene issue.
I have not verified exploitability, and I have deliberately not tested it — that would mean attacking our own production service. What I can say is what is in the source. Given ISO 27001 and the populations involved, my instinct is that the second one wants looking at today, ahead of anything in this deck.
Urgent — independent of which option we choose
Four clocks are already running. One has ten days left.
WhatsApp stops being free
From 1 October 2026 Meta charges per message for service messages — free since 2024 — and re-charges for utility messages. Verified in Meta's own developer documentation, not a vendor blog.
There is no free monthly allowance. Any unit-economics model built on free in-window conversation is about to be wrong.
The sync disks fill up
Both usync volumes are at 91.5% — 1,831 GiB of 2,000 GB — and growing about 14 GiB per week each. Straight-line, they hit the warning threshold within the quarter.
This is the forcing function. It needs a decision or an extension regardless of what we pick tomorrow.
The platform underneath is EOL
The sync tier runs on RHEL 7, end-of-maintenance June 2024, on a custom Beanstalk platform. Separately, 13 of 26 Lambda functions run Node.js runtimes AWS has deprecated — some untouched since 2021.
No security patches. AWS can block updates at its discretion.
A licence question worth closing
Couchbase's current React Native module is Enterprise-only and its docs state it is “not compatible with Couchbase Lite Community Edition”. Our app pins react-native-couchbase@1.6.1 — an older binding, so this may not apply to us.
Sync Gateway is also reported to be BSL, not open source, with a use grant excluding commercial products — I could not verify that text directly (Couchbase's legal pages refused automated access). Someone should read it properly.
Before the session ends, someone should own each of these. The WhatsApp one is not a rebuild question at all — it needs a decision this week on how our survey invitations are classified (marketing, utility or service), because that determines the rate we start paying on 1 October.
Volume fill and growth rate from CloudWatch, 21 Sep 2026. WhatsApp change verified at developers.facebook.com, 21 Sep 2026 — note several vendor blogs claim a “1,000 free service messages per month” allowance that does not appear in Meta's documentation. Licence reading is from published Couchbase terms and is not legal advice — it needs a lawyer, not an agent.
The larger financial question
The native app costs more than the infrastructure it's blamed for
We have been discussing the cheaper problem. The app's maintenance is twenty to fifty times its hosting, and it serves one customer.
The app can't currently ship an update
mobapp declares targetSdkVersion = 35. Since 31 August 2026 Google Play has required API 36 for any new app or update. An extension to 1 November 2026 can be requested in Play Console.
Existing installs keep working and it stays available to new users — but until someone bumps it, no update can be published at all. Last commit was 1 December 2025.
The blocking question turned out to have an answer
We asked whether the app uploads offline-captured answers without the user reopening it. It does not — and it never could.
- UIBackgroundModes in the iOS plist is an empty array; the remote-notification handler is commented out
- No headless task on Android, no WorkManager, no foreground service
- A push arriving with the app closed draws a tray notification and nothing else
So nothing is lost by moving to a PWA. The constraint we were most worried about isn't one.
And the uncomfortable one
The app has no offline write durability at all. Answers are POSTed directly; on failure the code resets a React state variable. There is no outbox and no queue. The Couchbase replica runs server → device only.
We are paying for an offline-first sync tier that does not give us offline answering. Neither does the Chatapp — its service worker is configured with handleFetch: false and exists only to carry push.
So if offline capture is a product requirement, it has to be built from zero — on whichever client survives. That is a genuine piece of work, but it is a week of outbox code, not a replication engine. And it is the same week whether we keep the app or not, which removes it as an argument for keeping the app.
The €15–35k range is from published practitioner estimates, not your timesheets — worth replacing with the real figure in the room. Note the app has had no commits in 2026, so the figure you have actually been paying this year is probably far lower — and the compliance debt is the other side of that coin.
Reach · the uncomfortable arithmetic
Push can never be the primary way we reach people
What this settles
Push is an accelerant for the engaged core, never the reach mechanism. Any design that makes notifications the front door will underperform the current hybrid, however elegant it looks.
The constraint nobody mentions
Everyone worries about iOS. But iOS is 7% of respondents in Kenya, India and Bangladesh. Meanwhile Opera is 32% of mobile browsing in Kenya and 22% in Nigeria — and Opera Mini's proxy mode cannot receive web push at all. In our markets that is the bigger hole.
And the hybrid actively blocks the fix
Links tapped inside WhatsApp or Facebook may open in an in-app browser. Android WebView does not implement the Push API — so those users can't be subscribed, and on iOS can't add to home screen either. Worth a 30-minute test on a real handset before we choose.
A defence of something we dislike
The hybrid isn't the mistake. The implementation is.
Notify on a channel people already have, then hand off to a richer surface — that is what the evidence says a reach-constrained platform should do. Every mature humanitarian data platform works this way.
Keep the pattern
- Reach via a channel with real delivery receipts — WhatsApp, SMS
- Answer on a surface we control and can change without anyone's approval
- Push as an upgrade for repeat respondents, not the entry point
Fix the seams
- Our canonical internal message format is a vendor's. Meta Cloud webhooks are translated into Twilio's field names — Body, NumMedia, MediaContentType0 — and that becomes the domain model
- Question rendering is forked three ways (Messenger, WhatsApp chat, WhatsApp Flows), each with vendor caps like “13 quick replies” baked into business logic
- The 24-hour window is handled in 8 places, inconsistently
- The handoff leaks users into in-app browsers where nothing can be improved
The design move, corrected: a delivery service must absorb the three senders we already have — botkit, uPublish (which already pushes to APNs, FCM and web-push) and the PHP webservice — not become a fourth. Its hardest prerequisite is identity: the platform identifier exists in four incompatible shapes across four repos with no shared constant. That is the real first task, and it is unglamorous.
Has the ground moved?
Web push in 2026: better than when we built the Chatapp, still gated on iOS
| Question | Answer, Sept 2026 |
|---|---|
| Does web push work on Android? | Yes — same FCM transport a native app uses. Widely available since 2023. |
| Does iOS still require add-to-home-screen? | Yes. Unchanged through Safari 27 beta. Checked every release. |
| Is delivery quality worse than native? | No — on iOS it rides the same APNs as your app. The gap is the opt-in funnel, not the transport. |
| Do we need an Apple developer account? | No. Standard web push on iOS is free. |
| Can we prompt users to install on iOS? | No. No install prompt exists — instructional UI only. |
| Would a vendor (OneSignal, Airship…) fix iOS? | No. They wrap the same constraint. They buy convenience, not capability. |
The one that undercuts “native push is reliable”
Low-end Android OEMs aggressively kill background processes — Tecno, Xiaomi, Oppo, and Samsung among the worst. The standard fix is integrating each OEM's own push SDK. A React Native app doesn't get that for free either. Our native app is probably losing notifications the same way.
No channel gives us delivery truth
Neither web push nor FCM confirms a message was rendered — only that the push service accepted it. We cannot distinguish “ignored the survey” from “never got it”. For a research instrument, that is a methodological hole, and an argument for keeping a receipted channel as the system of record for reach.
The cheap win
A Trusted Web Activity — a thin Android shell around the existing Chatapp — gives native-grade Android push and a Play Store presence, while keeping one web codebase. That covers 82–93% of respondents in four of our six markets.
Platform risk
Why Meta keeps breaking things — and what actually helps
It isn't bad luck or bad engineering. Meta hard-sunsets each Graph API version two years after its successor ships, and ships roughly three versions a year. On top of that sit template approvals, quality ratings, the 24-hour window and number-level blocking — each able to break us without a code change on our side.
And the churn isn't only technical. On 1 October the commercial terms change too — service messages inside the 24-hour window stop being free. That is the same dependency showing up as a cost line instead of a bug.
What doesn't help
Switching to a different Meta-owned channel. Adding a BSP in front. Both change who absorbs the churn, not whether it happens.
What does
An adapter boundary, a contract test per channel that runs on a schedule, and at least one non-Meta channel live in production — not as a plan, but actually carrying traffic, so cutover is tested rather than theoretical.
| Channel | Cost | Forced-change risk | Verdict |
|---|---|---|---|
| Per message — and rising 1 Oct | High — 2-year sunsets | Keep; it's where people are | |
| SMS | High per message | Low | The floor. Expensive but unkillable |
| Telegram | Free to 30 msg/sec | Low — no forced migrations | Strong second channel; strong survey tools |
| Web push | Free | Low | Accelerant, not reach |
| Signal | — | — | Rule out. No business API; bulk messaging is against its terms |
On Telegram, one caution: it was blocked nationwide in India for six days in June 2026 and the courts upheld it. It's a genuinely good second channel — but never a sole channel in a market.
What we're actually choosing
Three decisions. Everything else follows.
Do we keep a bidirectional sync engine?
Survey sync is asymmetric and conflict-free: download a shared questionnaire, upload single-author answers. Nobody ever edits the same record twice. A merge algorithm would run zero times in production.
Do we keep the native app?
One customer, and it is the only thing still holding the document fan-out open. The background-upload worry turned out to be unfounded, so this is now a product and commercial question, not a technical one.
How much Meta exposure do we accept?
Not a yes/no. It's whether the exposure sits behind an adapter with a tested alternative running, or wired through business logic as it is today.
The four options are combinations of these three answers, and they are cumulative — B contains A, C contains B. But reading the code changed one thing: because mobapp is the only remaining consumer of the documents, questions 1 and 2 turn out to be the same question. We can still stage the work, but we cannot defer the app decision and expect to finish B.
Stabilise
SafestStop the bleeding, buy time, change no architecture. Extend the disks, harvest the waste, patch what's EOL.
What we do
- Grow or archive the usync volumes
- gp2 → gp3; schedule staging off out-of-hours
- Delete stopped-instance disks, the idle load balancer, the dead replica
- Set CloudWatch retention; add free S3/DynamoDB VPC endpoints
- Migrate the 13 deprecated Lambda runtimes
- Get the Couchbase licence read
What it buys
- The capacity cliff goes away
- ~$250–300/month back, ~13% of spend
- Security posture stops degrading
- No product risk, no customer impact
~2–4 weeks of one engineer.
What it doesn't fix
- Three codebases, three channels — all still there
- RHEL 7 sync tier still under everything
- The app still costs €15–35k a year for one customer
- Meta can still break us next quarter
- The disks refill. This is a reprieve, not a cure
Do this one regardless. Every other option starts here — the only question is whether we stop here.
Delete the fan-out
Best value per euroA + kill uPublish. One immutable questionnaire per version, shared by everyone, instead of one document per respondent. Keep all three channels exactly as they are.
The change in one line
A questionnaire stops being N documents for N respondents and becomes one content-addressed file on S3 behind CloudFront, cached forever, fetched by everyone. Answers go back as rows, not as documents.
What it buys
- Removes the majority of documents — the thing actually driving the sync tier's size and RAM
- Turns a superlinear cost curve flat
- Deletes a whole service and its queue workers
- The Chatapp and both chat channels need no changes at all — they are already on the target model
Difficulties — revised after reading the code
- Splitting the per-respondent fields out of question content is 1–2 days, not the hard part we assumed
- The hard part is rebuilding that one CouchDB reduce — untested, minified, no reference implementation
- Elapsed time is gated by mobapp, not by throughput — the fan-out can't be switched off while respondents run old builds
- The legacy sync box may not be rebuildable at all — CouchDB 1.6.1 on Ubuntu trusty, pulling GPG keys from a keyserver network dead since 2019. If it can't rebuild, there is no rollback target
Effort & risk
8–14 weeks of engineering — but 4–6 months elapsed before the fan-out can actually be switched off, because that waits on app retirement. My earlier 4–8 week figure was the engineering, not the calendar.
The catch we didn't see before: since mobapp is the only remaining consumer of the documents, finishing B means either porting the app to REST or retiring it. B's tail and C's decision are the same decision — which makes the customer conversation more urgent, not less.
One client, many channels
Recommended shapeA + B + retire the native app and the replication engine. The Chatapp becomes the only respondent client. A delivery service decides how to reach people.
What we build
- Chatapp gains an outbox: cache the questionnaire, queue answers, retry with idempotency keys. About a week of work — no vendor, nothing to run
- Android via Trusted Web Activity — store presence, native-grade push, one codebase
- iOS via Apple Business Manager for the one customer, or add-to-home-screen
- A delivery service behind which WhatsApp, SMS, Telegram and push are interchangeable adapters
- Responses land in a relational store; cold data archived as compacted files
What it buys
- The €15–35k/year app maintenance stops
- CouchDB, uPublish and the 2 TB disks are switched off
- One respondent codebase instead of three
- Meta breakage is contained to an adapter
- Telegram becomes viable at effectively zero marginal cost
- The Couchbase licence question stops mattering
Difficulties — revised after the parity audit
- Cleared Question-type coverage is identical — 11 types each. Camera, microphone, GPS and file upload already exist in the Chatapp
- Cleared Background upload is not a blocker — the app never had it
- Rewards, vouchers and tickets are entirely missing from the Chatapp — the single largest genuine gap
- The Chatapp is one group per subdomain; the app manages several
- iOS push still needs a home-screen step, or Apple Business Manager distribution
- Requires the customer's agreement and a written sunset date — indefinite coexistence is how these migrations fail
This is the option that resolves all three decisions coherently. It stands or falls on one conversation with one customer — which is worth having this week, before we commit.
Rebuild the estate
Most radicalC + retire the Beanstalk generation entirely. Finish the migration somebody already started: serverless services, managed data, no long-lived fleet.
What we do
- Forty instances become a handful of managed services
- Staging stops being a 24/7 mirror and becomes ephemeral
- Single data model, one authoritative store
- The rte-platform stack's patterns become the house style rather than an exception
What it buys
The operational surface collapses. Most of the $2.3k/month goes, but more importantly the standing maintenance tax goes — no OS patching, no EOL platforms, no snapshot sprawl. For a team this size that is the real prize, not the hosting bill.
Difficulties — and they are real
- 9–18 months. Long enough that priorities will change underneath it
- Every service rewritten is a service that can regress; some are 2017-era and nobody remembers them
- Highest opportunity cost — this is most of the team's capacity for a year
- Big-bang rewrites have a poor track record, and ours would run while the business needs new features
- The saving is ~$27k/year of hosting. A year of engineering costs multiples of that
The honest case against
If C is done well, D's remaining benefit is mostly tidiness. The strongest version of D isn't a programme — it's a standing rule that anything we touch gets migrated, and we let the old generation die by attrition.
Side by side
The four options against what we care about
| A · Stabilise | B · Delete fan-out | C · One client | D · Rebuild | |
|---|---|---|---|---|
| Effort | 2–4 weeks | 4–8 weeks | 3–6 months | 9–18 months |
| Infra saving | ~$250–300/mo | + flattens the curve | + most of the sync tier | Most of $2.3k/mo |
| Maintenance saving | — | Deletes a service | €15–35k/yr | + the whole patching tax |
| Fixes capacity cliff | Defers it | Yes | Yes | Yes |
| Reduces Meta exposure | No | No | Yes | Yes |
| Fixes notification story | No | No | Yes | Yes |
| Needs customer sign-off | No | Yes, to finish | Yes | Yes |
| Risk | Very low | Moderate, contained | Moderate | High |
| Reversible? | Fully | Largely | Partly | No |
If we want one sentence
A is not a choice, it's hygiene. B and C turned out to be one decision wearing two hats. D is C plus a year we probably shouldn't spend.
But this is genuinely arguable
The case for D is that attrition never actually happens — teams say “we'll migrate as we touch it” and then don't, and in three years we're having this meeting again with an EOL RHEL 8. The code read gives that argument some weight: the Chatapp has been on the right architecture for a while, and nobody finished the move.
If we choose A → B → C
What the first ninety days look like
| When | Do | Why now |
|---|---|---|
| This week | Classify our WhatsApp traffic — marketing, utility or service — and re-cost it | Billing changes on 1 October. Not a rebuild question; it lands whatever we decide |
| Week 1 | Pull the real bill — Cost Explorer grouped by usage type, not service | Settles this deck's biggest uncertainty before a euro is spent |
| Week 1 | Ask the app customer: is background upload required? Do they use MDM? | A five-minute conversation that decides a five-figure annual line |
| Week 1 | Get the Couchbase licence terms read by counsel | Compliance exposure, independent of any migration |
| Week 1 | Extend or archive the usync volumes | The clock that doesn't wait for our decision |
| Week 1 | Test on a real low-end handset: does a link from WhatsApp reach a push-capable browser? | Materially changes the migration design; costs half an hour |
| Weeks 2–4 | Harvest the waste; migrate deprecated runtimes; set log retention | Pays for part of the rest |
| Weeks 2–6 | Kill the uPublish fan-out | Highest-leverage change; independent of everything else |
| Weeks 4–12 | Outbox in the Chatapp; delivery service with two adapters live | The rebuild proper — with a non-Meta channel actually carrying traffic |
| Before starting C | Agree a written sunset date with the app customer | Indefinite coexistence is the dominant failure mode of migrations like this |
Note what's in week one: four questions and a disk extension. None of it commits us to an option — but all of it makes the option choice better-informed. If we leave the session having only started these, that's a good session.
Intellectual honesty
What I couldn't verify, and what would change my mind
Gaps in this deck
- The fan-out arithmetic is structural, not measured. It comes from reading code paths. Pull the real database and document counts before committing to a number
- We don't know the live app version spread — and that, not engineering throughput, is what actually sets Option B's timeline. Invisible from the repos
- Costs are list-price estimates. RDS is inferred from memory metrics and is 26% of the total — if those two databases are Multi-AZ, the estimate is badly low
- Two readings of the app's target SDK disagree (34 on master, 35 on the default branch). Either way it is below the 36 required to publish — but check which branch actually ships
- WhatsApp template approval state lives in the admin database, not in code — an unquantified migration risk
- Elastic Beanstalk returned empty results despite 25 instances tagged to it — unexplained
What would change the recommendation
- The legacy sync box can't be rebuilt from its Dockerfile → we have no rollback target, and the migration has to be one-way and very carefully staged
- Respondents are spread across many old app builds → Option B's tail stretches past six months regardless of engineering effort
- Rewards and vouchers turn out to be central to the app customer → C's cost rises materially; that UI does not exist in the Chatapp
- More customers want the app next year → the whole calculus inverts; keep it and invest properly
- The real bill shows compute, not storage, dominating → D gets considerably stronger
I'd rather be corrected tomorrow morning than have this read as more certain than it is. The three premise corrections on slide 3 are the findings I'd defend; the effort estimates are the ones I'd expect to be wrong.
For the room
Five questions to leave with
1. Do rewards and vouchers matter to the app customer?
Now the largest genuine parity gap, and the main thing standing between us and Option C.
2. What is the app actually costing us in days?
The single most decision-relevant number, and only we have it — with the wrinkle that this year the answer looks like roughly zero.
3. Do we believe in migration-by-attrition?
If not, D deserves a serious hearing rather than a polite one.
4. Is one non-Meta channel in production a requirement or a nice-to-have?
This is a strategic posture, not an engineering preference.
5. Who owns the sunset date?
Migrations like this fail by drifting, not by failing.
And one thing to just do
Extend the usync volumes this week. It is the only item here with a deadline we didn't choose.
Prepared by Upii for the Upinion technical session, 22 September 2026 · Infrastructure findings from a read-only AWS sweep on 21 September 2026; no production resources were modified · External research is sourced in the accompanying briefs · Cost figures are list-price estimates, not billed cost.