<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Vladimir Fedorov</title>
    <description>The latest articles on DEV Community by Vladimir Fedorov (@vladifedorov).</description>
    <link>https://dev.to/vladifedorov</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4092068%2F300688ed-f7b4-4983-a881-647c306bb7bb.png</url>
      <title>DEV Community: Vladimir Fedorov</title>
      <link>https://dev.to/vladifedorov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vladifedorov"/>
    <language>en</language>
    <item>
      <title>Your PWA can't wake the phone: alarms and notifications for plain HTML, without Android Studio</title>
      <dc:creator>Vladimir Fedorov</dc:creator>
      <pubDate>Sun, 30 Aug 2026 12:29:18 +0000</pubDate>
      <link>https://dev.to/vladifedorov/your-pwa-cant-wake-the-phone-alarms-and-notifications-for-plain-html-without-android-studio-5h22</link>
      <guid>https://dev.to/vladifedorov/your-pwa-cant-wake-the-phone-alarms-and-notifications-for-plain-html-without-android-studio-5h22</guid>
      <description>&lt;p&gt;&lt;strong&gt;Short answer: a PWA cannot ring an exact alarm on a phone.&lt;/strong&gt; There is no Web API that lets a web page — installed or not — fire at 10:33 sharp with the screen off. Web push exists, but it needs a server pushing at the right moment; it can't schedule locally. If an AI chat wrote you a timer, tracker or reminder as an HTML file and you want it to actually ring, you have three real options: wrap it with Capacitor (needs Android Studio/Xcode), rebuild it native, or drop it into a runner app that already owns the native alarms. Here is the honest breakdown.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm39ohzh2ephb5a9vclre.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm39ohzh2ephb5a9vclre.gif" alt="Demo: paste code → Run → lock-screen notification" width="360" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why doesn't my PWA alarm work when the screen is off?
&lt;/h2&gt;

&lt;p&gt;Because every mechanism a web page could use for timing dies with the page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;setTimeout&lt;/code&gt; / &lt;code&gt;setInterval&lt;/code&gt; freeze when the tab is backgrounded or the screen locks. Mobile browsers throttle and then suspend them.&lt;/li&gt;
&lt;li&gt;The Notifications API can &lt;em&gt;show&lt;/em&gt; a notification — but only while the page or its service worker is awake. Nothing wakes it at a scheduled time.&lt;/li&gt;
&lt;li&gt;The one spec designed exactly for this — the &lt;strong&gt;Notification Triggers API&lt;/strong&gt; — was abandoned after an origin trial in Chrome. It never shipped.&lt;/li&gt;
&lt;li&gt;Service workers are killed within minutes of going idle. They wake for a &lt;em&gt;push from a server&lt;/em&gt;, not for a local schedule.&lt;/li&gt;
&lt;li&gt;On iOS, web push requires the PWA to be added to the home screen (iOS 16.4+), and background delivery is best-effort, not exact.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the classic tutorial alarm clock — an &lt;code&gt;&amp;lt;input type="time"&amp;gt;&lt;/code&gt;, a loop checking &lt;code&gt;new Date()&lt;/code&gt;, an &lt;code&gt;&amp;lt;audio&amp;gt;&lt;/code&gt; tag — works only while the tab is open and the screen is on. Tutorials rarely mention that part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Doesn't web push solve notifications?
&lt;/h2&gt;

&lt;p&gt;Only if you run a server. Real web push is: HTTPS site + service worker + VAPID keys + a push service + a backend that stores subscriptions and sends the push at the right moment. That's fine for a product. It's absurd for a 40-line HTML timer a chat wrote you in ten seconds. And even then it gives you &lt;em&gt;push&lt;/em&gt; — a server-initiated message — not an &lt;em&gt;exact local alarm&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually works for an HTML file
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Exact alarms&lt;/th&gt;
&lt;th&gt;Works offline&lt;/th&gt;
&lt;th&gt;Needs a server&lt;/th&gt;
&lt;th&gt;Needs Android Studio / Xcode&lt;/th&gt;
&lt;th&gt;Effort&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PWA&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;for push, yes&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Capacitor wrap&lt;/td&gt;
&lt;td&gt;✅ (plugin)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Native rewrite&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTML runner app (e.g. Kapsula)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;~1 minute&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Capacitor&lt;/strong&gt; is the right tool if you're building a real product: it wraps your web code into a native project with full plugin access. The cost is the whole native toolchain — Android Studio, Gradle, signing, and per-app builds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A runner app&lt;/strong&gt; is the shortcut for personal tools. Full disclosure: I built one — &lt;a href="https://kapsula.app" rel="noopener noreferrer"&gt;Kapsula&lt;/a&gt; — a small (~3 MB) Android/iOS app, after my own AI-written workday timer silently failed to ring. You paste HTML (or add a file), it runs sandboxed and gets native powers through a tiny bridge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// inside your plain HTML file, running in Kapsula&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;kapsula&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;45&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// in 45 minutes&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Stand up&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;45 min done — switch position&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;sound&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That schedules a &lt;strong&gt;real OS alarm&lt;/strong&gt; (&lt;code&gt;AlarmManager&lt;/code&gt; with exact timing on Android, local notifications on iOS) — it fires to the second with the app closed and the screen off. The bridge also gives local storage that survives restarts, sound, and vibration. No account, no cloud, no build step: the free tier runs your own project alongside the demo, which is enough for a timer or a reminder board. And the same file still works in a desktop browser — the bridge just no-ops there.&lt;/p&gt;

&lt;p&gt;Honest limits, so you can pick correctly: Kapsula runs &lt;em&gt;self-contained&lt;/em&gt; HTML (one file — inline CSS/JS, or a URL to such a file). It is not a website-to-app converter: pages that need their own backend, cookies or cross-origin fetches won't work in the sandbox. For that, use Capacitor.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I share it with a friend?
&lt;/h2&gt;

&lt;p&gt;Send them the HTML file (or the link). They install the same runner, drop the file in, and have the same app with the same working alarms — no store publishing, no signing, no rebuild. Your code stays yours.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;PWA: great for offline content and installable UIs. &lt;strong&gt;No exact alarms, and push means running a server.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Capacitor/native: full power, full toolchain.&lt;/li&gt;
&lt;li&gt;Runner (Kapsula): the one-minute path for AI-generated personal tools that must actually ring.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If an assistant told you "just make it a PWA" and your alarm never fired — that's why. Docs for the bridge live at &lt;a href="https://kapsula.app/docs/" rel="noopener noreferrer"&gt;kapsula.app/docs&lt;/a&gt;, and the app is free on &lt;a href="https://play.google.com/store/apps/details?id=com.fedorovbtc.kapsula" rel="noopener noreferrer"&gt;Google Play&lt;/a&gt; and the &lt;a href="https://apps.apple.com/app/kapsula-app/id6797597679" rel="noopener noreferrer"&gt;App Store&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>pwa</category>
      <category>javascript</category>
      <category>android</category>
    </item>
    <item>
      <title>Running untrusted single-file HTML as a phone app: sandbox, exact alarms, and a 5-method bridge</title>
      <dc:creator>Vladimir Fedorov</dc:creator>
      <pubDate>Mon, 24 Aug 2026 10:04:01 +0000</pubDate>
      <link>https://dev.to/vladifedorov/running-untrusted-single-file-html-as-a-phone-app-sandbox-exact-alarms-and-a-5-method-bridge-2lc6</link>
      <guid>https://dev.to/vladifedorov/running-untrusted-single-file-html-as-a-phone-app-sandbox-exact-alarms-and-a-5-method-bridge-2lc6</guid>
      <description>&lt;p&gt;In July I asked Claude for a workday timer — 45 minutes seated, two minutes at the pull-up bar, 45 standing, lunch, repeat. One HTML file, zero dependencies, worked perfectly in a desktop browser. By lunch on a phone it had failed at its one job: &lt;strong&gt;the phone never rang&lt;/strong&gt;. Screen off → tab frozen → &lt;code&gt;setTimeout&lt;/code&gt; dead. Web Notifications on mobile are a shade entry at best, and only while the tab lives. Push needs a server. The Notification Triggers API — the one spec that was exactly this — died in an origin trial.&lt;/p&gt;

&lt;p&gt;So I built Kapsula: a small Android/iOS app that runs one HTML file (pasted code, a file, or a URL) as a project with real OS alarms. This post is about the three technical decisions that shaped it — the sandbox, the bridge, and the alarm plumbing — plus the bugs I hit on real devices.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm39ohzh2ephb5a9vclre.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm39ohzh2ephb5a9vclre.gif" alt="Demo: paste code → Run → 3 min → lock-screen notification" width="360" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Constraint zero: no server, ever
&lt;/h2&gt;

&lt;p&gt;I wanted this project to survive on evenings and weekends, which meant: no backend, no accounts, no sync, no analytics. Everything below follows from that constraint. It rules out push entirely — all notifications must be local, scheduled into the OS. It also means the untrusted code a user pastes from an AI chat must be contained client-side.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sandbox: an iframe without &lt;code&gt;allow-same-origin&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;User code runs in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;iframe&lt;/span&gt; &lt;span class="na"&gt;sandbox=&lt;/span&gt;&lt;span class="s"&gt;"allow-scripts allow-forms allow-modals allow-popups"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The deliberate omission is &lt;code&gt;allow-same-origin&lt;/code&gt;. The document gets an opaque origin, which buys a lot for free:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no access to the shell's &lt;code&gt;localStorage&lt;/code&gt;, cookies, IndexedDB — or anything else; its origin is unique and empty;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;parent.document&lt;/code&gt; is unreachable;&lt;/li&gt;
&lt;li&gt;network requests out of the sandbox are blocked (deliberate for now; &lt;code&gt;kapsula.fetch&lt;/code&gt; with per-project permission is planned).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One consequence hurts: &lt;code&gt;localStorage&lt;/code&gt; inside the sandbox doesn't survive a restart, because the opaque origin is new every time. So persistent state goes through the bridge only, keyed by project. The other consequence is the whole point: code I've never read physically cannot reach beyond its frame except through the five methods I expose.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bridge: postMessage RPC with a 15-second timeout
&lt;/h2&gt;

&lt;p&gt;The shell injects &lt;code&gt;kapsula-client.js&lt;/code&gt; into the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;. It's a plain RPC over &lt;code&gt;postMessage&lt;/code&gt;: every call gets an id, replies come back as &lt;code&gt;{kapsula: true, type: 'reply', id, ok, result}&lt;/code&gt;, and calls await a &lt;code&gt;hello&lt;/code&gt; handshake before flying. The whole API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;kapsula&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;                          &lt;span class="c1"&gt;// {name, projectId, platform, version, lang, limits}&lt;/span&gt;
&lt;span class="nx"&gt;kapsula&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;  &lt;span class="c1"&gt;// {id} — exact OS alarm&lt;/span&gt;
&lt;span class="nx"&gt;kapsula&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt;                  &lt;span class="c1"&gt;// no id = cancel all yours&lt;/span&gt;
&lt;span class="nx"&gt;kapsula&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;kapsula&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sound&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;beep&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;triple&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;alarm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;kapsula&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;haptics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;vibrate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt;
&lt;span class="nx"&gt;kapsula&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why so small: each method is something a browser tab &lt;em&gt;cannot&lt;/em&gt; do. Everything a tab can do (DOM, timers, canvas, Web Audio) is not duplicated. A small API has a second advantage: it fits in a prompt, and the code is written by LLMs — more on that below.&lt;/p&gt;

&lt;p&gt;The integration contract for a mini-app is one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;K&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kapsula&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// null in a plain browser&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;endAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Tea is ready&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same file works unchanged as a web page on desktop and as an app on the phone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Android alarms: exact, and three real-device bugs
&lt;/h2&gt;

&lt;p&gt;Alarms are local notifications with &lt;code&gt;allowWhileIdle&lt;/code&gt; plus &lt;code&gt;USE_EXACT_ALARM&lt;/code&gt; (auto-granted to alarm apps on Android 13+). On a real Samsung (Note20 Ultra, Android 13) delivery is second-exact — 13 ms between scheduled and shown in the logs — including when the app process is dead; the system revives it.&lt;/p&gt;

&lt;p&gt;The bugs were more interesting than the happy path:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The silent repeat alarm.&lt;/strong&gt; The plugin sets &lt;code&gt;FLAG_ONLY_ALERT_ONCE&lt;/code&gt;, and a project's notification id is a fixed slot number. If the previous notification is still sitting in the shade, the next one with the same id counts as an &lt;em&gt;update&lt;/em&gt; — and updates don't ring. The vibration I did feel turned out to be Samsung's NotificationReminder, not my notification. Fix: &lt;code&gt;removeDeliveredNotifications&lt;/code&gt; with the same id right before scheduling.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Android remembers deleted channels.&lt;/strong&gt; I changed a channel's sound, deleted the channel, recreated it with the same id — and got the old sound. Channel settings survive deletion. So channel ids carry a version suffix (&lt;code&gt;kapsula.&amp;lt;project&amp;gt;.v2&lt;/code&gt;) that bumps when the sound changes; old channels are left alone because scheduled notifications may still point at them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Battery optimization.&lt;/strong&gt; The classic OEM problem: an "optimized" app's alarms can drift or drop. Kapsula shows an amber hint card linking to the system screen. Samsung detail: the app is only visible in that list after switching the filter to "All" — that sentence had to go into the hint text.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  iOS: the sound lives on the notification
&lt;/h2&gt;

&lt;p&gt;No channels on iOS — the sound is attached per notification (&lt;code&gt;sound: 'kapsula_alarm.wav'&lt;/code&gt;, file in the bundle), trigger is calendar-based. Honest note: the simulator doesn't play notification sounds, so the first audible test happened on TestFlight on a real iPhone.&lt;/p&gt;

&lt;p&gt;Non-code surprise: answering "yes" to unrestricted web access in App Store Connect (URL projects can load anything) sets the age rating to 16+ automatically. Google Play's IARC does the same and lands on 18+. Fine for this audience, but know it going in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The state rule that AI-generated code always violates
&lt;/h2&gt;

&lt;p&gt;The most common bug in LLM-written mini-apps: state in JS variables and relative time (&lt;code&gt;setTimeout(fn, 3*60*1000)&lt;/code&gt;). The host may recreate the sandbox at any moment — for example when the user taps the notification and the project reopens. Everything in memory is gone, and a "3 minute" timer restarts from zero.&lt;/p&gt;

&lt;p&gt;The rule is three lines:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;State lives in &lt;code&gt;kapsula.storage&lt;/code&gt; only — and the UI renders from it.&lt;/li&gt;
&lt;li&gt;Time is absolute: store &lt;code&gt;endAt&lt;/code&gt;, never "seconds left".&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cancel&lt;/code&gt; before every &lt;code&gt;schedule&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The tea-timer example is 60 lines and shows all three: &lt;a href="https://kapsula.app/gallery" rel="noopener noreferrer"&gt;https://kapsula.app/gallery&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Teaching the model: a prompt instead of an SDK
&lt;/h2&gt;

&lt;p&gt;Since the code is written by ChatGPT and Claude rather than humans, the most important artifact isn't the app — it's the public API reference and a prompt. &lt;a href="https://kapsula.app/prompt" rel="noopener noreferrer"&gt;https://kapsula.app/prompt&lt;/a&gt; tells the model the sandbox rules (no &lt;code&gt;localStorage&lt;/code&gt;, no external &lt;code&gt;fetch&lt;/code&gt;, inline everything) and the bridge methods; the output works in a desktop browser and in the app. The bridge client is open source (&lt;code&gt;kapsula-client&lt;/code&gt; on npm / github.com/Fedorov191/kapsula-client) — not because anyone npm-installs it (the shell injects it), but so the types and docs have a stable address that crawlers and models can cite.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's deliberately missing
&lt;/h2&gt;

&lt;p&gt;No network out of the sandbox (yet), no background scripts without UI (Android WorkManager's ≥15-minute granularity and iOS's "whenever the system feels like it" can't honestly be presented as parity), no custom sounds (see channel gotcha above), no cloud. Next up: a deep link + share-sheet entry so chat-to-phone takes ten seconds, a dev panel surfacing JS errors from the sandbox, and &lt;code&gt;kapsula.fetch&lt;/code&gt; with per-project permission.&lt;/p&gt;

&lt;p&gt;Kapsula is free (one own project + demo), on Google Play and the App Store — links at &lt;a href="https://kapsula.app" rel="noopener noreferrer"&gt;https://kapsula.app&lt;/a&gt;. I'm the author; questions about the sandbox or the alarm plumbing welcome. Reports from Xiaomi/Huawei devices especially — Samsung is tested live, the rest of the OEM zoo is only as good as your bug reports.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>android</category>
      <category>ios</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
