{"result":{"depiction_url":null,"edges":{"observes":{"objects":[{"created":"2026-09-09T13:47:46Z","object_id":{"id":2005,"is_a":["text","documentation","reference","notification"],"name":"doc_notification_module_activate","title":"observe_module_activate\/2","uri":"https:\/\/zotonic.com\/id\/2005"},"seq":1},{"created":"2026-09-09T13:47:46Z","object_id":{"id":2153,"is_a":["text","documentation","reference","notification"],"name":"doc_notification_action_event_type","title":"observe_action_event_type\/2","uri":"https:\/\/zotonic.com\/id\/2153"},"seq":2}],"predicate":{"id":2550,"is_a":["meta","predicate"],"name":"observes","title":{"_type":"trans","tr":{"en":"Observes"}},"uri":"https:\/\/zotonic.com\/id\/observes"}},"references":{"objects":[{"created":"2020-05-30T05:47:54Z","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":1000000}],"predicate":{"id":332,"is_a":["meta","predicate"],"name":"references","title":{"_type":"trans","tr":{"en":"References"}},"uri":"https:\/\/zotonic.com\/id\/references"}},"refers":{"objects":[{"created":"2026-09-09T13:47:46Z","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":1000000}],"predicate":{"id":2409,"is_a":["meta","predicate"],"name":"refers","title":{"_type":"trans","tr":{"en":"Refers"}},"uri":"https:\/\/zotonic.com\/id\/refers"}},"relation":{"objects":[{"created":"2020-05-30T05:47:54Z","object_id":{"id":1323,"is_a":["text","documentation","reference","template_scomp"],"name":"doc_template_scomp_scomp_live","title":"live","uri":"https:\/\/zotonic.com\/id\/1323"},"seq":1000000}],"predicate":{"id":303,"is_a":["meta","predicate"],"name":"relation","title":{"_type":"trans","tr":{"nl":"Relatie","en":"Relation"}},"uri":"http:\/\/purl.org\/dc\/terms\/relation"}},"subject":{"objects":[{"created":"2026-09-09T13:47:46Z","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-09T13:47:46Z","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-09T13:47:46Z","object_id":{"id":2598,"is_a":["categorization","keyword","keyword_domain"],"name":"zotonic_topic_messaging_and_pubsub","title":"Messaging and publish-subscribe","uri":"https:\/\/zotonic.com\/id\/2598"},"seq":3},{"created":"2026-09-09T13:47:46Z","object_id":{"id":2635,"is_a":["categorization","keyword","keyword_architecture"],"name":"zotonic_topic_module","title":"Module","uri":"https:\/\/zotonic.com\/id\/2635"},"seq":4},{"created":"2026-09-09T13:47:46Z","object_id":{"id":2655,"is_a":["categorization","keyword","keyword_task"],"name":"zotonic_topic_publish_and_subscribe","title":"Publish and subscribe","uri":"https:\/\/zotonic.com\/id\/2655"},"seq":5},{"created":"2026-09-09T13:47:46Z","object_id":{"id":2677,"is_a":["categorization","keyword","keyword_technology"],"name":"zotonic_topic_mqtt","title":"MQTT","uri":"https:\/\/zotonic.com\/id\/2677"},"seq":6}],"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":1324,"is_a":["text","documentation","reference","module"],"links":[{"rel":"self","target":"https:\/\/zotonic.com\/.zotonic\/websub\/topic\/1324"},{"rel":"hub","target":"https:\/\/zotonic.com\/.zotonic\/websub"}],"medium":null,"medium_url":null,"name":"doc_module_mod_mqtt","page_url":{"en":"https:\/\/zotonic.com\/docs\/1324\/mod_mqtt","x-default":"https:\/\/zotonic.com\/docs\/1324\/mod_mqtt"},"preview_url":null,"resource":{"version":3992,"pivot_location_lat":null,"title":"mod_mqtt","is_authoritative":true,"body":"<p><a href=\"http:\/\/mqtt.org\">MQTT<\/a> is a machine-to-machine (M2M)\/“Internet of Things” connectivity protocol. It was designed as an extremely lightweight publish\/subscribe messaging transport. It is useful for connections with remote locations where a small code footprint is required and\/or network bandwidth is at a premium. For example, it has been used in sensors communicating to a broker via satellite link, over occasional dial-up connections with healthcare providers, and in a range of home automation and small device scenarios<\/p>\n<p>MQTT uses a simple message broker to route messages from publishers to multiple subscribers.<\/p>\n<h2>A quick overview of MQTT<\/h2>\n<h3>Publish\/subscribe<\/h3>\n<p>With MQTT messages are published to topics. All subscribers to a topic will then receive the message. A topic is a\nstring, much like a file-path, for example: <code>truck\/0001\/temperature<\/code><\/p>\n<p>A subscriber can directly subscribe to a topic, or can use wildcards to subscribe to related topics. For this the\nwildcards <code>+<\/code> and <code>#<\/code> can be used. <code>+<\/code> matches exactly one word between the slashes, and <code>#<\/code> can be used at the end of a\npattern to match all sub-topics.<\/p>\n<p>Examples of subscription patterns:<\/p>\n<ul><li><code>truck\/0001\/temperature<\/code> matches the temperature publications of truck 0001.<\/li><li><code>truck\/+\/temperature<\/code> matches all temperature publications for all trucks.<\/li><li><code>+\/+\/temperature<\/code> matches all temperature publications for all trucks and other things with a temperature<\/li><li><code>truck\/0001\/#<\/code> matches all publishes to <code>truck\/0001<\/code> and all its sub-topics<\/li><\/ul>\n<h3>Retained messages<\/h3>\n<p>A publisher can publish a <em>retained<\/em> message to a topic. When publishing all current topic subscribers will receive the\nmessage. Above that, if a new subscription is made to the topic, then all retained messages are sent to the new subscriber.<\/p>\n<h3>Quality of service<\/h3>\n<p>MQTT has three levels for the quality of message delivery. These are used when sending messages between machines. The\nlevels are:<\/p>\n<ul><li>Level 0: send the message, no reception acknowledgments are reported.<\/li><li>Level 1: on receipt a single ack is sent back to the publisher<\/li><li>Level 2: a double handshake is performed<\/li><\/ul>\n<p>For most communication level 0 is used.<\/p>\n<h3>Wills<\/h3>\n<p>A client can set a <em>last will<\/em> message and topic. This is a message that will be published to the topic at the moment\nthe client is unexpectedly disconnected.<\/p>\n<h2>MQTT in Zotonic<\/h2>\n<p>Zotonic has a central MQTT message broker. Optionally clients can connect to this broker using the normal MQTT protocol.<\/p>\n<p>The broker is used for internal publish\/subscribe support.<\/p>\n<p>Each open HTML page can also have a local (simplified) broker. The system can relay messages between the brokers on open\npages and the central broker in Zotonic. In this way it is possible for HTML pages to have their own local\npublish\/subscribe system and also subscribe or publish to topics on the central broker.<\/p>\n<p>As the central broker is shared between sites it is even possible to publish\/subscribe between different sites. In the\nfuture it will be possible to bridge the brokers between servers.<\/p>\n<h3>Predefined topics<\/h3>\n<p>Currently the following topics are defined:<\/p>\n<table class=\"table\"><thead><tr><th>Topic<\/th><th>Description<\/th><\/tr><\/thead><tbody><tr><td>public<\/td><td>Freely accessible topic, both for subscribe and publish<\/td><\/tr><tr><td>test<\/td><td>Test topic. If you publish here then <code>mod_mqtt<\/code> will log a debug message.<\/td><\/tr><tr><td>user<\/td><td>Topic available for any authenticated user<\/td><\/tr><tr><td>user\/UserId<\/td><td>Topic available for a specific user of the site<\/td><\/tr><tr><td>bridge\/ClientId<\/td><td>The topic forwarding to the client with id ClientId<\/td><\/tr><\/tbody><\/table>\n<h3>Topics and namespaces<\/h3>\n<p>To make it easier to write generic software, without changing topic names, some namespace conventions and mappings are introduced.<\/p>\n<p>The following topics are expanded:<\/p>\n<table class=\"table\"><thead><tr><th>Topic<\/th><th>Expansion<\/th><th>Description<\/th><\/tr><\/thead><tbody><tr><td>~client<\/td><td>bridge\/vWCUKL9QKmfLxotWorZv<\/td><td>The bridge topic that forwards to the user agent<\/td><\/tr><tr><td>~user<\/td><td>user\/1234 <em>or<\/em> user\/anonymous<\/td><td>The topic for the current user<\/td><\/tr><\/tbody><\/table>\n<p>Note that there are not automatic subscriptions for user topics. All subscriptions need to be added explicitly.<\/p>\n<h3>Access control<\/h3>\n<p>All topics have access control added. For this an extra <code>#acl_mqtt{}<\/code> ACL object is\ndefined, with the actions <code>publish<\/code> and <code>subscribe<\/code>; see <a href=\"\/id\/doc_notification_acl_mqtt\" class=\"doc-reference doc-reference-notification\"><code>notification#acl_mqtt<\/code><\/a>.\nModules can observe the usual <a href=\"\/id\/doc_notification_acl_is_allowed\" class=\"doc-reference doc-reference-notification\"><code>notification#acl_is_allowed<\/code><\/a> notification to\nallow access to MQTT topics:<\/p>\n<p>your_site.erl<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-erlang\">observe_acl_is_allowed(#acl_is_allowed{object = #acl_mqtt{topic = [ &lt;&lt;&quot;my&quot;&gt;&gt;, &lt;&lt;&quot;topic&quot;&gt;&gt; ]}}, _Context) -&gt;\n    %% Allow anonymous access on this topic\n    true;\nobserve_acl_is_allowed(#acl_is_allowed{}, _Context) -&gt;\n    undefined.\n<\/code><\/pre>\n<h3>Subscribing modules<\/h3>\n<p>Modules can automatically subscribe to topics. This is done by adding specially named functions.<\/p>\n<p>For example, the following function subscribes to the topic <code>test\/#<\/code>:<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-erlang\">-include_lib(&quot;kernel\/include\/logger.hrl&quot;).\n\n-export([\n    &#39;mqtt:test\/#&#39;\/2\n]).\n\n-spec &#39;mqtt:test\/#&#39;( map(), z:context() ) -&gt; ok.\n&#39;mqtt:test\/#&#39;(Message, Context) -&gt;\n    ?LOG_DEBUG(&quot;mqtt:test on site ~p received ~p&quot;, [ z_context:site(Context), Message ]),\n    ok.\n<\/code><\/pre>\n<p>Here <em>Message<\/em> is a map with the received MQTT message (of type <code>publish<\/code>):<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-erlang\">#{\n    type =&gt; publish,\n    pool =&gt; Pool,                 % A MQTT pool (and topic tree) per site\n    topic =&gt; Topic,               % Unpacked topic for the publish [ &lt;&lt;&quot;foo&quot;&gt;&gt;, &lt;&lt;&quot;bar&quot;&gt;&gt; ]\n    topic_bindings =&gt; Bound,      % Variables bound from the topic\n    message =&gt; Msg,               % The MQTT message itself\n    publisher_context =&gt; PublisherContext\n}\n<\/code><\/pre>\n<p>The <em>Context<\/em> is the context of the process\/user that subscribed to the message. Use the <code>publisher_context<\/code> for the\n<em>Context<\/em> (and ACL permissions) of the publisher.<\/p>\n<h3>Erlang API<\/h3>\n<p>Subscribe a function F in a module M to a topic:<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-erlang\">-spec subscribe(z_mqtt:topic(), mfa(), pid(), z:context()) -&gt; ok | {error, eacces | term()}.\nz_mqtt:subscribe([ &lt;&lt;&quot;my&quot;&gt;&gt;, &lt;&lt;&quot;topic&quot;&gt;&gt;, &#39;#&#39; ], {M, F, []}, self(), Context)\n<\/code><\/pre>\n<p>This will subscribe the function, with the current process (<code>self()<\/code>) as the managing process. If the process exits then\nthe subscription is removed.<\/p>\n<p>Access control applies and the result <code>{error, eacces}<\/code> will be returned if access is denied, <code>ok<\/code> will be returned on a\nsuccesful subscription.<\/p>\n<p>Subscribe the current process to a topic:<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-erlang\">-spec subscribe(z_mqtt:topic(), z:context()) -&gt; ok | {error, eacces | term()}.\nz_mqtt:subscribe(Topic, Context)\n<\/code><\/pre>\n<p>When the process stops it will automatically be unsubscribed. The process will receive messages <code>{mqtt_msg, map()}<\/code>,\nwhere the <code>map()<\/code> is like the map in the section above.<\/p>\n<p>Subscribe another process to a topic:<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-erlang\">-spec subscribe(z_mqtt:topic(), pid(), z:context()) -&gt; ok | {error, eacces | term()}.\nz_mqtt:subscribe(Topic, Pid, Context)\n<\/code><\/pre>\n<p>To unsubscribe, use <code>z_mqtt:unsubscribe<\/code> with the same arguments as during subscription.<\/p>\n<p>To publish a message:<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-erlang\">-spec publish( z_mqtt:topic(), term(), z:context() ) -&gt; ok | {error, term()}.\nz_mqtt:publish(Topic, Payload, Context)\n<\/code><\/pre>\n<p>With options (<code>qos<\/code> or <code>retain<\/code>):<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-erlang\">-spec publish( z_mqtt:topic(), term(), z_mqtt:publish_options(), z:context() ) -&gt; ok | {error, term()}.\nz_mqtt:publish(Topic, Payload, #{ qos =&gt; 1, retain =&gt; true }, Context)\n<\/code><\/pre>\n<p>Or, with a complete MQTT message:<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-erlang\">-spec publish( mqtt_packet_map:mqtt_packet(), z:context()) -&gt; ok | {error, term()}.\nMsg = #{\n    type =&gt; publish,\n    qos =&gt; 0,\n    topic =&gt; [ &lt;&lt;&quot;~client&quot;&gt;&gt;, &lt;&lt;&quot;public&quot;&gt;&gt;, &lt;&lt;&quot;hello&quot;&gt;&gt; ],\n    payload =&gt; #{ key =&gt; 1, foo =&gt; &lt;&lt;&quot;bar&quot;&gt;&gt; }\n},\nz_mqtt:publish(Msg, Context)\n<\/code><\/pre>\n<h3>JavaScript API<\/h3>\n<p>Every browser page has a local Cotonic broker. Use <code>cotonic.broker.subscribe<\/code> and <code>cotonic.broker.publish<\/code> for messages\nwithin that browser context:<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-javascript\">cotonic.broker.subscribe(&quot;example\/+name&quot;, function (msg, bindings) {\n    console.log(bindings.name, msg.payload);\n});\n\ncotonic.broker.publish(&quot;example\/world&quot;, { greeting: &quot;Hello&quot; });\n<\/code><\/pre>\n<p>The browser broker is connected to the Zotonic server through the <code>origin<\/code> bridge. Prefix a server-side topic with\n<code>bridge\/origin\/<\/code> when publishing or subscribing in the browser. For example, this subscribes to the server topic\n<code>public\/hello<\/code>:<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-javascript\">cotonic.broker.subscribe(&quot;bridge\/origin\/public\/hello&quot;, function (msg) {\n    console.log(msg.payload);\n});\n<\/code><\/pre>\n<p>Messages sent by Erlang to a <code>~client<\/code> topic are routed back to that client. This publishes to the browser-local topic\n<code>example\/notice<\/code> belonging to the current client:<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-erlang\">z_mqtt:publish(\n    [ &lt;&lt;&quot;~client&quot;&gt;&gt;, &lt;&lt;&quot;example&quot;&gt;&gt;, &lt;&lt;&quot;notice&quot;&gt;&gt; ],\n    #{ text =&gt; &lt;&lt;&quot;Updated&quot;&gt;&gt; },\n    Context\n).\n<\/code><\/pre>\n<p><code>subscribe<\/code> accepts options including <code>qos<\/code>; <code>publish<\/code> accepts <code>qos<\/code> and <code>retain<\/code>. QoS defaults to 0. Access control\nis checked on the server after bridge topic mappings have been applied.<\/p>\n<h2>Enabling the MQTT listener<\/h2>\n<p>MQTT can listen on a port for incoming connections. Per default the listener is enabled.<\/p>\n<h3>Configuration<\/h3>\n<p>The MQTT listener is configured in the <code>zotonic.config<\/code>. Use <code>bin\/zotonic configfiles<\/code> to see where this file is located.<\/p>\n<p>If this file is missing then it can be copied from <code>~apps\/zotonic_launcher\/priv\/zotonic.config.in<\/code>.<\/p>\n<p>Per default it listens on MQTT port 1883 and MQTT with TLS on port 8883:<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-erlang\">%%% IP address for MQTT connections - defaults to &#39;listen_ip&#39;\n%%% Use &#39;none&#39; to disable.\n   %% {mqtt_listen_ip, any},\n\n%%% IPv6 address for MQTT connections - defaults to &#39;listen_ip6&#39;\n%%% Use &#39;none&#39; to disable.\n   %% {mqtt_listen_ip6, any},\n\n%%% Port number for MQTT connections\n   %% {mqtt_listen_port, 1883},\n\n%%% Port number for MQTT ssl connections\n   %% {mqtt_listen_ssl_port, 8883},\n<\/code><\/pre>\n<h3>Authentication<\/h3>\n<p>All connections must authenticate using a username and password. The username is prefixed with the hostname of the\nuser’s site, for example: <code>foobar.com:myusername<\/code>. In this way Zotonic knows which site the user belongs to.<\/p>\n<p>If no matching site can be found, or if no hostname is given, then Zotonic will try to authenticate against the default site.<\/p>\n<h2>Accepted Events<\/h2>\n<p>This module handles the following notifier callbacks:<\/p>\n<ul><li><code>observe_acl_is_allowed<\/code>: Apply module ACL checks for the requested action\/object.<\/li><li><code>observe_action_event_type<\/code>: Register the <code>{mqtt, ...}<\/code> action event type so wire actions can subscribe through MQTT.<\/li><li><code>observe_module_activate<\/code>: On module activation, scan the module for exported <code>mqtt:<\/code> observer callbacks and subscribe them as MQTT handlers using a sudo context.<\/li><\/ul>","slug":"mod_mqtt","is_protected":false,"visible_for":0,"tz":"UTC","language":["en"],"doc_source_hash":"19d6a11dae6685fd6d94733de35ee1dc1dff8d9ff57a204f7115ef5230040ee9","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_mqtt\/src\/mod_mqtt.erl","publication_start":"2023-06-24T08:17:57Z","github_url":"https:\/\/github.com\/zotonic\/zotonic\/blob\/master\/apps\/zotonic_mod_mqtt\/src\/mod_mqtt.erl","pivot_location_lng":null,"doc_source_kind":"module","name":"doc_module_mod_mqtt","is_unfindable":false,"is_published":true,"pivot_geocode":null,"created":"2020-05-30T05:47:18Z","uri":null,"doc_status":"current","is_dependent":false,"erlang_app":"zotonic_mod_mqtt","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":"66cb145eae7002b184e7d8c4c7bba5e53e8248e5\n","creator_id":{"id":336,"is_a":["person","robot"],"name":"gitbot","title":"Git","uri":"https:\/\/zotonic.com\/id\/336"},"modified":"2026-10-02T12:40:20Z","title_slug":"mod_mqtt"},"uri":"https:\/\/zotonic.com\/id\/1324","uri_template":"https:\/\/zotonic.com\/id\/:id","websub":{"hub":"https:\/\/zotonic.com\/.zotonic\/websub","topic":"https:\/\/zotonic.com\/.zotonic\/websub\/topic\/1324"}},"status":"ok"}