{"result":{"depiction_url":null,"edges":{"observes":{"objects":[{"created":"2026-09-29T13:07:37Z","object_id":{"id":1466,"is_a":["text","documentation","reference","notification"],"name":"doc_notification_admin_menu","title":"observe_admin_menu\/3","uri":"https:\/\/zotonic.com\/id\/1466"},"seq":1},{"created":"2026-09-29T13:07:37Z","object_id":{"id":1468,"is_a":["text","documentation","reference","notification"],"name":"doc_notification_acl_is_allowed","title":"observe_acl_is_allowed\/2","uri":"https:\/\/zotonic.com\/id\/1468"},"seq":2},{"created":"2026-09-29T13:07:37Z","object_id":{"id":1981,"is_a":["text","documentation","reference","notification"],"name":"doc_notification_rsc_update_done","title":"observe_rsc_update_done\/2","uri":"https:\/\/zotonic.com\/id\/1981"},"seq":3},{"created":"2026-09-29T13:07:37Z","object_id":{"id":2126,"is_a":["text","documentation","reference","notification"],"name":"doc_notification_edge_delete","title":"observe_edge_delete\/2","uri":"https:\/\/zotonic.com\/id\/2126"},"seq":4},{"created":"2026-09-29T13:07:37Z","object_id":{"id":2137,"is_a":["text","documentation","reference","notification"],"name":"doc_notification_edge_update","title":"observe_edge_update\/2","uri":"https:\/\/zotonic.com\/id\/2137"},"seq":5},{"created":"2026-09-29T13:07:37Z","object_id":{"id":2168,"is_a":["text","documentation","reference","notification"],"name":"doc_notification_edge_insert","title":"observe_edge_insert\/2","uri":"https:\/\/zotonic.com\/id\/2168"},"seq":6},{"created":"2026-09-29T13:07:37Z","object_id":{"id":2286,"is_a":["text","documentation","reference","notification"],"name":"doc_notification_resource_headers","title":"observe_resource_headers\/3","uri":"https:\/\/zotonic.com\/id\/2286"},"seq":7},{"created":"2026-09-29T13:07:37Z","object_id":{"id":2293,"is_a":["text","documentation","reference","notification"],"name":"doc_notification_rsc_import_fetch","title":"observe_rsc_import_fetch\/2","uri":"https:\/\/zotonic.com\/id\/2293"},"seq":8},{"created":"2026-09-29T13:07:37Z","object_id":{"id":2699,"is_a":["text","documentation","reference","notification"],"name":"doc_notification_rsc_import_fetch_result","title":"observe_rsc_import_fetch_result\/3","uri":"https:\/\/zotonic.com\/id\/2699"},"seq":9},{"created":"2026-09-29T13:07:37Z","object_id":{"id":2700,"is_a":["text","documentation","reference","notification"],"name":"doc_notification_rsc_import_done","title":"observe_rsc_import_done\/2","uri":"https:\/\/zotonic.com\/id\/2700"},"seq":10},{"created":"2026-09-29T13:07:37Z","object_id":{"id":2701,"is_a":["text","documentation","reference","notification"],"name":"doc_notification_rsc_export_done","title":"observe_rsc_export_done\/3","uri":"https:\/\/zotonic.com\/id\/2701"},"seq":11}],"predicate":{"id":2550,"is_a":["meta","predicate"],"name":"observes","title":{"_type":"trans","tr":{"en":"Observes"}},"uri":"https:\/\/zotonic.com\/id\/observes"}},"subject":{"objects":[{"created":"2026-09-29T13:07:37Z","object_id":{"id":2555,"is_a":["categorization","keyword","keyword_information_type"],"name":"zotonic_topic_reference","title":"Reference","uri":"https:\/\/zotonic.com\/id\/2555"},"seq":1},{"created":"2026-09-29T13:07:37Z","object_id":{"id":2565,"is_a":["categorization","keyword","keyword_audience"],"name":"zotonic_topic_integrator","title":"Integrator","uri":"https:\/\/zotonic.com\/id\/2565"},"seq":2},{"created":"2026-09-29T13:07:37Z","object_id":{"id":2599,"is_a":["categorization","keyword","keyword_domain"],"name":"zotonic_topic_api_and_integration","title":"API and integration","uri":"https:\/\/zotonic.com\/id\/2599"},"seq":3},{"created":"2026-09-29T13:07:37Z","object_id":{"id":2601,"is_a":["categorization","keyword","keyword_domain"],"name":"zotonic_topic_export_and_syndication","title":"Export and syndication","uri":"https:\/\/zotonic.com\/id\/2601"},"seq":4},{"created":"2026-09-29T13:07:37Z","object_id":{"id":2635,"is_a":["categorization","keyword","keyword_architecture"],"name":"zotonic_topic_module","title":"Module","uri":"https:\/\/zotonic.com\/id\/2635"},"seq":5},{"created":"2026-09-29T13:07:37Z","object_id":{"id":2665,"is_a":["categorization","keyword","keyword_data_type"],"name":"zotonic_topic_structured_data","title":"Structured data","uri":"https:\/\/zotonic.com\/id\/2665"},"seq":6},{"created":"2026-09-29T13:07:37Z","object_id":{"id":2676,"is_a":["categorization","keyword","keyword_technology"],"name":"zotonic_topic_http","title":"HTTP","uri":"https:\/\/zotonic.com\/id\/2676"},"seq":7}],"predicate":{"id":308,"is_a":["meta","predicate"],"name":"subject","title":{"_type":"trans","tr":{"en":"Keyword"}},"uri":"http:\/\/purl.org\/dc\/elements\/1.1\/subject"}}},"id":2703,"is_a":["text","documentation","reference","module"],"links":[{"rel":"self","target":"https:\/\/zotonic.com\/.zotonic\/websub\/topic\/2703"},{"rel":"hub","target":"https:\/\/zotonic.com\/.zotonic\/websub"}],"medium":null,"medium_url":null,"name":"doc_module_mod_websub","page_url":{"en":"https:\/\/zotonic.com\/docs\/2703\/mod_websub","x-default":"https:\/\/zotonic.com\/docs\/2703\/mod_websub"},"preview_url":null,"resource":{"version":39,"pivot_location_lat":null,"title":"mod_websub","is_authoritative":true,"body":"<p>WebSub resource synchronization, following <a href=\"https:\/\/www.w3.org\/TR\/websub\/\">https:\/\/www.w3.org\/TR\/websub\/<\/a>.<\/p>\n<h2>Integration points<\/h2>\n<p>Enable <code>mod_websub<\/code> on the publisher and importing site. Anyone who can view an original resource can subscribe to it, including anonymous\nvisitors for public resources. Private subscriptions use explicit HTTP authorization\nand current resource access. The <code>use mod_websub<\/code> permission controls administration.<\/p>\n<ul><li><code>controller_websub<\/code> handles hub requests, callback verification, and deliveries.<\/li><li><code>controller_websub_topic<\/code> serves the complete, fixed JSON topic representation.<\/li><li>The admin Content menu links to a subscription overview, filtered by direction\nand local resource ID. It requires <code>use mod_websub<\/code>; edit pages show the\nactive incoming subscriber count and a filtered overview link.<\/li><li><code>m_websub<\/code> provides the ACL-checked status\/start\/stop API and delivery\/import queues.<\/li><li><code>z_websub_subscription<\/code> persists subscriber intent, leases, and renewal work.<\/li><li><code>z_websub_discovery<\/code> and <code>z_websub_http<\/code> handle discovery and outbound fetch policy.<\/li><\/ul>\n<p>The <code>resource_headers<\/code> and <code>rsc_export_done<\/code> observers advertise discovery links.\nFull resource exports also include <code>websub.hub<\/code> and <code>websub.topic<\/code> for clients\nsubscribing from exported JSON. This optional JSON extension is present only on\nauthoritative resources and agrees with the standard discovery links. The semantic\nresource identifier remains <code>uri<\/code>.\n<code>rsc_import_fetch_result<\/code> exposes subscription availability to the import dialog;\n<code>rsc_import_done<\/code> starts an explicitly requested subscription. <code>rsc_update_done<\/code>\nqueues publication. Second ticks consult the module server&#39;s dirty flag and next due\ntime; ten-minute ticks reconcile with the database. Enqueues signal after commit, and the\ndaily tick performs maintenance. The protocol adapters are documented separately\nfrom the resource importer: external peers need no Zotonic-specific WebSub fields.<\/p>\n<h2>Resource identity versus the WebSub topic<\/h2>\n<p>A resource&#39;s identity is its language-less <code>m_rsc:uri\/2<\/code>, including <code>\/id\/name<\/code>.\nImports store this in <code>source_uri<\/code> and the resource&#39;s <code>uri<\/code>. It is never replaced\nby a page, representation, or hub URL. Discovery advertises <code>rel=self<\/code> pointing\nto the fixed JSON <code>websub_topic<\/code> dispatch, and <code>rel=hub<\/code> to the local hub. The\nWebSub topic is stored separately in <code>topic_url<\/code>. This distinction lets <code>\/id<\/code>\nretain semantic-web content negotiation while a WebSub topic has one media type.<\/p>\n<h2>Flow between two Zotonic systems<\/h2>\n<ol><li>On importing a connected resource from A into B, B fetches A&#39;s <code>\/id<\/code> URI with\n<code>Accept: application\/json<\/code>. The JSON export retains A&#39;s semantic resource URI.\nHTTP Link headers (or the JSON links extension) advertise A&#39;s JSON topic and hub.<\/li><li>If the editor opts into automatic updates, B persists the source URI, local\nresource, editor, and desired subscription state. Recursive imports do not opt in.<\/li><li>B rediscovers the source and POSTs a standard URL-encoded subscription request\nto the advertised hub. <code>hub.topic<\/code> is the discovered self URL, not necessarily\n<code>\/id<\/code>. B supplies a unique callback, random secret, and requested lease.<\/li><li>A bounds and persists verification work and replies 202. Independently, A\nchecks the subscribing user&#39;s resource access and GETs B&#39;s callback with the\ntopic, mode, random challenge, and lease. B verifies pending intent and echoes\nthe challenge as plain text with nosniff. A records the verified subscription.<\/li><li>Changes on A queue the resource&#39;s newest version. A rechecks access and lease,\nthen POSTs the complete JSON topic representation, Link hub\/self headers, and\nan HMAC-SHA256 signature to B. Private topics also send full authorized JSON;\nthere is no notification-only or Zotonic-specific wire format.<\/li><li>B verifies the signature and callback, queues work, and quickly acknowledges.\nPublic imports consume the signed export; credentialed imports refetch A&#39;s\nsemantic URI using the editor&#39;s source credentials. Inside the import transaction\nB checks that the stored source, current resource URI, and payload URI agree,\nthen applies newer content with the editor&#39;s current permissions and saved options.<\/li><li>B renews before lease expiry, rediscovering the hub\/topic and rotating its\ncallback and secret. The old callback remains active until the new one verifies.\nStopping immediately disables imports and queues verified remote unsubscription.\nConfirmation also queues a catch-up fetch to recover missed updates.<\/li><\/ol>\n<h2>Collections and connected resources<\/h2>\n<p>Edge insertions, removals, and reordering advance the authoritative subject&#39;s\nversion and publish its updated export. Receivers synchronize edges using saved\nimport depth and filters, then fetch newly referenced placeholders after commit.\nExisting imported members are reused, not refreshed by the collection&#39;s delivery.<\/p>\n<p>The optional <code>is_subscribe_connections<\/code> import setting subscribes imported\nconnected resources across all predicates, up to the saved connection depth.\nIt defaults to false. Connected imports inherit the option with decremented depth,\nso later additions follow the same bound. Subscriptions remain independent:\nremoving a connection or stopping the parent does not unsubscribe them. See the\nmodule README for the complete collection lifetime and retry behavior.<\/p>\n<h2>Interoperation with other WebSub implementations<\/h2>\n<p>No peer-brand detection or private protocol extension is required. Any subscriber\ncan discover the fixed JSON topic from headers or HTML, subscribe using standard\nform fields, answer verification, and receive its full <code>application\/json<\/code> body.\nThe body&#39;s <code>result.uri<\/code> is the semantic identity; the delivery Link self names the\nWebSub topic. Private subscriptions require explicit HTTP authorization and current access at\nA&#39;s hub (cookies alone are insufficient); granting one authorizes full content delivery to the verified callback.<\/p>\n<p>A non-Zotonic hub can relay the same JSON topic to B without changes. A non-Zotonic\npublisher can be imported if it supplies the resource-export JSON understood by\nZotonic&#39;s resource importer. WebSub is media-type agnostic, but this application\nadapter imports resources, not arbitrary Atom\/RSS\/HTML documents. It does not\nclaim that those document formats can be imported as Zotonic resources.<\/p>\n<p>Standard subscription verification, signatures, leases, 307\/308 hub redirects,\nand unsubscription apply to all peers. A 202 alone never activates a subscription.\nDelivery retry exhaustion drops only that notification, preserving the lease for\nfuture updates. Semantic resource identity is independent of WebSub topic migration.<\/p>\n<h2>Security and operation<\/h2>\n<p>All WebSub requests use <code>z_fetch<\/code> with automatic redirects disabled. Destinations\nare checked for non-public addresses on each hop; private, loopback, link-local,\nand reserved networks are rejected. Development sites may use .test peers on\nloopback\/private networks with self-signed TLS. Other destinations keep verified\nTLS and public-address checks. HTTPS downgrades are refused. Cross-origin\nGET redirects permanently drop user credentials for that chain. Callback requests\nare anonymous. <code>z_fetch<\/code> integrates <code>mod_oauth2<\/code> consumer tokens using the original\nHTTPS URL, hostname, and subscribing user; the source&#39;s same-origin hub can use\nthat token as well. Token lookup and renewal remain owned by <code>mod_oauth2<\/code>.<\/p>\n<p>TODO: add validated-address pinning to <code>z_fetch<\/code>\/<code>z_url_fetch<\/code>, preserving the\noriginal Host header and TLS hostname. The current DNS preflight does not prevent\nDNS rebinding between validation and connection establishment.<\/p>\n<p>Hub admission deduplicates identical requests and limits each site to 120 new\nverification tasks per minute and 1000 queued tasks. Admission is serialized in the\ndatabase across nodes; overload returns 503 before accepting work. Token group restrictions are retained when recreating delivery ACL contexts. Admin start,\nstop, and status require edit permission. Callback tokens and secrets are not exposed\nby status. Imports serialize identity and permission checks with the update.<\/p>\n<p>The unique per-site sidejob processes renewal, delivery, and import queues. Schema\nversion 3 separates source identity from topic and adds durable admission accounting.\nSee README.md for deployment details and the regression suite.<\/p>","slug":"mod_websub","is_protected":false,"visible_for":0,"tz":"UTC","language":["en"],"doc_source_hash":"342b5b28959151c22b6ab3f0923196a7a91bac523593403d84b882e9cdf1a0ac","is_featured":false,"content_group_id":{"id":2551,"is_a":["meta","content_group"],"name":"content_group_imported_docs","title":"Imported documentation","uri":"https:\/\/zotonic.com\/id\/content_group_imported_docs"},"category_id":{"id":320,"is_a":["meta","category"],"name":"module","title":"Modules","uri":"https:\/\/test.zotonic.com\/id\/320"},"doc_source_path":"apps\/zotonic_mod_websub\/src\/mod_websub.erl","publication_start":"2026-09-29T13:07:37Z","github_url":"https:\/\/github.com\/zotonic\/zotonic\/blob\/master\/apps\/zotonic_mod_websub\/src\/mod_websub.erl","pivot_location_lng":null,"doc_source_kind":"module","name":"doc_module_mod_websub","is_unfindable":false,"is_published":true,"pivot_geocode":null,"created":"2026-09-29T13:07:37Z","uri":null,"doc_status":"current","is_dependent":false,"erlang_app":"zotonic_mod_websub","publication_end":"9999-06-01T00:00:00Z","modifier_id":{"id":1,"is_a":["person"],"name":"administrator","title":"Site Administrator","uri":"https:\/\/zotonic.com\/id\/1"},"privacy":0,"doc_source_commit":"85498256abd162b9bc082d43a90c995844fd4408\n","creator_id":{"id":1,"is_a":["person"],"name":"administrator","title":"Site Administrator","uri":"https:\/\/zotonic.com\/id\/1"},"modified":"2026-09-29T14:40:09Z","title_slug":"mod_websub"},"uri":"https:\/\/zotonic.com\/id\/2703","uri_template":"https:\/\/zotonic.com\/id\/:id","websub":{"hub":"https:\/\/zotonic.com\/.zotonic\/websub","topic":"https:\/\/zotonic.com\/.zotonic\/websub\/topic\/2703"}},"status":"ok"}