Skip to content

Commit 2115a81

Browse files
committed
refactor: move the api to its own subdomain
The Worker answered under the site's path, which meant the API depended on a zone route and shared a hostname with the static site. It now serves /v1/* on api.sudonotes.com — a Workers custom domain, so the hostname brings its own DNS instead of needing a route added beside it. Nothing is released yet, so the old /api/v1/* paths are dropped outright rather than kept alongside.
1 parent 9908eb8 commit 2115a81

7 files changed

Lines changed: 17 additions & 18 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ in the archive is needed.
121121
| --- | --- |
122122
| `core/` | `sudonotes-core` — the note format: frontmatter, filenames, links, paste splitting. Pure Rust, no filesystem, and it compiles to WebAssembly so a browser build can share it. |
123123
| `app/` | The desktop app. React front end in `src/`, Tauri and the filesystem in `src-tauri/`. |
124-
| `worker/` | The Cloudflare Worker behind `sudonotes.com/api/*`, which holds the AI provider key. |
124+
| `worker/` | The Cloudflare Worker behind `api.sudonotes.com`, which holds the AI provider key. |
125125
| `site/` | sudonotes.com — Astro, static. |
126126

127127
The split between `core/` and `app/src-tauri/` is deliberate and worth

app/src-tauri/src/ai.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const SETTINGS_FILE: &str = "settings.json";
1818
/// The device id, which is not a secret and is not vault-specific.
1919
const DEVICE_FILE: &str = "device.txt";
2020
const DEFAULT_ANALYZER_MODEL: &str = "deepseek-chat";
21-
const API_BASE: &str = "https://sudonotes.com/api/v1";
21+
const API_BASE: &str = "https://api.sudonotes.com/v1";
2222

2323
const TAG_VOCABULARY: &[&str] = &[
2424
"feedback",

site/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ Static output, so anything that serves files works. On Cloudflare the site and
5555
the API share the `sudonotes.com` origin:
5656

5757
- the site is served from `dist/` (Workers Static Assets or Pages),
58-
- `sudonotes.com/api/*` routes to the Worker in `../worker`.
58+
- The API is a separate Worker on `api.sudonotes.com`; this build does not route it.
5959

6060
The two deploy independently. Nothing on this site calls the API — it is all
6161
static — so the order does not matter.

worker/README.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# sudonotes API
22

3-
The Cloudflare Worker behind `https://sudonotes.com/api/v1/*`. It holds the
3+
The Cloudflare Worker behind `https://api.sudonotes.com/v1/*`. It holds the
44
provider key so the apps never do, and it is the only thing standing between a
55
free feature and an unbounded bill.
66

@@ -71,9 +71,8 @@ rotating it invalidates every issued device id. Then:
7171
npx wrangler deploy
7272
```
7373

74-
Add the route `sudonotes.com/api/*` to the Worker in the Cloudflare dashboard,
75-
and create a Turnstile widget for `app.sudonotes.com` whose secret is the one
76-
above.
74+
Point `api.sudonotes.com` at the Worker as a custom domain, and create a
75+
Turnstile widget for `app.sudonotes.com` whose secret is the one above.
7776

7877
## Locally
7978

worker/src/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -274,13 +274,13 @@ export default {
274274
}
275275

276276
try {
277-
if (url.pathname === "/api/v1/chat/completions" && request.method === "POST") {
277+
if (url.pathname === "/v1/chat/completions" && request.method === "POST") {
278278
return await handleChat(request, env, ctx);
279279
}
280-
if (url.pathname === "/api/v1/models" && request.method === "GET") {
280+
if (url.pathname === "/v1/models" && request.method === "GET") {
281281
return await handleModels(request, env);
282282
}
283-
if (url.pathname === "/api/v1/device" && request.method === "POST") {
283+
if (url.pathname === "/v1/device" && request.method === "POST") {
284284
const headers = corsHeaders(origin, env);
285285
const ip = request.headers.get("CF-Connecting-IP");
286286
if (ip) {

worker/test/api.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { mintDevice, readDevice } from "../src/auth";
55
import worker from "../src/index";
66

77
const APP = "https://app.sudonotes.com";
8-
const API = "https://sudonotes.com/api/v1/chat/completions";
8+
const API = "https://api.sudonotes.com/v1/chat/completions";
99

1010
/** What the stubbed network should do for this test. */
1111
let upstream: () => Response;
@@ -108,12 +108,12 @@ describe("routing and CORS", () => {
108108
});
109109

110110
it("404s an unknown path", async () => {
111-
const response = await call({ method: "GET", body: undefined }, "https://sudonotes.com/api/v1/nope");
111+
const response = await call({ method: "GET", body: undefined }, "https://api.sudonotes.com/v1/nope");
112112
expect(response.status).toBe(404);
113113
});
114114

115115
it("serves the model catalog verbatim for the browser", async () => {
116-
const response = await call({ method: "GET", body: undefined, headers: { Origin: APP } }, "https://sudonotes.com/api/v1/models");
116+
const response = await call({ method: "GET", body: undefined, headers: { Origin: APP } }, "https://api.sudonotes.com/v1/models");
117117
expect(response.status).toBe(200);
118118
expect(response.headers.get("Access-Control-Allow-Origin")).toBe(APP);
119119
// The desktop client parses models.dev's own shape, so it must survive intact.

worker/wrangler.jsonc

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
"main": "src/index.ts",
55
"compatibility_date": "2026-08-01",
66
"compatibility_flags": ["nodejs_compat"],
7-
// The desktop app calls https://sudonotes.com/api/v1 (ai.rs API_BASE), so the
8-
// zone must route /api/* here rather than to the static site Worker. That
9-
// route is added out of band: the deploy token has no Workers Routes
10-
// permission on the zone, and declaring it here disables workers.dev as a
11-
// side effect, which would take the API down when the route then fails.
7+
// The app calls https://api.sudonotes.com/v1 (ai.rs API_BASE). That hostname
8+
// is attached to this Worker as a custom domain, which is configured out of
9+
// band rather than here: declaring routes in this file turns workers.dev off
10+
// as a side effect, and losing that fallback while a route is misconfigured
11+
// leaves the API with no reachable hostname at all.
1212
"workers_dev": true,
1313
"observability": {
1414
"enabled": true,

0 commit comments

Comments
 (0)