A small Cloudflare Worker that wraps custom URL schemes (obsidian://, x-apple-reminderkit://, calshow:, things://, shortcuts://, etc.) in plain https:// links so Telegram and other chat apps recognize them as tappable. Part of the Synodic Patchbay family; it lives at a short go. host (e.g. go.synodic.co) — tap a link on the go, the app opens.
When an agent (or a script, or a human) wants to send a tappable link to a note in your Obsidian vault, a reminder, a calendar date, or anything else that lives behind a custom scheme, it cannot send obsidian://open?vault=... directly — most chat apps do not render custom schemes as links. Instead it sends https://your-domain.example/obs/<vault>/<path>. Tapping the link in the chat app opens it in the browser, which serves a tiny page that immediately redirects to the real native-scheme URI through meta refresh and a JS fallback. The app opens on your phone.
This is part of the Patchbay family of small tools that connect a phone chat app to a host that runs agents and apps.
| Route | Redirects to |
|---|---|
/obs/<vault>/<path> |
obsidian://open?vault=<vault>&file=<path> |
/remind/<title> |
x-apple-reminderkit://REMCDReminder/<title> |
/cal/<yyyy-mm-dd> |
calshow:<epoch> (Calendar.app) |
/cal/<yyyy-mm-dd>/<hh:mm> |
calshow:<epoch> at a specific time |
/raw/<base64url> |
Any native custom scheme (base64url-encoded full URI). Browser-privileged schemes (javascript:, data:, http(s):, …) are refused. |
/key/<uuid> |
Token-secured paste form (KV-backed, optional) |
/ |
Usage page (renders the deployed hostname automatically) |
So callers rarely need /raw, common apps get named routes. Content routes take a single free-text value (URI-encoded automatically); launcher routes just open the app.
/things/<title> things:///add?title=<title>
/todoist/<content> todoist://addtask?content=<content>
/fantastical/<sentence> x-fantastical3://parse?sentence=<sentence>
/shortcuts/<name> shortcuts://run-shortcut?name=<name>
/bear/<title> /drafts/<text> /ulysses/<text> /omnifocus/<name> /due/<title>
/twitter/<handle> /instagram/<username> /telegram/<username> /whatsapp/<phone>
/googlemaps/<query> /waze/<address> /zoom/<meeting-id>
/music /podcasts /overcast /soundcloud /slack /discord /reddit /linkedin (launchers)
The full, live list renders on the / page of a deployment (single source of truth is APP_ROUTES in src/worker.js — adding an app is one entry). For any scheme not listed, use /raw/<base64url>.
Runs as a Cloudflare Pages project (direct upload, advanced mode — the build step just copies src/worker.js to dist/_worker.js). You need a Cloudflare account and the Wrangler CLI.
npm install
cp wrangler.example.toml wrangler.toml # edit project name to taste
npm run deploy # build + wrangler pages deploy distnpm run deploy runs wrangler pages deploy dist --project-name <name> --branch main. The first deploy gives you https://<name>.pages.dev.
To put it on a short custom domain (recommended — nicer in chats), attach it to the Pages project and point DNS at <name>.pages.dev (proxied):
# attach custom domain to the project
curl -H "Authorization: Bearer $CF_TOKEN" -X POST \
"https://api.cloudflare.com/client/v4/accounts/$CF_ACCT/pages/projects/<name>/domains" \
-d '{"name":"go.example.com"}'
# then create a proxied CNAME go -> <name>.pages.dev in that zoneSet CLOUDFLARE_API_TOKEN (a token with Cloudflare Pages: Edit, plus Workers Routes / DNS: Edit on the zone if you attach a custom domain) and CLOUDFLARE_ACCOUNT_ID — no interactive wrangler login needed:
CLOUDFLARE_API_TOKEN=... CLOUDFLARE_ACCOUNT_ID=... npm run deployThe /key routes move a secret from a phone into a service without the worker ever seeing the plaintext. The requester holds a keypair; the secret is encrypted in the browser to the requester's public key; the worker only ever stores ciphertext.
POST /key/register {label, publicKey, webhook?} -> {uuid, secret, url}
GET /key/<uuid> -> browser-encrypting form
POST /key/<uuid> {envelope} -> stores ciphertext, fires webhook
GET /key/<uuid>/result -> one-shot ciphertext retrieval
Flow: a requester generates a keypair and POSTs its public key to /key/register; it sends the returned https://<host>/key/<uuid> link; the user taps, pastes, and the page encrypts (AES-GCM wrapped with the requester's RSA-OAEP key) and submits the envelope; the worker stores ciphertext and either fires the signed webhook or holds it for GET /key/<uuid>/result (one-shot). The requester decrypts with its private key, which never leaves its machine. The public key is validated at registration and required — there is no plaintext path.
A ready-to-use client is in clients/patchbay_key.py (a self-contained uv script):
uv run clients/patchbay_key.py register my-service # prints the tappable link
uv run clients/patchbay_key.py fetch my-service # decrypts into `pass`To enable the routes, bind a KV namespace:
npx wrangler kv namespace create VAULTPaste the returned id into the [[kv_namespaces]] block in your wrangler.toml. If the binding is not present, /key/* returns 400 — every other route still works.
Once it is deployed, any code that wants to send a tappable native-scheme link from a chat message can construct one of the wrapped URLs by hand. The Worker has no auth, no logging, and no state outside the optional KV.
For Obsidian:
https://your-domain.example/obs/MyVault/notes/today.md
For any other scheme, base64url-encode the full URI:
https://your-domain.example/raw/dGhpbmdzOi8vLw # → things:///
Once deployed, export the URL prefix as PATCHBAY_URL_WRAPPER on the host so agents pick it up:
export PATCHBAY_URL_WRAPPER="https://go.example.com"Agent code that builds links reads this prefix and emits ${PATCHBAY_URL_WRAPPER}/obs/<vault>/<path> instead of raw obsidian:// URIs.
MIT.