Rebounder Tech Blog

Written by the people who actually run these systems in production.

is_pinned=false Can't Exclude Specials, Only Pins

Published About 4 min readBy the Rebounder engineering team — the people who operate these systems

This article may contain affiliate links. Its content is not affected by advertising.

In short

'Newest unpinned row' only excludes rows that happen to be pinned. If a special event isn't pinned, it passes straight through — specialness and pin state are independent flags.

Conclusion

An auto-copy that picks “the newest unpinned row” will still pick up a special event as long as that special event isn’t pinned. Being special and being pinned (visually fixed in place) are independent flags; one can’t stand in for the other. The fix wasn’t to add another condition — it was to drop auto-selection entirely and let the copy source be picked from a dropdown.

The failure mode

The workshop admin screen has a feature: when adding a new date to a category, it auto-copies an existing event’s content in that category (description, price, capacity, location, images). The copy source was selected with this query:

const { data: latestWs } = await supabase
  .from('workshops')
  .select('*')
  .eq('category_id', fromCategory)
  .eq('is_private', false)
  .eq('is_pinned', false)   // pinned specials (e.g. summer parent-child special) shouldn't be used as a template
  .eq('is_service', false)  // services shouldn't be used as a template either
  .order('event_date', { ascending: false })
  .limit(1)
  .maybeSingle()

The comment’s intent was “don’t use a pinned special as the template.” In practice, an unpinned special event existed — a one-off like a summer parent-child special — and it had the most recent event date. Adding a new date to a regular category copied that special event’s content wholesale.

Cause

The only exclusion condition was is_pinned=false. That condition can only ever exclude “rows that are pinned” — it has nothing to do with “rows that are special.” Pinning, operationally, was only ever turned on when staff wanted that row visually fixed in the listing, so a special event not being pinned was an entirely ordinary state.

// A special event added on 8/3. No is_pinned value was set — defaults to false.
// chore: push summer parent-child special 8/11 (Tue) 14:00 date addition to production (rebuild)

With that row in the table, taking “the first row by event_date descending” surfaced this recently-added special as the newest match — becoming the copy source for whatever unrelated category an admin happened to add a new date to next. The code’s comment stated the intent to exclude specials, but the implementation had exactly one condition supporting that intent, and no value anywhere directly represented “specialness” itself.

The fix

Rather than adding another exclusion condition (e.g., introducing a new “special event” flag and also filtering on eq(false)), auto-selection itself was removed and replaced with a dropdown:

<select
  value={sourceId}
  onChange={(e) => applySource(e.target.value)}
>
  <option value="">コピーしない(空から作成)</option>{/* "Don't copy (start blank)" */}
  {categories.map((cat) => {
    const items = sourceWorkshops.filter((ws) => ws.category_id === cat.id)
    return (
      <optgroup key={cat.id} label={cat.name}>
        {items.map((ws) => (
          <option key={ws.id} value={ws.id}>{formatSourceLabel(ws)}</option>
        ))}
      </optgroup>
    )
  })}
</select>

The list of copy sources is grouped by category and sorted descending, each entry labeled with its event date and title. is_service=false still keeps services out of the list, but there’s no longer any condition trying to infer “is this special.” The admin sees exactly which events are candidates and picks one, so an unintended row can no longer be selected silently. A category-based default (close to the old auto-selection behavior) is still applied on load, but applySource lets the admin override it — so correcting a bad default now costs nothing more than picking again.

Making it not recur

A design that auto-copies “the most recent row” or “the latest active row” as a template, default, or source carries risk the moment any temporary special-case or exception row enters that same list. Exclusion conditions added to prevent this tend to be reactive — one flag per exception noticed so far — and the same failure returns the next time a new kind of exception shows up.

What was done here instead was to surface the candidates as a visible list and let a human choose, rather than discarding the convenience of auto-selection outright — a category-based default is still applied. The difference is that when the default is wrong, nothing proceeds silently; a path to pick again is always visible. When reviewing a similar “auto-use the latest row” design elsewhere, it’s worth asking whether the selection can be shown to a human before reaching for another exclusion flag.

Frequently asked questions

Q1Why didn't the original 'copy the newest event' logic exclude the special event?

The exclusion condition was only is_pinned=false. The special event itself wasn't pinned, so it passed straight through that filter, and taking the newest row by event date simply landed on it. 'Is this special' and 'is this pinned' are independent, and nothing guaranteed one implied the other.

Q2Was the fix to add another exclusion condition for the copy source?

No. is_service=false still excludes services, but no new condition was added to detect 'is this a special event' automatically. Auto-selection itself was replaced with a dropdown, grouped by category, that lets an admin pick the source event by hand instead of guessing.

Q3Does this pattern show up outside this one workshop admin screen?

Any design that auto-copies the 'most recent' or 'latest active' row as a template or default hits the same failure once a one-off special row appears in that list. Showing the candidate to a human to pick is often the more durable fix than adding another exclusion flag.

Environment verified

  • Next.js App Router (admin UI) / Supabase
  • Fixed 2026-08-15

What this article is based on

  • TypeScript file lines 57-69commit e7c0894
  • TypeScript file lines 66-135commit d7c7cae

Every claim in this article comes from the records above. The repositories we operate are private so we cannot link to them, but which file, which lines, and at which commit we read them is recorded for every article. Nothing here is written from guesswork.