One App, Two Websites: Instant Ham Radio Reminders

Last time I wrote why I use Bark. This time I am writing what I actually did to get it working. No tricks, just three things to get ready first. One app on my phone, Bark. Two websites, GitHub and Cloudflare. I keep the code on GitHub. It runs on Cloudflare. The push lands in Bark. I did every step below in a browser, with my own hands. When you finish, your own Worker will be running, and your phone will have got one test push. Then the next article can talk about satellites.

The same code is also in my GitHub repository, https://github.com/bg7bag/bark-relay-worker — if you would rather copy it from there, open src/index.js on GitHub and use GitHub’s own copy button. The full code is also pasted below in this article, so you do not have to leave this page to get it.

Before Step 1, copy the code first. Scroll down to Code 1 and Code 2 below, copy them into a text file on your computer, and keep that file open. You will need them in Step 2 and Step 3, and they are not on the screen yet when you get to those steps. Copying first saves you scrolling back and forth later.

Before I started, I had three things ready: a computer with a browser and a terminal; an iPhone; and an email address I can receive mail at, for registering accounts. I also had a piece of paper ready, for two things I would need to write down by hand: my Bark device key, and a shared passphrase I would make up myself.

Step 1: Download Bark and find your device key

I started with Bark, because if it is not on the phone, the rest is pointless.

  1. Open the App Store on your iPhone, tap Search, and type Bark. Look for the Bark that turns a web request into a push notification, not any other app that happens to share the name. Tap Get, confirm with your passcode or Face ID, and wait for the download to finish.
  2. Open Bark. The first time it opens, it asks whether it may send you notifications. Tap Allow. This permission is the whole point: if you tap Don’t Allow, no push will ever arrive, no matter how correct everything else is. If you already tapped Don’t Allow by mistake, go to Settings, then Notifications, then Bark, and turn Allow Notifications back on.
  3. On Bark’s home screen, look at the card at the very top, the one with a QR code. Under that card is a string of 22 characters. That string is your device key. Tap it to copy it, and save it somewhere only you can see, such as your password manager. Do not post it anywhere, and do not share a screenshot that shows it: anyone who has that string can push to your phone.

Check at this point: Bark installed on your iPhone, notifications allowed, and your device key saved in your own hands. If the app you opened does not show a QR card with a 22-character string on its home screen, you installed the wrong app sharing the name. Delete it and search again.

Step 2: Register on GitHub and upload the code

GitHub is where I keep the code, so a reader can find it again later.

  1. Open your browser, go to github.com, and click Sign up at the top right.
  2. Enter your email first and click Continue; then go to your inbox, find the verification code GitHub sent you, and enter it.
  3. Set a password and click Continue; if the password does not meet the rules shown on the page, the page will tell you on the spot what is wrong — change it until it passes.
  4. Pick a username and click Continue; if the name is already taken, the page will tell you to pick another one — keep changing it until you find one that is free.
  5. Finish the human verification on the page and click Create account; then go to your inbox and click the verification link — only then is the account really active.
  6. After signing in, click the plus sign at the top right, choose New repository, name it bark-relay-worker, choose Public, and click Create repository.
  7. Inside the repository, click Add file, choose Create new file, name it src/index.js, paste in Code 1, the one you copied at the start, and click Commit changes.
  8. Do the same again for a second file: name it wrangler.toml, paste in Code 2, the one you copied at the start, and click Commit changes. This file is only for command-line deployment later. The browser deployment in Step 3 does not use it. Keep it in the repository as a backup, so you do not have to come back for it later.

You do not need a GitHub token for this. A token is only for deploying from the command line later; everything here is done in the browser, so skip that part for now.

Check at this point: a repository containing src/index.js and wrangler.toml.

Step 3: Register on Cloudflare and upload the code

Cloudflare is where the code actually runs.

  1. Open your browser, go to dash.cloudflare.com, click Sign up, enter your email, set a password, and click Create account; then go to your inbox and click the verification link — only then is the account really active — and sign in with it.
  2. If you have more than one account under your login, make sure you are in your own personal account before going on.
  3. On the left, find Workers & Pages, click Create, choose Worker, name it bark-relay-worker, and click Deploy. Use this name exactly, so the next article in this series can find it by name.
  4. Click Edit code, delete everything in the editor, paste in Code 1, the one you copied at the start, and click Deploy.
  5. Go back to the Worker’s page and look at the Workers.dev section: if it shows your Worker’s address, the route is already on; if opening the address shows “There is nothing here yet”, turn on this script’s workers.dev route separately.
  6. Open your Worker’s address in a browser.

Check at this point: the page showing “bark-relay-worker is running”, with the POST JSON to /push instructions underneath. Seeing that text means the Worker is up.

Step 4: Fill in the two secrets

  1. On the Worker’s page, click Settings, scroll back to the very top, find the “Variables and Secrets” section, and click Add variable at its top right.
  2. Add the first variable: name it BARK_KEY, and set its value to the device key in the Bark app on your phone. Tick the “Secret” box next to the value, and click Add variable and deploy. Copy only the 22-character string; do not copy the URL, slashes, or parameters along with it.
  3. Click Add variable again for the second variable: name it RELAY_SECRET, set its value to a shared passphrase you make up yourself, tick “Secret” the same way, and click Add variable and deploy. Use letters and numbers only, no spaces, and write it on your paper now. Cloudflare encrypts secrets, so you cannot view this passphrase later. If you forget it, you can only replace it on the Worker, not read it back.
  4. Go back to the settings page and look at the “Variables and Secrets” section.

Check at this point: two rows, BARK_KEY and RELAY_SECRET, with both values shown as encrypted and no plaintext visible.

Step 5: Run one test command yourself

Open a terminal first: on a Mac, open Spotlight and type Terminal; on Windows, open Start and type Terminal. I typed this in my terminal, with my own address and passphrase in place of the placeholders. Copying it blind will not work — change those two parts first, then press Enter:

curl -X POST https://your-worker-address/push 
  -H "Content-Type: application/json" 
  -H "X-Relay-Secret: your-shared-passphrase" 
  -d '{"title": "Test", "body": "Bark relay test", "group": "test"}'

Check at this point: the command returns {"delivered":true}, and at the same time your phone receives a push titled “Test”. If either one is missing, it is not working yet. Do not move on.

If the test does not pass, check in this order, do not guess:

  • The command returns 401 Unauthorized. Your RELAY_SECRET in the command does not match the one on the Worker. Check the passphrase on your paper, type it again, and run the command again. If you lost the paper, replace the secret on the Worker with a new passphrase, deploy again, and use the new one.
  • The command returns 400, device not found. Your BARK_KEY is not the bare key. Go back to Step 4 and check you copied only the 22-character string, with no URL, slashes, or parameters along with it.
  • The command returns an error about a missing key, or no delivered field at all. Go back to Step 4 and check both secrets are set and both show as encrypted. A secret that was never saved cannot be used.
  • The command returns delivered:true, but no push arrives on the phone. Go back to Step 1 and check notifications are allowed for Bark in Settings, then Notifications, then Bark. A delivered push with notifications off will never show.
  • Your computer says the curl command was not found. Curl is already on a Mac and on modern Windows. If yours says it is not found, you are not in a terminal, or you typed the command into a search box. Open Terminal again, as above, and paste the command there.

The success checklist: all four must pass before you move on

I do not move on until all four are true. If one fails, I go back to that step, I do not guess.

  1. The Worker address opens and shows “bark-relay-worker is running”;
  2. The settings page shows both BARK_KEY and RELAY_SECRET, both encrypted;
  3. The test command returns delivered:true;
  4. Your phone received the push titled “Test”.

When all four pass, any script that knows the Worker address and the shared passphrase can push a message to your own iPhone, without handing over your device key.

Appendix: three places I got stuck

  1. The workers.dev route does not open by itself: when you upload through an API or a script, the script exists and the address exists, but opening it shows “There is nothing here yet”. Turn on this script’s workers.dev route separately, and the address will really work.
  2. BARK_KEY must be the bare key: if you copy it with the /title/body?group=... tail from an example address, Bark will answer 400, device not found. The path and parameters in an example address are not part of the key — delete them and keep only the key itself.
  3. The compatibility date is for command-line deployment only: the browser deployment in Step 3 does not ask for a date, so do not look for a date field in the editor. The date in Code 2 is the date I uploaded. If you deploy by command line later, you may leave it as it is, or change it to the UTC date of your upload day — never a future date, or the upload will be rejected outright.

In this series: iPhone Low-Power Instant Ham Radio Reminders — why I use Bark. Satellite Pass Reminders by Hand: Start at 30 Degrees — use this relay for satellite pass reminders, starting at 30 degrees. Bark + SDR: Never Miss a Satellite Recording Again — a script watches the recording folder and pushes when a satellite recording finishes. Bark + SDR: Get Pushed Only When the Repeater Talks — a push only when squelch opens on one repeater frequency. Bark + SDR: A Push When the Band Opens to You — a push when WSJT-X decodes what you want. Bark + SDR: Know When Recording Finishes or Fails — a push when a long recording finishes, when disk space runs low, or when the expected file never appears.

Code 1: the relay code (src/index.js, copy from here)

/**
 * bark-relay-worker — a minimal Cloudflare Worker that forwards a JSON
 * request to Bark (iOS push) and throttles duplicate messages.
 *
 * Deploy it to YOUR OWN Cloudflare account, with YOUR OWN Bark key.
 * Nobody else's phone is involved: this Worker only ever pushes to the
 * key you configure as a secret.
 *
 * Secrets (wrangler secret put):
 *   BARK_KEY      - your Bark device key (required)
 *   RELAY_SECRET  - optional shared secret; when set, every request must
 *                   carry header "X-Relay-Secret" with this value, so a
 *                   stranger who finds your Worker URL cannot push to
 *                   your phone.
 * Vars (wrangler.toml):
 *   BARK_SERVER   - default https://api.day.app, or your self-hosted
 *                   bark-server URL
 *   ALLOW_USER_KEYS - "true" only if you deliberately run a shared relay:
 *                   then a request may carry its own "device_key" and the
 *                   push goes to that key, not yours. Default off.
 *
 * Throttling: identical messages (same group + title + body) are sent
 * at most once per 10 minutes. This is a best-effort in-memory window:
 * it holds within a Worker isolate; for a strict cross-isolate throttle,
 * put the same key behind Workers KV (see README roadmap).
 */

const WINDOW_MS = 10 * 60 * 1000;
const seen = new Map(); // key -> timestamp ms

function throttleKey(group, title, body) {
  return `${group || ""}n${title || ""}n${body || ""}`;
}

function isDuplicate(key) {
  const now = Date.now();
  for (const [k, t] of seen) {
    if (now - t > WINDOW_MS) seen.delete(k);
  }
  if (seen.has(key)) return true;
  seen.set(key, now);
  return false;
}

export default {
  async fetch(request, env) {
    const url = new URL(request.url);

    if (request.method === "GET" && url.pathname === "/") {
      return new Response(
        "bark-relay-worker is running.n" +
          "POST JSON to /push: {"title": "...", "body": "...", " +
          ""group": "optional", "sound": "optional", "level": "optional"}n" +
          "Set BARK_KEY as a secret. If RELAY_SECRET is set, send header " +
          "X-Relay-Secret with it.n",
        { headers: { "Content-Type": "text/plain" } }
      );
    }

    if (request.method !== "POST" || url.pathname !== "/push") {
      return new Response("Not found", { status: 404 });
    }

    if (env.RELAY_SECRET) {
      if (request.headers.get("X-Relay-Secret") !== env.RELAY_SECRET) {
        return new Response("Unauthorized", { status: 401 });
      }
    }

    let payload;
    try {
      payload = await request.json();
    } catch {
      return new Response("Expected a JSON body", { status: 400 });
    }
    const { title, body, group, sound, level } = payload;
    if (!title && !body) {
      return new Response("Provide at least a title or a body", { status: 400 });
    }

    const key = throttleKey(group, title, body);
    if (isDuplicate(key)) {
      return Response.json({ delivered: false, reason: "throttled-duplicate-10min" });
    }

    let deviceKey = env.BARK_KEY;
    if (env.ALLOW_USER_KEYS === "true" && payload.device_key) {
      deviceKey = payload.device_key;
    }
    if (!deviceKey) {
      return new Response("BARK_KEY secret is not configured", { status: 500 });
    }

    const server = (env.BARK_SERVER || "https://api.day.app").replace(//$/, "");
    const barkPayload = { device_key: deviceKey, title: title || "", body: body || "" };
    if (group) barkPayload.group = group;
    if (sound) barkPayload.sound = sound;
    if (level) barkPayload.level = level;

    const resp = await fetch(`${server}/push`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(barkPayload),
    });
    if (!resp.ok) {
      seen.delete(key);
      const detail = (await resp.text()).slice(0, 200);
      return new Response(`Bark rejected the push (HTTP ${resp.status}): ${detail}`, { status: 502 });
    }
    return Response.json({ delivered: true });
  },
};

What it does is simple. First it checks the passphrase. If it is wrong, it stops there. Then it reads the title, body, group, sound and level out of the request. The same message goes out at most once in 10 minutes. Then it sends the message to Bark’s server.

Code 2: the deployment configuration (wrangler.toml, for command-line deployment)

name = "bark-relay-worker"
main = "src/index.js"
compatibility_date = "2026-11-02"

# Optional vars. Secrets are set with `wrangler secret put`, never here.
[vars]
BARK_SERVER = "https://api.day.app"
ALLOW_USER_KEYS = "false"

Never write a secret into either block of code; secrets are filled in only in the Cloudflare dashboard. The compatibility date in Code 2 is for command-line deployment only, as explained in the appendix. The browser deployment in Step 3 does not use this file.

一个 App,两个网站:业余无线电即时提醒

上一篇我写了为什么用 Bark。这篇写我实际是怎么把它跑起来的。没什么花招,先准备三样东西:手机上的一个 App,Bark;两个网站,GitHub 和 Cloudflare。代码我放在 GitHub 上,跑在 Cloudflare 上,推送落在 Bark 里。下面每一步我都是在浏览器里亲手做的。做完,你自己的 Worker 就在跑了,手机也收到了一条测试推送。下一篇就可以讲卫星了。

同样的代码也在我的 GitHub 仓库里,https://github.com/bg7bag/bark-relay-worker ——要是你更愿意从那儿抄,打开 src/index.js,用 GitHub 自己的复制按钮就行。全文代码这篇里也贴了,不用离开这个页面也能拿到。

动手之前,先把代码抄好。往下翻到代码一和代码二,把它们复制到你电脑上的一个文本文件里,文件保持打开。第二步、第三步要用到它们,可你走到那两步的时候,代码还没出现在屏幕上。先抄好,后面就不用前后翻了。

动手之前,我准备了三样东西:一台带浏览器和终端的电脑;一部 iPhone;一个能收邮件的邮箱,用来注册账号。我还准备了一张纸,留着亲手写两样东西:Bark 设备密钥,和一个我自己想的共享口令。

第一步:下载 Bark,找到你的设备密钥

我先从 Bark 开始,因为手机上没有它,后面全白搭。

  1. 在 iPhone 上打开 App Store,点搜索,输入 Bark。找那个把网络请求变成推送的 Bark,别装成同名的别的 App。点获取,用密码或者面容确认,等下载完。
  2. 打开 Bark。第一次打开,它会问你能不能给你发通知。点允许。这个权限就是关键:要是点了不允许,后面全对了,推送也永远到不了。要是已经手滑点了不允许,就去设置、通知、Bark,把允许通知重新打开。
  3. 在 Bark 首页,看最上面那张带二维码的卡片。卡片下面是一串 22 个字符。这串就是你的设备密钥。点它复制下来,存在只有你自己能看到的地方,比如密码管理器。别发到任何地方,也别分享带它的截图:拿到这串的人就能往你手机上推消息。

到这一步,你应该看到:Bark 装在 iPhone 上了,通知开了,设备密钥在你自己手里。要是你打开的 App 首页没有带二维码的卡片、下面也没有 22 个字符,说明你装错了同名的别的 App。删掉,重新搜。

第二步:注册 GitHub,上传代码

GitHub 是我放代码的地方,以后读者还能找回来。

  1. 打开浏览器,去 github.com,点右上角注册。
  2. 先填邮箱,点继续;再去收件箱找 GitHub 发来的验证码,填进去。
  3. 设一个密码,点继续;密码要是不符合页面上写着的规则,页面会当场告诉你错在哪——改到通过为止。
  4. 起个用户名,点继续;名字要是已经被占了,页面会让你换一个——换到空着的为止。
  5. 做完页面上的人机验证,点创建账号;再去收件箱点验证链接——点了才算真的激活。
  6. 登录之后,点右上角加号,选新建仓库,名字填 bark-relay-worker,选公开,点创建仓库。
  7. 进仓库,点添加文件,选新建文件,名字填 src/index.js,把开头抄好的代码一粘贴进去,点提交修改。
  8. 再来一次,建第二个文件:名字填 wrangler.toml,把开头抄好的代码二粘贴进去,点提交修改。这个文件只是以后命令行部署才用。第三步的浏览器部署用不着它。放在仓库里当备份,以后就不用再回来找它了。

这一步用不着 GitHub 的 Token。Token 只是以后从命令行部署才用,全在浏览器里做的这一步,跳过就行。

到这一步,你应该看到:仓库里有 src/index.js 和 wrangler.toml 两个文件。

第三步:注册 Cloudflare,上传代码

Cloudflare 是代码真正跑的地方。

  1. 打开浏览器,去 dash.cloudflare.com,点注册,填邮箱,设密码,点创建账号;再去收件箱点验证链接——点了才算真的激活——然后用它登录。
  2. 名下要是有多个账号,先认准自己的个人账号再往下走。
  3. 左边找到 Workers & Pages,点 Create,选 Worker,名字填 bark-relay-worker,点 Deploy。名字就填这个,别自己另起一个,下一篇就是按这个名字找它的。
  4. 点 Edit code,把编辑器里原来的代码全删掉,把开头抄好的代码一粘贴进去,点 Deploy。
  5. 部署完回到 Worker 的页面,看 Workers.dev 这一栏:如果显示的是你的 Worker 地址,说明路由已经开了;如果打开地址显示的是“There is nothing here yet”,就去把这个脚本的 workers.dev 路由单独打开。
  6. 在浏览器里打开你的 Worker 地址。

到这一步,你应该看到:页面上显示“bark-relay-worker is running”,下面还有往 /push 发 JSON 的说明。看到这句话,说明 Worker 已经跑起来了。

第四步:填两个密钥

  1. 在 Worker 的页面点 Settings,回到最上面,找到“运行时变量和密钥”这一栏,点右上角添加变量。
  2. 加第一个变量:名字填 BARK_KEY,值填手机 Bark App 里的设备密钥。值旁边有个“密钥”的框,勾上,点添加变量并部署。只抄那 22 个字符,别把网址、斜杠、参数一起抄进来。
  3. 再点添加变量,加第二个变量:名字填 RELAY_SECRET,值填你自己想的一个共享口令,同样勾上“密钥”,点添加变量并部署。口令只用字母和数字,别带空格,现在就写在纸上。Cloudflare 里的密钥是加密的,以后看不到这个口令。忘了就只能在 Worker 上换一个新的,读不回来。
  4. 回到设置页,看“运行时变量和密钥”这一栏。

到这一步,你应该看到:BARK_KEY 和 RELAY_SECRET 两行,值都显示已加密,看不到明文。

第五步:跑一条命令测试

先打开终端:Mac 上打开聚焦搜索输入 Terminal,Windows 上打开开始菜单输入 Terminal。我就是在终端里敲的下面这条命令,地址和口令换成你自己的。照抄是跑不通的——先把这两处换掉,再按回车:

curl -X POST https://你的Worker地址/push 
  -H "Content-Type: application/json" 
  -H "X-Relay-Secret: 你的共享口令" 
  -d '{"title": "Test", "body": "Bark relay test", "group": "test"}'

到这一步,你应该看到:命令返回 {"delivered":true},同时手机收到一条标题是“Test”的推送。两个少一个,都算还没通,别往下走。

测试没通过,按这个顺序查,别猜:

  • 命令返回 401。 命令里的 RELAY_SECRET 跟 Worker 上的不一样。看纸上的口令,重新敲一遍,再跑一次。纸丢了,就去 Worker 上把密钥换成一个新口令,重新部署,再用新的跑。
  • 命令返回 400,设备找不到。 BARK_KEY 不是裸密钥。回去第四步,检查是不是只抄了 22 个字符,网址、斜杠、参数有没有一起抄进来。
  • 命令报密钥缺失,或者根本没有 delivered 这一项。 回去第四步,检查两个密钥是不是都填了、都显示已加密。没存进去的密钥用不了。
  • 命令返回 delivered:true,手机却没收到推送。 回去第一步,检查设置、通知、Bark 里是不是开了通知。推送送到了、通知关了,屏幕上永远看不到。
  • 电脑说找不到 curl 这条命令。 Mac 和新版 Windows 本来就有 curl。要是说找不到,说明你不在终端里,或者敲进了搜索框。按上面说的重新打开 Terminal,再把命令粘贴进去。

成功标准,一共四条,对不齐就别往下走

四条全对,我才往下走。有一条不对,我就回那一步,不猜。

  1. Worker 地址打开,显示“bark-relay-worker is running”;
  2. 设置页显示 BARK_KEY 和 RELAY_SECRET,都是已加密;
  3. 测试命令返回 delivered:true;
  4. 手机收到了标题是“Test”的推送。

四条全对,任何知道 Worker 地址和共享口令的脚本,都能往你自己的 iPhone 上推消息,不用把设备密钥交出去。

附录:我实测踩过的坑

  1. workers.dev 路由不会自动开:用接口或脚本上传时,脚本在、地址也在,打开却是“There is nothing here yet”。得把这个脚本的 workers.dev 路由单独打开,地址才真正通。
  2. BARK_KEY 必须是裸密钥:要是照示例地址连 /title/body?group=... 的尾巴一起抄,Bark 会回 400,设备找不到。示例地址里的路径和参数不是密钥的一部分——删掉,只留密钥本身。
  3. 兼容日期只是命令行部署才用:第三步的浏览器部署不问日期,别在编辑器里找日期字段。代码二里的日期是我上传那天的日期。以后要是从命令行部署,留着也行,或者改成你上传当天的 UTC 日期——千万别写未来日期,否则上传会被直接拒掉。

这个系列里还有:我为什么用 Bark 来做提醒——我为什么用 Bark。卫星过顶提醒,亲手做:先从 30 度起——用这个转发器做卫星过顶提醒,先从 30 度起。第 4 篇:卫星录音——录音文件夹里文件一完就推送。第 5 篇:中继守听——静噪打开才推送。第 6 篇:波段打开——WSJT-X 解出你要的台才推送。第 7 篇:录音状态——录音完成、磁盘不足、预计文件没出现都推送。

代码一和代码二在上面英文部分的 Code 1、Code 2 里,直接从那里复制,不用再抄一遍。密钥绝不要写进代码里,密钥只在 Cloudflare 后台填。兼容日期按命令行部署那节讲的办,浏览器部署不问日期。