DEV Community

Daniel Pertu
Daniel Pertu

Posted on

Our widening setting is on by default, and the only way to store that is to store nothing

A Nakodo campaign can run out. Every keyword it was given has been searched, the new angles it generated have had their turn, and the brand still wants more creators to talk to. The honest options are to sit there finding nothing, or to widen what counts as a fit.

We widen, but only with the brand's permission, only one step at a time, and only in the two directions that are ours to move. src/lib/pipeline/widen.ts is 152 lines and most of the interesting parts are decisions rather than code.

The step order is a cost order

export type WidenStep =
  | { kind: "range"; minSubscribers: number; maxSubscribers: number }
  | { kind: "platform"; platform: Platform };
Enter fullscreen mode Exit fullscreen mode

The audience range goes first:

const wider = {
  minSubscribers: Math.max(MIN_SUBSCRIBERS_FLOOR, Math.round(min / 2)),
  maxSubscribers: Math.min(MAX_SUBSCRIBERS_CEILING, max * 2),
};
if (wider.minSubscribers < min || wider.maxSubscribers > max) return { kind: "range", ...wider };
Enter fullscreen mode Exit fullscreen mode

Half the minimum, double the maximum, floored at 1,000 subscribers and capped at 10 million. A brand that asked for 10,000 to 200,000 gets 5,000 to 400,000, which our guide to micro creators would call reaching down into nano and up into mid-tier. Those bands are a convention rather than a standard, which is exactly why doubling and halving is defensible as a first move.

The reason the range comes first is that it is free. A campaign has already met creators it turned away purely for their size, and their data is already stored, so widening the range brings them back with no search and no quota spent:

const back = await db
  .update(campaignChannels)
  .set({ stage: "discovered", rejectionReason: null })
  .where(and(
    eq(campaignChannels.campaignId, c.id),
    eq(campaignChannels.stage, "rejected"),
    or(like(campaignChannels.rejectionReason, "Too small%"), like(campaignChannels.rejectionReason, "Too large%")),
  ))
  .returning({ channelId: campaignChannels.channelId });
Enter fullscreen mode Exit fullscreen mode

A rejection reason stored as a sentence is being used as a query here, which I would normally flag in review. The alternative is an enum column plus a migration every time a reason is added, and the like prefix is stable because the sentence is generated in one place. It also makes the readmission auditable in one line: the count goes into the audit row for the widening.

Adding a platform comes second, because that is new supply and therefore new searches:

const open = searchedPlatforms(KIND_PLATFORMS[input.kind].filter((p) => !input.platforms.includes(p)), input.planPlatforms);
return open.length > 0 ? { kind: "platform", platform: open[0] } : null;
Enter fullscreen mode Exit fullscreen mode

planPlatforms is the gate. On the Free plan a campaign searches Instagram and TikTok, and YouTube arrives with Pro, so widening can never hand a Free campaign a platform its plan does not include. Returning null when nothing is left is the third state: not "widened", not "failed", just nothing left to give.

What is never widened

Markets, languages and the brief's own description of the product are untouched, and that is in a comment at the top of the file rather than hidden in the diff:

// Markets, languages and the brief's own words are never touched: they decide
// who hears from the brand and in what language, which is the brand's to say.
Enter fullscreen mode Exit fullscreen mode

This is the line I would defend hardest. Automatically emailing people in a country the brand never asked for is not a wider search, it is a different campaign. The brand's own copy of this promise sits in the brief editor next to the checkbox: "It never changes your markets or what you sell."

The default is an absence

The permission lives in the brief's JSONB, and it is on unless the brand says otherwise. Which means the sanitizer can never write widen: true:

// Only when turned off: widening is on unless the brief says otherwise,
// so an untouched brief carries nothing. A field left out here is dropped
// from every save, which is how it would quietly turn itself back on.
...(raw.widen === false && { widen: false }),
Enter fullscreen mode Exit fullscreen mode

Both halves of that comment matter. Storing the default would mean every brief written before the feature existed disagrees with every brief written after it, for no behavioural reason. And because the sanitizer rebuilds the object rather than spreading the old one, a key it forgets to carry is a key that gets deleted on the next save. An opt-out that silently re-opts you in is worse than no opt-out, so the test asserts both directions:

assert.equal(sanitizeBrief({ ...base, widen: false }).widen, false);
assert.equal(sanitizeBrief({ ...base, widen: true }).widen, undefined);
Enter fullscreen mode Exit fullscreen mode

Three clocks before anything happens

export function widenDue(c, now = Date.now()): boolean {
  if (!c.brief || c.brief.widen === false) return false;
  if (c.launchedAt && now - c.launchedAt.getTime() < WIDEN_AFTER_LAUNCH_DAYS * DAY_MS) return false;
  if (c.widenedAt && now - c.widenedAt.getTime() < WIDEN_EVERY_DAYS * DAY_MS) return false;
  return true;
}
Enter fullscreen mode Exit fullscreen mode

One day after launch, then no more than one step every two days. The pacing is not about load, it is about legibility: a brand that opens the campaign on Thursday should be able to see which single thing changed since Tuesday, and the brief page prints the date of the last step. Five tests cover the ladder and the clocks, including the one that says a campaign still holding unsearched keywords is not stuck and widens nothing:

const unsearched = await db.select({ id: keywords.id }).from(keywords)
  .where(and(eq(keywords.campaignId, c.id), eq(keywords.enabled, true), isNull(keywords.lastSearchedAt))).limit(1);
if (unsearched.length > 0) return null;
Enter fullscreen mode Exit fullscreen mode

Where it runs, and what it is not allowed to restart

The daily maintenance cron calls it for each campaign that is still searching, before that campaign asks for anything else, so the round that follows works to the wider brief. One failure does not take the others down:

const step = await widenIfStuck(c).catch((e) => {
  console.error("Widening failed", c.id, e);
  return null;
});
Enter fullscreen mode Exit fullscreen mode

The platform step reuses the launch path to create keywords and searches for the new platform, and passes { resume: false }, because a campaign whose emails the brand paused must stay paused. Widening is a change to what gets found, and it is not allowed to become a change to what gets sent.

Business campaigns opt out of this entirely:

// A business campaign is widened by its places and the kinds of business it
// asks for, which are the brand's own choices, not by an audience size.
if (c.kind !== "creators" || !c.brief) return null;
Enter fullscreen mode Exit fullscreen mode

They widen by area instead, which is a separate mechanism with its own post. If you want the product-level version of all this, the how it works page covers what a brief contains and where the subscriber range comes from in the first place.

Top comments (0)