Skip to content

Not-now

Not-now is Subspace’s answer to “I’ll just check mail for a second.” You define restrictions on specific surfaces, mail, the task queue, instant messaging, and the server enforces them on the data itself. A locked surface never receives the data it asked for, so reloading the page, restarting the app, or reinstalling the extension changes nothing: the lock lives in Postgres, not in client state.

Gated surfaces are wrapped server-side by gate(), sitting directly on the surface’s data procedures (mail.view, mail.thread, ops.tasks.view, and the IM equivalents). A gated query returns one of two shapes:

// unlocked
{ locked: false, /* ...normal read model... */ }
// locked
{ locked: true, surface, reason: 'frequency' | 'quota' | 'condition' | 'mode', notNow, until }

Because the lock check runs on the query itself, not on a client-side wrapper, there is no “just don’t render the lock screen” bypass: the underlying data genuinely never leaves the server. until is the ISO timestamp the surface reopens (null for a condition lock, since those depend on an event rather than a clock). Frequency “opens” and quota counters live in ops.surface_counters and survive reload and reinstall for the same reason.

Each restriction is one row: a shape, a target surface, a rule, and a screen to show while locked.

Shape Rule Behavior
frequency {everyMinutes, windowMinutes} The surface opens for a windowMinutes viewing window at most once every everyMinutes. The first data load consumes the window; windowMinutes: 0 closes it immediately after that first view.
quota {limit, windowMinutes} Locks once limit counted items accumulate in the trailing window. Counted items come from the extension’s feed-item sync.
condition {afterTime: iso} or the default {withinMinutes} Opens at a wall-clock time, or opens only for a stretch after a task is marked done.

Every restriction also carries a screen: {kind: 'quotes', quotes: [...]}, {kind: 'images', images: [url, ...]}, or {kind: 'flashcards'}, shown on the not-now screen while that restriction is active.

When a surface’s query comes back locked, the surface renders the not-now screen instead of its normal content:

  • An amber rule chip describing why: frequency rule · once every 2h, quota rule · N items / M min, or <mode> mode.
  • A live countdown, <SURFACE> OPENS IN in H:MM:SS or M:SS, ticking every second, or the hint “opens after a task is marked done” when until is null.
  • The configured screen: a quote (picked deterministically by hour, so it’s stable across reloads within the same hour) or an image.
  • A dark “Review due flashcards instead” CTA linking to the spaced-repetition review page, and a ghost Back.

When the countdown hits zero, the surface’s query refetches automatically, no manual reload needed. On the mail page, while unlocked and a mail frequency restriction exists, a small amber viewable every Nh chip shows in the header as a standing reminder that the window is finite.

Every lock screen carries a dotted-underline override link: “I actually need access.” Clicking it opens a dialog, “What do you need from <surface> right now?”, with a reason textarea and a friction line pulled from your override history, “3rd override this week.”

Submitting:

  1. Logs the goal

    The reason you gave is written to ops.override_log and an override-used event fires, so overrides are visible, not silently granted.

  2. Grants a window

    You get one viewing window of at least 5 minutes, or the restriction’s own window if that’s longer.

  3. Unlocks without reload

    The surface’s query invalidates and the data loads immediately. The window is server-side, so it survives a reload, it isn’t a client-side “just this once.”

The escalating friction line is the point: overriding is always possible, but it isn’t free, and the count is a running tally you see every time.

Two modes sit above individual restrictions and are evaluated first, so a mode-driven lock reports reason: 'mode':

  • No-comms locks every comms surface: mail, instant messaging, social feeds, and ingestion (matched by prefix).
  • Focus locks everything No-comms locks, plus the task queue.

Only ultra-severity notifications pierce an active mode; everything else queues silently until the mode lifts. A mode can carry an auto-expiry until, or the settings toggle can flip it on/off with no expiry at all.

/settings/not-now (sidebar settings group) is where restrictions and modes live:

  • A restrictions table (Shape | Rule | Not-now screen), each row deletable.
  • A “New restriction” inline form: pick a shape, a surface, and the shape-specific numeric inputs (everyMinutes + windowMinutes for frequency, limit + windowMinutes for quota, no extra fields for condition), plus a screen-kind select with a one-quote-per-line textarea when the screen is quotes.
  • Two mode cards, No-comms and Focus, each with a toggle switch and a status line reading on · until HH:MM, on, or off.

The page is backed by notnow.restrictions.list / .save / .delete.

The Chrome extension’s content script enforces quota restrictions on social feeds directly in the DOM, so a locked feed is actually hidden, not just visually dimmed:

  • The site key is the hostname (www. stripped).
  • Feed items ([data-feed-item], or article as a fallback) count once they cross 50% viewport visibility.
  • Counts sync to the server every 3 items and on every visibilitychange, via notnow.counters.sync, windowed to the restriction’s windowMinutes boundary and kept idempotent server-side.
  • Once the synced total reaches the limit, the feed container is hidden, the notifications button is hidden, and a full-page overlay reads “not now, quota spent, reopens in N min.”
  • A fresh tab re-checks the current total on load, so quota spent on one device is respected on another.