Too loud?

Keyboard shortcuts

Anywhere on the site, while you are not typing.

M
Sounds on or off
E
Copy my email
T
Day or night
S
Change the season
D
Developer mode
?
These shortcuts
Esc
Close what is open

  • internship
  • 2025 to now
  • Voice rooms
  • Stripe billing
  • AI pipeline
  • AWS

IABTM is a US self-improvement platform: personalised growth paths, curated film, music and writing, a social feed, live voice stages, paid tiers and two Shopify stores.

I joined in September 2025 and picked up a 55,000-line codebase. Since then I’ve rebuilt its voice rooms and its billing, built the AI pipeline, the email platform and the tools the team runs the product from, and I look after the servers it runs on.

The IABTM home page: Become the self you imagine, beside a collage of portrait photos.
become the self you imagine
IABTM pricing: Free, Premium at $5.99 a month with a free first month, and Premium+ at $19.99.
live stripe billing
The IABTM shop: a grid of apparel with sizes, prices and add to cart buttons.
two stores, one backend
Type
internship
Role
Software Engineer, internship, remote
When
Sep 2025 to now
Stack
Next.js, React, Express, MongoDB, Redis, Socket.IO, mediasoup, Stripe, Shopify, Gemini, PostHog, AWS
Links
iambetterthanme.com
publishers, curated daily
6
jobs that run themselves
7
admin tools for the team
25

What I’ve shipped

  • Live Stripe billing

    Three tiers and four plans, rebuilt after I found four holes in the old one. Prices are set on the server, and a payment event that arrives twice only counts once.

    How it works
  • Voice rooms on an SFU

    Live audio rooms where five members hold the stage and the rest raise a hand, rebuilt from a peer-to-peer mesh onto mediasoup.

    How it works
  • A daily AI pipeline

    Every morning, new articles from six major publishers land in the right members’ growth paths. Gemini picks the fit from the platform’s own list, never its own words.

    How it works
  • Seven jobs that run themselves

    Email campaigns, daily nudges in each member’s own time zone, store syncs and health digests, each run once on schedule however many servers are up.

    How it works
  • Premium, enforced once

    Free members can browse the whole catalogue and get a taste of every premium section. One check on the server decides what plays.

    How it works
  • Failures that make noise

    If a new member’s growth path fails to build, the account is marked and the team hears about it, instead of signup quietly saying yes.

    How it works
  • Two Shopify stores

    Two stores behind one headless backend. Orders arrive verified, a repeat can’t duplicate one, and new products publish themselves overnight.

  • Email end to end

    Sign-in codes, billing, lifecycle and campaign email moved onto Resend. Every send is logged, bounces are kept off the list, and failures reach the team within the hour.

  • The tools the team works in

    25 admin screens: a support desk, a campaign CRM, analytics and A/B panels, and controls for subscriptions and the shops.

  • SEO as a system

    Every page gets its own title, share image and structured data from one builder, across about 30 layouts.

  • Monitoring and alerts

    Uptime checks, server errors and model telemetry that never records what members wrote, plus a daily health digest.

  • The server it runs on

    EC2 with nginx and PM2, media on S3 behind CloudFront, and a release tag to roll back to for every launch.

  • Uploads straight to S3

    Posts and banners of up to 2 GB go from the browser to S3 in parallel parts that retry on their own, so a dropped connection costs one part, not the whole upload.

  • A feed that stays fast

    One aggregation, cursor pagination checked against the query plan, and a short Redis cache, so page fifty costs the database what page one does.

  • Ready for more servers

    Sockets and caches shared through Redis, jobs that claim their turn, and duplicates stopped in the database, so the web side can run on several servers.

  • Accessible by default

    I built most of the app's accessibility: keyboard paths, focus handling, screen-reader labels and reduced motion.

How it fits together

Browsermembers · adminsCloudFront + S3media · direct uploadsnginxTLS · HTTP + WSone EC2 box · PM2Next.js client:3000 · SSRTURNWebRTC relayExpress API:8000 · one worker517 endpointsSocket.IOmediasoup SFU7 leased jobsMongoDB Atlasmodels · leasesRediscache · socket adapterStripeShopify ×2ResendGeminiPostHogmedia
One EC2 box under PM2. The API stays one worker on purpose: audio rooms hold their state in the process, so the plan is to move realtime to its own machine, not to add workers.

Billing that counts every payment once

The Stripe integration I inherited let the browser choose the price, let anyone cancel anyone’s subscription, never actually switched a paying member on, and handled every retried event all over again.

I rebuilt it around three rules: prices live on the server, one service decides who has access, and every Stripe event is written down before it is acted on, so a retry can’t do anything twice. It went live in August 2026 with existing members carried over, covered by unit, end-to-end and live-Stripe tests.

Striperetries on 5xxWebhookraw bodyLedgerunique eventIdHandlergrants accessevent + signaturebad signature: 400insert { eventId }duplicate: 200, donedispatch(event)completed200, even on failurethe row keeps the error for a replay
Each event is written down first. If Stripe sends it again, the copy finds it already there and does nothing; if handling fails, it stays on record to be replayed on purpose.

billingWebhookController.js · handleStripeWebhook

try {  await StripeWebhookEvent.create({    eventId: event.id,    type: event.type,  });} catch (error) {  if (error?.code === 11000) {    return res.status(200).send('Already processed');  }  return res.status(500).send('Could not record event');}try {  await dispatch(event);  await StripeWebhookEvent.updateOne(    { eventId: event.id },    { $set: { completed: true } },  );  return res.status(200).send('OK');} catch (error) {  // Deliberately a 200: a 5xx makes Stripe retry a  // request that will fail the same way. The row  // above records the failure so it can be replayed.  await StripeWebhookEvent.updateOne(    { eventId: event.id },    { $set: { completed: false, error: error.message } },  );  return res.status(200).send('Recorded with error');}
Insert first, work second. A duplicate key is not an error here; it is the answer.

Seven jobs, one run each

Campaign emails, daily nudges, store syncs and the morning AI pipeline all run on a schedule. When I arrived, that schedule had never been installed on the server at all, and the app is built to run as several processes, where a plain timer fires once in each and emails every member several times.

So every job now claims its slot in the database before it runs. The first claim wins and the rest skip, and if the database can’t answer, the job skips rather than risk sending twice.

W1W2W3PM2 workersCronLockunique key · TTL 2 ddigest:2026-09-26insertRuns the jobowns this windowSkips11000: duplicate
However many copies of the app start, only the first to claim the slot runs the job. Old claims clear themselves after two days.

helpers/runWithLease.js

export const acquireRunLease = async (name, bucket) => {  try {    await CronLock.create({ key: `${name}:${bucket}` });    return true;  } catch (err) {    // another worker won this window    if (err?.code === 11000) return false;    throw err;  }};export const runWithLease = async (name, bucket, fn) => {  let owns = false;  try {    owns = await acquireRunLease(name, bucket);  } catch (err) {    // Fail closed: skip this window rather than    // risk a double send. The job fires again on    // its next schedule.    return;  }  if (!owns) return; // another worker has it  await fn();};
Every scheduled job goes through this, with a kill switch of its own.

The model only chooses from a list

Every morning, new articles from six publishers, among them The Atlantic, Wired and MIT Technology Review, go to the members they suit. Gemini reads each one and picks who it is for from the platform’s own list of growth attributes, up to three of each kind. Anything it makes up is thrown away.

If the model answers with nonsense, answers with nothing or isn’t there at all, a plain keyword tagger does the job instead. Articles it has already seen are skipped before a single model call is paid for.

Six publishersRSSDedupeby URL, firstGeminipicks from the listValidateknown · ≤ 3 + 3Keyword taggerthe fallbackPublishtagged articlesbad JSON · empty · 429first 429: the model is off for the rest of the rundry run · CSV export · kill switch · one run a day, leased
An LLM in a job nobody watches has to fail quietly and correctly: every exit that isn't a clean, known set of tags goes to the keyword tagger.

scripts/automated-editorial-pipeline.js

function normalizeAndValidateClassification(  parsed, allowedCurrent, allowedImagine,) {  const cur = Array.isArray(parsed?.currentSelf)    ? parsed.currentSelf : [];  const img = Array.isArray(parsed?.imagineSelf)    ? parsed.imagineSelf : [];  const currentSelf = uniq(    cur.map(normalizeAttributeTitle)      .filter(a => allowedCurrent.has(a)),  ).slice(0, 3);  const imagineSelf = uniq(    img.map(normalizeAttributeTitle)      .filter(a => allowedImagine.has(a)),  ).slice(0, 3);  return { currentSelf, imagineSelf, via: 'gemini' };}// In classifyArticle, on any model error:if (looksLikeGeminiQuotaError(err)) {  // Rate limited: off for the rest of this run.  geminiDisabledForRun = true;}return keywordFallbackTagger({  title, description, currentSelfAttrs, imagineSelfAttrs,});
What the model says is filtered against the list, deduplicated and capped; the first rate limit turns it off for the run.

Voice rooms on an SFU

Members meet in live audio rooms and on stages, where five people speak and everyone else listens and raises a hand. The rooms were a peer-to-peer mesh, where every speaker sends their audio to every listener, which falls apart past a handful of people.

I rebuilt them on an SFU (mediasoup): each speaker sends once and the server forwards, the ring lights up on whoever is talking, and a crashed media worker restarts on its own instead of taking the app down. Then I audited the whole realtime layer, chat and notifications included, and shipped the critical fixes.

mesh: every link is an uploadSLLLn people, n(n - 1) streamsSFU: one upload eachrouterSLLLa router per room, 5 speakers
In a mesh every speaker uploads to every listener; on an SFU each uploads once and the room's router forwards.

Premium locks the tracks, not the tab

Free members still see the whole catalogue; what they can play is decided on the server, by the one entitlement service, with seven days' grace after a failed renewal.

R

Paths

Free

Deep Work, Morning

42:10

The Discipline Tape

18:04

Premium

Cold Start

27:55

Premium

Recovery, Slow

33:20

Premium

tease what is personal, open what is not

Premium locks what a free member can play, not the whole tab.

A signup that said yes for over a week

Every new member is meant to leave signup with a growth path of their own. For over a week none did, and signup kept saying yes: a change had broken the path builder, and the way it was called swallowed the error before anyone could see it.

The fix was one line. The real work was making sure it can’t happen quietly again: signup now checks the path exists, flags the account if it doesn’t, and tells the team.

paths created per signup200 OK, all of them

try {

await createPersonlisedPath(user, fakeRes)

} catch (e) { /* never fires */ }

Every signup in the two weeks before the change got a path; not one after, while signup kept answering yes.

Next. Refunds and dispute handling, and a load test that puts a number on how many listeners one voice box carries.