The working product document. The public landing page lives in README.md; the deep technical design lives in TECHNICAL_DESIGN.md. Image paths below resolve from the repo root.
A privacy-first Chrome (Manifest V3) extension that tracks how long you spend on chosen sites and enforces a daily budget per domain. When the budget for the day is spent, the site is intercepted and replaced by a calm reminder page until the next day.
Working name: Curfew (alternatives if ever needed: Recess, Curb; see §9). Target order: use it yourself → prove the value → publish to the Chrome Web Store.
![]() |
![]() |
![]() |
![]() |
Popup dashboard with budgets and passes · the settings · the wall · a Willpower Protection challenge.
“Block the whole site” tools (blocklist approach) are too blunt: YouTube is needed for study, X for work. The problem is the infinite scroll, not the site. “Screen time” dashboards show you wasted 3 hours but don’t stop you.
Curfew combines both sides: visibility (what actually took time) and enforcement (a hard wall after the budget is spent), for the sites YOU choose, with budgets YOU set.
The metaphor is a curfew: the site is open during the day, and at a moment you choose it closes. The extension’s single job: meter time on configured patterns and make the site unreachable when the meter hits the budget.
chrome.storage.local.
No accounts, no sync, no telemetry, no server, no remote code. This is
also what a Web Store privacy policy can say in one sentence.<all_urls> (see §4.3).x.com), subdomains (*.reddit.com), optional
path rules (github.com/hm...: v1 keeps it domain-level; paths are a
roadmap item). Grammar is defined once in common/patterns.js and
tested: example.com = that host only; *.example.com = the domain
AND all subdomains. Our matcher is the single source of truth; its
mapping onto permission prompts (match-patterns) and DNR conditions
is specified in the tech doc (§5.2, §9.3).enabled toggle + global master switch (pause all budgets).blocked.html): shows the domain, the time spent today, and a
“stay anyway” button ONLY inside a 15-minute pass window with an
honest counter (see §3.5).The model is deliberately two-way: either you play by the rules (budget + passes), or you consciously opt out (disable the site or the whole app, both password-gated). No in-between “allow today” loophole.
chrome:// extensions page, devtools,
incognito, or other browsers. Be honest in the README: this is a habit
tool, not a lock.Deep technical specification: full stack, module contracts, algorithms, DNR rule lifecycle, testing: docs/TECHNICAL_DESIGN.md. Where details differ, the tech doc wins; this section stays the summary.
curfew-extension/
manifest.json
docs/ # technical design doc (see §4 intro)
icons/ (#16/32/48/128, later M3)
_locales/ # en, ru; strings only in UI
src/
service-worker.js # event-driven core (see below)
blocked.html/.js/.css # the Curfew page
popup.html/.js/.css # the dashboard
options.html/.js/.css # settings + budgets editor
common/
time.js # day key, minutes helpers (pure, testable)
patterns.js # pattern → URL match (pure, testable)
budget.js # open/closed decision, state machine (pure)
storage.js # typed storage wrapper + migrations
tests/ # node --test for common/*.js + invariants
package.json # no build step for v1: tests/runner only
README.md
Event-driven only; no timers that must survive sleep (storage persists, SW restarts on events):
| Event | Action |
|---|---|
tabs.onUpdated / onActivated + windows.onFocusChanged |
compute current tracked pattern. tab.url is visible ONLY for hosts the user granted (browser-enforced); the events themselves need no tabs permission |
chrome.idle.onStateChanged (idle 60s) |
stop/start counting |
chrome.alarms (every 5 min) |
flush usage; daily reset at local midnight; re-check the still-open tab: if it hit its budget while sitting on it → add block rule + redirect it (covers the “tab was already open” gap) |
chrome.permissions (optional per-site host access) |
one prompt per site the user adds (see §4.3) |
| DNR dynamic rules | budget exhausted → request-level redirect to blocked.html; rule removed on reset / “allow rest of day” (see §4.4) |
Orientation copy. The authoritative schema (per-pattern
passes,sessionfor SW-restart recovery,runtimefor pass windows) is tech doc §6.1.{ "schema": 1, "config": { "masterEnabled": true, "graceSeconds": 10, "passMinutes": 15, "items": [ { "id": "u1", "pattern": "*.reddit.com", "budgetMinutes": 30, "enabled": true } // pattern grammar: hostname with optional leading *. // No port/path in v1. ] }, "usage": { "days": { "2026-09-02": { "patternSeconds": { "*.reddit.com": 772 }, "passes": 1, "bySite": { "reddit.com": 772 } // resolved site for dashboard } } }, "settings": { "version": 1 } }
- Day key = local date at first write of the day (
YYYY-MM-DDbased on the local timezone; DST handled by comparingdatehour-by-hour).storage.localquota (10MB) is enough for years of one-day rows; prune 60 days on import/reset (default keep 60).- Timezone change: cap on the current day’s totals (the day is local by definition; a travel across timezones can only shorten the day, and the daily reset recomputes).
Manifest permissions (all SILENT: the browser shows no install warnings):
"permissions": [
"storage", // budgets + usage, local only
"idle", // active-time detection (idle 60s)
"alarms", // flush, midnight reset, open-tab re-check
"declarativeNetRequestWithHostAccess", // request-level redirects; no
// implicit host access, no warning
"contextMenus" // right-click "Add this site to Curfew"
],
"optional_host_permissions": ["*://*/*"] // nothing granted at install
Deliberately NOT present (this is the positioning, verified against Chrome docs):
tabs: no history/URL visibility as a blanket (url/title/favIcon
are only present for hosts the user granted, browser-enforced);host_permissions / <all_urls>: no “read and change all
your data” warning, ever;webNavigation, scripting, cookies, notifications,
identity, unlimitedStorage.Per-site consent model, the only way access ever grows:
chrome.permissions.request
(must come from an extension page with a user gesture, by design)
for that site’s pattern, e.g. *://*.reddit.com/*;tab.url becomes readable
for that host (and only that host, browser-enforced);Note on chrome.tabs.update/reload/create: per the tabs API docs
these need NO permission at all, so the fallback redirect (and the
alarm re-check) require nothing extra.
Onboarding copy: “Curfew sees only the sites you add. Data never leaves your browser. Uninstall = gone.”
redirect → extensionPath "/src/blocked.html", main_frame,
condition mapped from common/patterns.js (||domain/ for wildcard
patterns, anchored filter for exact ones; the mapping is pure and
tested). The browser enforces it at the request boundary: no flash of
the site, no race, works even while the SW is asleep.blocked.html must be listed in web_accessible_resources (required
for DNR redirect to an extension path). It is the ONLY resource
exposed; it contains local display + bundled JS, no remote anything.chrome.tabs.update redirect (no
permission needed per tabs API docs; slower and flashy, not the default).| M | What | Acceptance |
|---|---|---|
| M0 | Skeleton: manifest v3 (§4.3 permission set), popup w/ add-current-tab, options page w/ pattern+budget editor, local storage wrapper | loads unpacked; add x.com → permission prompt → appears in both pages; no crashes |
| M1 | Tracker + quota + interstitial | 10 min on reddit → wall at budget-0; grace does not count; idle pauses counting; midnight reset; “add side note” not yet |
| M2 | Dashboard (daily totals, per-site bars, pass counter) + export/import + usage pruning | popup shows yesterday vs today; export→import round-trip |
| M3 | Store package: icons, screenshots (3), privacy policy page, listing copy, review checklist (permissions, offstore repo, no tracking), publish | appears in store, installs, works |
| M4+ | Ideas: weekday-aware budgets; path rules; per-site “blocked until” button; optional sync via file; Firefox WebExtension port; stats export CSV |
v1 = M0..M2 as the personal product; M3 once you are happy.
chrome.runtime.getURL), never a
remote page.common/* has no chrome
import): node --test covers time/day/pattern/budget decision; a
DOM/db harness is not needed for v1.fetch, no XHR, no external scripts
(default MV3 CSP blocks remote code anyway; keep it that way), no
update polls, no error reporting. The only “bytes out” in the whole
extension is the user-initiated export file.chrome.storage.local until you export/import manually;
no permission asks for anything else while you use it.If curfew fails a name check (store / npm scope / GitHub):
manifest.json and package.json.curfew-extension/.npm test (runs ESLint then node --test; dev-only deps,
nothing of them ships in the extension package).