Skip to main content

max / makenotwork

Publish developer feeds doc and wire Swagger UI at /api/docs Reconciles mbp commit c605752 onto current main. Adds a developer/feeds.md guide, edits api-overview.md + oauth.md, and serves interactive API docs at /api/docs via utoipa-swagger-ui (the OpenAPI spec already generated by openapi.rs). Compiles against the tree's utoipa 5.5; lock adds utoipa-swagger-ui 9.0.2 + its deps only (no existing pins moved). Site-docs marker CI guard green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-09 01:27 UTC
Commit: 6e247eb84ad00c86c3f1836349a618312a869433
Parent: e837ed7
7 files changed, +214 insertions, -2 deletions
@@ -4402,6 +4402,7 @@ dependencies = [
4402 4402 "urlencoding",
4403 4403 "utoipa",
4404 4404 "utoipa-axum",
4405 + "utoipa-swagger-ui",
4405 4406 "uuid",
4406 4407 "webauthn-authenticator-rs",
4407 4408 "webauthn-rs",
@@ -6069,6 +6070,41 @@ dependencies = [
6069 6070 ]
6070 6071
6071 6072 [[package]]
6073 + name = "rust-embed"
6074 + version = "8.12.0"
6075 + source = "registry+https://github.com/rust-lang/crates.io-index"
6076 + checksum = "e9e7760e252aaba7b09f4be00e36476cf585bdb68a53552ac954cdf504ab4bc9"
6077 + dependencies = [
6078 + "rust-embed-impl",
6079 + "rust-embed-utils",
6080 + "walkdir",
6081 + ]
6082 +
6083 + [[package]]
6084 + name = "rust-embed-impl"
6085 + version = "8.12.0"
6086 + source = "registry+https://github.com/rust-lang/crates.io-index"
6087 + checksum = "3bcfc4d6f53af43755f7a723e4b6b8794fcce052a178dd8c6c1dadc5f5343097"
6088 + dependencies = [
6089 + "mime_guess",
6090 + "proc-macro2",
6091 + "quote",
6092 + "rust-embed-utils",
6093 + "syn 2.0.118",
6094 + "walkdir",
6095 + ]
6096 +
6097 + [[package]]
6098 + name = "rust-embed-utils"
6099 + version = "8.12.0"
6100 + source = "registry+https://github.com/rust-lang/crates.io-index"
6101 + checksum = "42ffa149f6aa81b58a5b3011d01a857c4ed12c7a732d2c51947a4c7c692185f0"
6102 + dependencies = [
6103 + "sha2 0.11.0",
6104 + "walkdir",
6105 + ]
6106 +
6107 + [[package]]
6072 6108 name = "rustc-hash"
6073 6109 version = "1.1.0"
6074 6110 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -7949,6 +7985,24 @@ dependencies = [
7949 7985 ]
7950 7986
7951 7987 [[package]]
7988 + name = "utoipa-swagger-ui"
7989 + version = "9.0.2"
7990 + source = "registry+https://github.com/rust-lang/crates.io-index"
7991 + checksum = "d047458f1b5b65237c2f6dc6db136945667f40a7668627b3490b9513a3d43a55"
7992 + dependencies = [
7993 + "axum",
7994 + "base64 0.22.1",
7995 + "mime_guess",
7996 + "regex",
7997 + "rust-embed",
7998 + "serde",
7999 + "serde_json",
8000 + "url",
8001 + "utoipa",
8002 + "zip 3.0.0",
8003 + ]
8004 +
8005 + [[package]]
7952 8006 name = "uuid"
7953 8007 version = "1.23.4"
7954 8008 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -9232,6 +9286,20 @@ dependencies = [
9232 9286
9233 9287 [[package]]
9234 9288 name = "zip"
9289 + version = "3.0.0"
9290 + source = "registry+https://github.com/rust-lang/crates.io-index"
9291 + checksum = "12598812502ed0105f607f941c386f43d441e00148fce9dec3ca5ffb0bde9308"
9292 + dependencies = [
9293 + "arbitrary",
9294 + "crc32fast",
9295 + "flate2",
9296 + "indexmap",
9297 + "memchr",
9298 + "zopfli",
9299 + ]
9300 +
9301 + [[package]]
9302 + name = "zip"
9235 9303 version = "8.6.0"
9236 9304 source = "registry+https://github.com/rust-lang/crates.io-index"
9237 9305 checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b"
@@ -21,6 +21,7 @@ axum-extra = { version = "0.10.3", features = ["cookie", "form", "typed-header"]
21 21 # OpenAPI spec generation
22 22 utoipa = { version = "5", features = ["axum_extras", "chrono", "uuid"] }
23 23 utoipa-axum = "0.2"
24 + utoipa-swagger-ui = { version = "9", features = ["axum"] }
24 25 serde = { version = "1.0.228", features = ["derive"] }
25 26 serde_json = "1.0.149"
26 27 toml = "0.8"
@@ -1,6 +1,6 @@
1 1 # Developer API Overview
2 2
3 - Makenot.work provides four APIs for building integrations, desktop applications, and developer tools: session-based (web dashboard), SyncKit JWT (cloud sync and OTA), OAuth2 PKCE (third-party apps), and a public License Key API. This page covers authentication methods, error handling, and rate limits shared across all endpoints.
3 + Makenot.work provides four APIs for building integrations, desktop applications, and developer tools: session-based (web dashboard), SyncKit JWT (cloud sync and OTA), OAuth2 PKCE (third-party apps), and a public License Key API. Public [RSS feeds](./feeds.md) are documented separately. This page covers authentication methods, error handling, and rate limits shared across all endpoints.
4 4
5 5 ## Authentication Methods
6 6
@@ -20,6 +20,7 @@ Public endpoints that require no auth:
20 20 - [License Key API](./license-keys.md): key validation, activation, deactivation
21 21 - [OTA update check](./ota.md): Tauri-compatible update endpoint
22 22 - [OAuth authorize](./oauth.md): authorization page and code exchange
23 + - [Public Feeds](./feeds.md): RSS feeds for creators, projects, and blog posts
23 24
24 25 ## Error Format
25 26
@@ -65,10 +66,25 @@ All rate limits are per IP. Check response headers (`X-RateLimit-Limit`, `X-Rate
65 66
66 67 Exceeding a limit returns HTTP 429. Implement exponential backoff in your client.
67 68
69 + ## OpenAPI Spec
70 +
71 + A machine-readable OpenAPI 3 spec is served at:
72 +
73 + ```
74 + GET /api/openapi.json
75 + ```
76 +
77 + An interactive Swagger UI viewer for the same spec is available at [`/api/docs`](/api/docs) — send test requests, browse schemas, and copy example payloads without leaving the browser.
78 +
79 + The spec covers the SyncKit and License Key endpoints (the surfaces with frozen request/response shapes). Point any OpenAPI-aware client generator, Postman-style tool, or IDE plugin at the JSON URL. The spec's `version` matches the server version, so a hard-refresh after a deploy picks up any schema changes.
80 +
81 + OAuth, OTA, and RSS feeds are documented on their own pages rather than in the spec — OAuth is fully described by [discovery metadata](./oauth.md#discovery-metadata), OTA follows Tauri's protocol, and RSS is an XML surface.
82 +
68 83 ## API Reference
69 84
70 85 - [SyncKit Cloud Sync](./synckit.md): push/pull encrypted data, device management, blob storage
71 86 - [OTA Updates](./ota.md): app auto-update server (Tauri-compatible protocol)
72 87 - [OAuth2 PKCE](./oauth.md): "Log in with Makenot.work"
73 88 - [License Key API](./license-keys.md): validate, activate, and deactivate license keys
89 + - [Public Feeds](./feeds.md): RSS feeds for creators, projects, blogs, and the changelog
74 90 - [Rustdoc API Reference](/rustdoc/synckit_client/): SyncKit client SDK documentation
@@ -0,0 +1,113 @@
1 + # Public Feeds API
2 +
3 + Makenot.work publishes creator and project content as RSS 2.0 feeds. The feeds are unauthenticated, uncapped by application, and stable — aggregators can rely on them without registering an app or requesting a key.
4 +
5 + The fan-facing walkthrough lives at [RSS Feeds](../guide/rss.md). This page is for aggregators, feed readers, and anyone building on top of MNW content.
6 +
7 + ## Open by Policy
8 +
9 + These feeds are a permanent, first-class integration surface. Any aggregator — including ones that compete with Balanced Breakfast (our own aggregator) — can consume them on equal footing. There is no privileged internal path; BB reads the same URLs as everyone else.
10 +
11 + Concretely, this means:
12 +
13 + - The URL shapes below will not be broken. Additions are fine; removals require a migration path and an entry in the [changelog feed](https://makenot.work/changelog/feed.xml).
14 + - No API key, contract, or business relationship is required to poll them.
15 + - We will not rate-limit a well-behaved aggregator more aggressively than a browser polling the same URL.
16 +
17 + ## Endpoints
18 +
19 + All responses are `application/rss+xml; charset=utf-8`, RSS 2.0.
20 +
21 + | Endpoint | Contents |
22 + |----------|----------|
23 + | `GET /u/{username}/rss` | A creator's public items across all projects |
24 + | `GET /p/{slug}/rss` | A project's public items |
25 + | `GET /p/{slug}/blog/feed.xml` | A project's published blog posts |
26 + | `GET /changelog/feed.xml` | Platform changelog (canonical alias for the `changelog` project blog) |
27 + | `GET /feed/{user_id}?sig={hmac}&v={version}` | Personal feed of items from followed creators and projects |
28 +
29 + Sandbox accounts are excluded from all feeds; requests against a sandbox username or slug return `404`.
30 +
31 + ### Item Feeds (`/u/*/rss`, `/p/*/rss`)
32 +
33 + Each `<item>` element carries:
34 +
35 + | Field | Source |
36 + |-------|--------|
37 + | `<title>` | Item title |
38 + | `<link>` | `{host}/i/{item_id}` — the canonical item page |
39 + | `<description>` | Item description (empty string if unset) |
40 + | `<pubDate>` | Item `created_at` |
41 + | `<guid>` | Item ID (UUID). Stable for the life of the item. |
42 +
43 + The GUID is the item's UUID, not its URL, so aggregators can detect a renamed or moved item as the same entry.
44 +
45 + ### Blog Feeds (`/p/*/blog/feed.xml`, `/changelog/feed.xml`)
46 +
47 + Blog `<item>` elements carry:
48 +
49 + | Field | Source |
50 + |-------|--------|
51 + | `<title>` | Post title |
52 + | `<link>` | `{host}/p/{project_slug}/blog/{post_slug}` (or `{host}/changelog/{post_slug}` for the changelog alias) |
53 + | `<description>` | First 300 characters of the post body (Markdown, not rendered) |
54 + | `<pubDate>` | `published_at`, falling back to `created_at` if unset |
55 + | `<guid>` | Post ID (UUID) |
56 +
57 + Only published posts appear. Drafts and scheduled-future posts are omitted.
58 +
59 + ### Personal Feed (`/feed/{user_id}`)
60 +
61 + The personal feed aggregates new items from creators and projects the user follows. Unlike the other endpoints, it is per-user and private, so it is authenticated by an HMAC signature in the query string — no cookie or bearer token, which lets any RSS reader fetch it.
62 +
63 + The signed URL is minted for the user in the dashboard. Users can rotate it (the URL carries a version `v`; when the user regenerates, the old version's signatures are rejected). Aggregators should treat a `403` on this endpoint as a signal that the user needs to paste in a fresh URL.
64 +
65 + ## URL Stability
66 +
67 + The paths above are load-bearing. In particular:
68 +
69 + - `/u/{username}/rss` and `/p/{slug}/rss` will remain valid for as long as the username or slug exists.
70 + - If a creator renames their account or a project changes slug, the old feed URL stops working. There is no automatic redirect. GUIDs remain stable across renames, so an aggregator that already ingested items will not lose de-duplication.
71 + - `/changelog/feed.xml` is a canonical alias; it will follow the changelog wherever it moves.
72 +
73 + New endpoints may be added under `/feed/`, `/u/`, or `/p/`. Existing ones will not be removed without a deprecation window announced on the changelog feed.
74 +
75 + ## Discovery
76 +
77 + There is no JSON directory endpoint yet, but three existing mechanisms cover the common cases:
78 +
79 + - **Sitemap.** `GET /sitemap.xml` enumerates every active public creator (`/u/{username}`) and every public item (`/i/{item_id}`). An aggregator can walk the sitemap and construct `/u/{username}/rss` from each creator URL. The sitemap is cached and rate-friendly to crawl.
80 + - **Autodiscovery `<link>` tags.** Three page types advertise their feed via `<link rel="alternate" type="application/rss+xml">` in the HTML `<head>`:
81 + - `/u/{username}` — the creator's item feed
82 + - `/p/{slug}` — the project's item feed
83 + - `/p/{slug}/blog` — the project's blog feed
84 + Standard feed-reader auto-detection picks these up.
85 + - **Direct construction.** If you have a username or project slug already, build the URL from the endpoint table above.
86 +
87 + A JSON discovery endpoint (creators and projects opting into aggregation, with metadata) is a planned addition; it will land here when it ships.
88 +
89 + ## Polling
90 +
91 + Feeds are cheap to generate but not free. Recommended cadence:
92 +
93 + - **Item and blog feeds**: poll no more than every 15 minutes per URL.
94 + - **Changelog feed**: hourly is plenty.
95 + - **Personal feed**: poll on the same cadence the user's reader would — typically 15–60 minutes.
96 +
97 + There is no per-endpoint rate limit distinct from the platform baseline described in [API Overview](./api-overview.md). Feeds share the general read-path budget. If you are polling thousands of URLs, stagger requests rather than firing them in a burst.
98 +
99 + Conditional requests (`If-Modified-Since`, `ETag`) are not currently honored — every request re-renders the feed. Assume you will download the full body each poll and de-duplicate on GUID. This may change; watch the changelog feed.
100 +
101 + ## Content and Licensing
102 +
103 + The feeds carry titles, descriptions, publication dates, and canonical links back to makenot.work. They do not carry the underlying media (audio, video, downloads, or paid content), which remains gated behind the item page.
104 +
105 + Redistribution of feed metadata (titles, descriptions, links) for the purpose of aggregation, indexing, or notification is expressly permitted. Attribution back to the creator's makenot.work page via the `<link>` element is required and is included in every entry by default — do not strip it.
106 +
107 + Republishing feed content as if it were your own, or scraping full media through the feed's link elements, is not permitted and is not what these feeds are for. When in doubt, link back.
108 +
109 + ## See Also
110 +
111 + - [RSS Feeds (fan guide)](../guide/rss.md): subscribing in a reader, podcast setup
112 + - [API Overview](./api-overview.md): shared error format and rate-limit policy
113 + - [Changelog](https://makenot.work/changelog): platform changes, including feed additions and deprecations
@@ -2,6 +2,16 @@
2 2
3 3 Makenot.work supports OAuth2 Authorization Code with PKCE for "Log in with Makenot.work" flows. This lets third-party applications authenticate users without handling their passwords directly.
4 4
5 + ## Discovery Metadata
6 +
7 + Endpoints, supported scopes, and grant types are published at the standard RFC 8414 discovery URL:
8 +
9 + ```
10 + GET /.well-known/oauth-authorization-server
11 + ```
12 +
13 + Most OAuth libraries auto-configure from this URL — pass the base host and they will fetch the metadata. Manual configuration values are documented in the sections below.
14 +
5 15 ## Overview
6 16
7 17 1. Your app generates a PKCE code verifier and challenge
@@ -213,6 +213,10 @@ pub fn build_app(
213 213 .merge(git_routes())
214 214 .merge(routes::embed::embed_routes())
215 215 .route("/api/openapi.json", axum::routing::get(openapi::openapi_json))
216 + .merge(
217 + utoipa_swagger_ui::SwaggerUi::new("/api/docs")
218 + .url("/api/openapi.json", <openapi::ApiDoc as utoipa::OpenApi>::openapi()),
219 + )
216 220 .nest_service(
217 221 "/static",
218 222 tower::ServiceBuilder::new()
@@ -119,7 +119,7 @@ use utoipa::OpenApi;
119 119 ("bearer" = []),
120 120 ),
121 121 )]
122 - struct ApiDoc;
122 + pub struct ApiDoc;
123 123
124 124 /// Serve the OpenAPI spec as JSON.
125 125 pub async fn openapi_json() -> impl IntoResponse {