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.
How enforcement works
Section titled “How enforcement works”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.
Restriction shapes
Section titled “Restriction shapes”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.
The not-now screen
Section titled “The not-now screen”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 INinH:MM:SSorM:SS, ticking every second, or the hint “opens after a task is marked done” whenuntilisnull. - 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.
“I actually need access”
Section titled ““I actually need access””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:
-
Logs the goal
The reason you gave is written to
ops.override_logand anoverride-usedevent fires, so overrides are visible, not silently granted. -
Grants a window
You get one viewing window of at least 5 minutes, or the restriction’s own window if that’s longer.
-
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
Section titled “Settings”/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+windowMinutesfor frequency,limit+windowMinutesfor 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, oroff.
The page is backed by notnow.restrictions.list / .save / .delete.
Extension feed quotas
Section titled “Extension feed quotas”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], orarticleas a fallback) count once they cross 50% viewport visibility. - Counts sync to the server every 3 items and on every
visibilitychange, vianotnow.counters.sync, windowed to the restriction’swindowMinutesboundary 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.