Permissions
List permissions in manifest.json. Users see them on the install sheet.
Sensitive ones are also asked the first time you use them, in a prompt
showing your justifications text. Users can turn any of them off at any
time in Settings → Plugins, so plan for “no”.
"permissions": ["network", "location"],
"network": { "domains": ["api.open-meteo.com"] },
"justifications": { "location": "Weather where you are." }
| Permission | API | Sensitive |
|---|---|---|
network |
notch.net.fetch(url, { method, headers, body }) → .status, .ok, .headers, .json(), .text(), .bytes() |
no |
notifications |
notch.notify(title, body) |
no |
peek |
notch.peek({ title, detail?, icon? }): the collapsed notch grows into a banner for a few seconds, then shrinks back |
no |
clipboard:write |
notch.clipboard.writeText(text) |
no |
clipboard:read |
notch.clipboard.readText() (only while the user is using your widget) |
yes |
location |
notch.location.get() → { latitude, longitude, accuracyM }, city level (≥ 10 km) |
yes |
location:precise |
the same, at full accuracy | yes |
files |
notch.files.pick({ accept: [".csv"], multiple }) → FileRef[], then notch.files.read(ref) → .text() / .bytes() |
yes |
media:read |
notch.media.get(), notch.media.onChange(cb) |
no |
media:control |
notch.media.control("playPause" | "next" | "previous") |
yes |
timer:read |
notch.timer.get(), notch.timer.onChange(cb) |
no |
timer:control |
through the timer service | yes |
tasks:read |
notch.tasks.list() (today’s tasks) |
yes |
tasks:write |
notch.tasks.add(title), notch.tasks.complete(id) |
yes |
notes:read / notes:write |
through the notes service | yes |
background |
your tile keeps running while the notch is collapsed, within background.budget |
no (needs a justification) |
audio:play |
play sound; notch.audio.setPlaying(true) keeps your tile running while collapsed |
no |
alarms |
notch.alarms.set({ key, at, title, body?, repeatEveryMinutes?, maxRepeats?, startsAt?, completable? }), .cancel(key), .list(), .onChange(fn): the app rings them full screen with a sound |
no (needs a justification) |
Limits
- network: https only, only your
domains, no cookies, ≤ 10 s, ≤ 5 MB per response, 60 requests a minute. Keep secret API keys out of your package, because reviewers and users can read them. - notifications: 6 an hour, titled with your plugin’s name.
- peek: one a minute and 6 an hour; title ≤ 48 and detail ≤ 80
characters;
iconis one of the pill icons (PILL_ICONS). Your plugin’s name is shown under it. It doesn’t show while the panel is open, and the user can turn plugin pop-ups off in Settings, so treat it as a nudge for news worth a glance (rain soon, a review requested), never as the only place something is said. Ongoing status belongs in the pill. - alarms: 100 scheduled per plugin, title ≤ 64 and body ≤ 240
characters,
atfrom an hour ago to 400 days ahead, 60set/cancelcalls a minute. - files: ≤ 5 MB per file. Picking works only while the user is using
your widget. A picked file stays readable by
handleuntil the user removes it.
Running in the background
Normally your frames are destroyed when the notch collapses and reloaded when it opens. Two permissions change that for your tile:
-
backgroundkeeps it running whenever it’s on the board. Declare a CPU budget, and say why injustifications:"permissions": ["background"], "background": { "budget": "low" }, "justifications": { "background": "Checks your build status every minute." }Budget CPU while collapsed (share of one core) low2% medium5% high10% -
audio:playlets you play sound (autoplay is allowed). Withnetworktoo,<audio>may stream from yournetwork.domains(still https only;fetchstays blocked). Callnotch.audio.setPlaying(true)when playback starts andfalsewhen it stops: while you’re playing, that frame (your tile, or your expanded view) keeps running when the notch collapses or the user goes back to the board, on themediumbudget (or yourbackgroundbudget if you have one). If the frame reloads or is paused, it counts as stopped. Tile and expanded view are separate pages: if both can play, keep them in step (e.g. throughnotch.storage.plugin) so the user doesn’t hear two streams.
While collapsed, visibility is false and the tile isn’t drawn, so
requestAnimationFrame doesn’t run: use timers, and do as little as you
can. Going over your budget for 10 seconds in a row pauses your plugin
until the user resumes it. While the notch is open, every plugin’s limit
is 10% of a core.
Checking answers
const ctx = await notch.ready();
if (ctx.permissions.location === "denied") showCityPicker();
notch.on("permissions", (p) => { /* the user changed something */ });
ask means the user hasn’t answered yet. The first call asks them.
Alarms
With alarms you schedule alerts and the app rings them: at at it
covers the screen with an alert (your plugin’s name, your title and
body) and plays a sound, even while the notch is closed and your plugin
isn’t running. The user can Dismiss or Snooze (5, 10 or 15 min;
with startsAt, never past it, plus “Until it starts”). With startsAt
the alert counts down to it (“Starts 10:00 AM · in 10 min”). With
completable: true it also offers Mark done: the alarm becomes done
with completed: true, and notch.alarms.onChange tells a running
plugin. If nobody
answers within a minute it counts as missed and, with
repeatEveryMinutes, rings again that much later, up to maxRepeats
times (default 3).
await notch.alarms.set({
key: "standup-2026-10-22", // yours; setting it again replaces it
at: Date.parse("2026-10-22T09:50"),
title: "Standup",
body: "Room 4",
startsAt: Date.parse("2026-10-22T10:00"),
completable: true,
repeatEveryMinutes: 5,
});
const mine = await notch.alarms.list(); // state: scheduled | ringing | done | missed
notch.alarms.onChange((all) => {
for (const a of all) if (a.completed) markDone(a.key);
});
await notch.alarms.cancel("standup-2026-10-22");
Alarms ring only on this PC and aren’t synced. An alarm that came due while
the app was closed rings when it starts if it’s at most an hour late, and
is missed otherwise. Dismissed and missed alarms stay in list() for a
week, so you can tell what happened.