Due to a family health emergency, the OpMeet that has been moved to the 31st October needs to be cancelled. OpMeet will hopefully return hopefully in early 2027.
Apologies to anyone who was planning on attending, and for those who put their ideas forward, this was unavoidable.
OpMeet are online meetups/conferences targeted at XMPP Operators.
With the latest update to the popular Conversations app, which requires “channel binding” by default, lots of people are encountering channel binding for the first time. This post aims to answer questions for both Prosody operators and users.
Channel binding is a security feature which helps to keep your connection secure. We’ll dig into exactly what it is, how it is used, and what (if anything) you need to do.
When you establish connections on the internet today, they will almost certainly be using TLS (on the web, TLS connections are referred to as HTTPS). It is well known that TLS provides encryption, but there is no point in encrypting your bytes if you are sending them to an attacker, e.g. someone who can intercept or divert your internet traffic. TLS therefore provides something else - authentication, or the ability to cryptographically prove who you are communicating with - using certificates.
Anyone can create a certificate that claims they are anybody. You can trivially
use the openssl command-line tool to create a certificate for microsoft.com
or google.com. But nobody would trust your certificate - you have to have it
signed by a recognised Certificate Authority. The most well-known CA is
Let’s Encrypt, but there are many more. Operating systems and browsers ship a
list of trusted certificate authorities, and software automatically trusts
certificates that have been signed by one of these CAs.
However, this arrangement has suffered numerous problems. Any trusted CA can issue a certificate for any domain, and in the past, they have. By utilizing Chrome’s pinning and telemetry, Google discovered multiple cases of CAs issuing certificates for domains without permission. This led them to develop Certificate Transparency, a system of public logs that (these days) every CA trusted by browsers is required to submit every single issued certificate to, leading to transparency and accountability.
Browsers have now started requiring every certificate to include a proof that it has been submitted to a Certificate Transparency log. This is known as a Signed Certificate Timestamp (SCT).
SCTs solve the problem of CAs silently misissuing certificates - via CT logs every certificate can now be tracked and audited. CAs found to have incorrectly issued certificates can be investigated and potentially distrusted by browsers and operating systems.
But the CT/SCT solution isn’t perfect. It provides a record after the attack has already happened, and this record can assist with remediation. But it does not prevent the attack in the first place.
It also does not protect against:
A case of a compromised CA happened just a couple of weeks ago. The eMudhra CA incident found that this CA’s keys were extremely weak and guessable, meaning anyone could generate a certificate that looked like it was officially signed by eMudhra. Although CT helped to determine the impact after the compromise was discovered, during the period between the discovery of the weak keys and the CA being distrusted by systems, anyone could forge a valid-looking certificate, submit it to CT logs, and obtain SCTs for the forged certificates. The attack was not prevented by CTs/SCTs, they only made things auditable after the compromise became known.
In another kind of incident in 2023, a public XMPP server was found to have had its TLS connections intercepted. Because the intercepting machine had access to the legitimate server’s traffic, it was also able to intercept the “challenge” requests from Let’s Encrypt, and obtain a valid certificate. This certificate was published in CT logs, while the attack lasted for months.
From the perspective of Let’s Encrypt, this was not a misissued certificate.
They performed the expected checks before issuing the certificate.
Unfortunately the victim domain did not have a CAA record with a pinned
accounturi. This was a mechanism introduced in 2019 to prevent
this kind of attack.
However, even with CAA records, it is not that hard to imagine situations where a CA could be tricked or compelled into issuing certificates.
Another mechanism - you guessed it, channel binding - would have made both these attacks immediately detectable by XMPP clients.
Enough of the problems. What can we do? Well, XMPP has for some time supported a technique known as channel binding. This is actually part of the authentication layer - the layer that verifies your password/token - and all it does is cryptographically “bind” that authentication to the encrypted data channel (your TLS connection).
To work, channel binding requires a few things:
In a non-mutual authentication mechanism, the client might send the password to the server and the server says “Correct password! You’re now authenticated.” But with mutual authentication, the client submits proof that it has the password/token, and the server also returns a proof that it has the password/token (or a hash of it) to the client. This allows the client to ensure it is logging in to the legitimate server.
Client Attacker Server
(MITM)
| | |
| == TLS connection A ==> | === TLS connection B ===> |
| attacker's valid cert | attacker poses as client |
| | |
|-- password proof ------->|-- relayed as-is ---------->| proof OK
|<---------- server proof -|<----------- relayed as-is -|
| | |
Logged in! Attacker sees and can modify Logged in!
everything in both directions
SCRAM is a widely used authentication mechanism which supports mutual authentication and channel binding (SCRAM “PLUS”), and allows the client and the server to store a hashed version of the credentials.
Channel binding extends mutual authentication to include unique per-connection data from the TLS layer. This means that the entity you are authenticating with must now also be the entity that is decrypting your TLS connection. If you have a malicious machine in the middle, decrypting the client’s TLS and then encrypting it again and sending it onwards to the real server, this would now be detected - because the client’s connection to the MITM is not the same connection as the one between the MITM and the server.
Client Attacker Server
(MITM)
| | |
| == TLS connection A ==> | === TLS connection B ===> |
| | |
|-- proof bound to A ----->|-- relayed as-is ---------->| I'm on B, but
| | | this proof is
| | | for A: REJECT
| |<----------------- failure -|
| | |
The attacker can't generate a correct proof for connection B
because it needs knowledge of the password. For the same reason,
it also can't fake a proof from the server and trick the client
into believing that authentication succeeded.
Channel binding seems rather magical. It doesn’t depend on the CA ecosystem at all, and works with any certificate, including self-signed. It doesn’t matter if the CA has been compromised - if your mutual authentication with channel binding passes, your connection is being handled by the same server that you are authenticating to.
It prevents a range of attacks, and is relatively simple from an operational perspective. However, for it to be an effective defence, the client must not simply try authentication without channel binding when channel binding fails. Otherwise an attacker can just make channel binding fail, and when the client tries again without channel binding, the attack will succeed. This is known as a downgrade attack.
Channel binding is actually already enabled in Prosody out of the box, there is nothing specific you need to do to enable it. The requirements are simply:
authentication = "internal_hashed" and network_backend = "epoll" (the libevent backend also works in latest 13.0.x)But this doesn’t mean that it is available in every deployment. As discussed above, channel binding provides proof that the server authenticating you is the same server your TLS connection is being handled by. Channel binding cannot work if:
In both these cases, channel binding breaks because the situation is similar to a MITM attack. Something is decrypting your TLS connection before the authentication. Channel binding is trying to ensure that your authentication server is also your encryption server.
For the TLS offloading case, there is a version of channel binding which can work by providing
a hash of the certificate. This will then be compared with the hash of the certificate that the
client sees. It has weaker security guarantees, but it allows things to work even if
Prosody is not terminating the TLS connection. If you have such a setup, set tls_server_end_point_hash
in your configuration to the hash of your DER-encoded certificate, and note that this needs
to be updated every time your certificate changes (which, with modern CAs, can be rather frequently).
If you’re having problems with channel binding and you’re not sure why, enable
debug logs (or use prosodyctl shell watch log) - it’s likely that the debug
logs will tell you whether channel binding is available, and if not, why not.
Some external services can work in theory. Authentication providers which are compatible with channel binding are those which can provide to Prosody either:
If your authentication provider is receiving either a plain or hashed password from Prosody, and making its own decision about whether the authentication was successful, your authentication provider is very likely to be incompatible with channel binding.
If your server doesn’t support channel binding, it’s not the end of the world.
The new Conversations requirement for channel binding can be turned off in the security settings of the app, and you only need to do this once. Even with the setting disabled, Conversations will still use channel binding when a server supports it. For other servers you will still benefit from standard TLS, just make sure you have taken other precautions such as adding CAA records for your domain.
Channel binding is great, but it’s not going to save you from every kind of attack (nothing will). You should still do things like add CAA records and use DNSSEC.
Channel binding has the following weaknesses:
Channel binding is a powerful tool for ensuring connection security. It’s generally automatically enabled for most people, if your server is up to date and has the recommended configuration.
If not, just disable the requirement in Conversations. Your connections will still be encrypted!
As a bit of background, I have been to a few XMPP stands organised by others, at FOSDEM and FrOSCon, seeing their strengths and where I felt they were weaker. This was my first attempt at organising a stand, and to my surprise it was more successful than I could have ever dreamed of.
We had a large proportion of the conference come over and chat, not just take stickers, and leaflets like many other stands at other conferences, but full on in-depth discussions, which inevitably led to the XMPP stand becoming a hackerspace of sorts, the organisers suggesting we pull more tables over to the end so that people could hack at the end of the stand and not block the main stand.
We also trialled some new ideas, which I haven’t seen at any other stands so far. I will also cover the ideas which are common at every stand.

OmniOpenCon 2026 Devices stand
Like every stand, we had stickers. This in my opinion doesn’t really help too much with getting people interested in XMPP, most people end up throwing stickers into a box at home. This doesn’t mean they are a waste of time, people who are passionate about XMPP love to be able to decorate their devices with stickers, which then helps with making XMPP more mainstream and familiar.
para designed new leaflets for the conference, these were more popular than the stickers for newcomers, because it explained exactly what XMPP is, and on the back explained how to try XMPP, even the organisers of the conference were taking a look at the leaflets, and could potentially play with XMPP in the future.
Leaflets are also ridiculously cheap to print (compared to stickers) making them much more disposable, allowing you to spread XMPP to more people for less.
The device demos we picked were to target the most platforms possible, so that anyone who came over to the stand could see the interop between different clients and devices. We had an iPhone with Monal IM, two Androids with Conversations and Cheogram and a Linux laptop with Gajim (which would also run on Windows as well).
As with all conferences, the device demos were popular, and people like playing with them. However they are much more of a “woah cool” rather than a method of getting people onto XMPP.

OmniOpenCon 2026 Devices Close-up
Edward designed two example diagrams showing at a high level how servers federate, and users message across different servers. This also was invaluable in my talk to describing how the XMPP protocol functioned. These diagrams made it far easier to explain to people who were less tech-savvy how XMPP is beneficial to them, and why it would suit their use case.
On top of this, para had rendered an A3 image of the XMPP operators graph, as this is so big and hard to render on the web that under conference conditions it’s hard to have it on hand. Having it printed on paper allowed us to use it as a high level overview of how many servers (which publish themselves) are federating within the network, with the protocol diagrams being a “slice” of that network.

OmniOpenCon 2026 Diagram Stand
I have not yet seen this as at any stand personally. I have seen the use of a demo MUC hosting on hardware at the conference, but not an actual public MUC with a QR code for people to join as they walked past the stand.
This was probably the most successful part of the stand, those who already used XMPP or had dabbled in it came past and joined the MUC and were chatting throughout the conference, and even days later theres been regular activity after the conference. This has allowed people to make new connections, which is something XMPP has always struggled with providing due to the limited number of communities outside of XMPP development.
We printed the XMPP URI off as a QR code stuck it in the middle of the stand.
We had two operators (other than myself) who were at the stand. We had already decided in advance to try to break the cycle of people coming over, speaking and then forgetting about XMPP, that we would hand out invite links along with the leaflets they could collect, so they could easily install the app on their phone at the stand, and scan a QR code, make a username and then immediately join the MUC.
This had some success, some people still decided to use random providers as is their freedom to, but we also saw a few people sign up via the invite links.
The invite links were printed off and then cut up into sticker-sized QR codes which people could take away with them.
Unfortunately, due to a mistake, half way through the day the invite link was invalidated, and all the invite links distributed in the morning had to be discarded. For this reason, we can not draw a definite conclusion whether this method of onboarding is actually successful or not. Better luck next time.
Unfortunately, I was given the last talk slot before the closing notes. This meant a lot of people had already left by the time my talk was up, and I don’t feel like it properly reached the audience I intended. The talk was focused at sparking interest in XMPP, rather than a deep dive into some topic within XMPP. However the majority of the audience already used XMPP.
Nevertheless, it was a fun talk, and at the bare minimum I got to thank everyone who had been involved in making the stand possible.
The stand was definitely the highlight fo the conference, it drew many more people towards it, and was far better at onboarding and getting new people interested in XMPP.
You can see the slides here.
Most talks are just people standing at the front speaking about something for the allotted time, however we instead handed out XMPP leaflets, and invite links to everyone who attended. This provided them with the information to go away after the talk and look into it more if they were interested.
Due to the few people who attended the talk, I can’t say if this would be effective if scaled, but 1-2 people who attended my talk and got the leaflet ended up making an account and messaging me after the conference, so at the limited scale I had, it was successful.
The XMPP Standards Foundation (XSF), together with our friends from the Matrix and Delta Chat communities, is organising a Decentralised Messaging devroom at FOSDEM, and we are looking for speakers!
Focused on federated and self-hostable chat, this devroom brings our ecosystems together to tackle shared challenges. Although our architectures look pretty different under the hood, we face many of the same hurdles. We want to get together to share operational and implementation experiences, discuss shared technical challenges, and look at practical ways our networks can cooperate.
Our own communities have plenty of material to share, and we are also deliberately reaching beyond them to invite contributors working on chat across other federated and decentralised ecosystems (such as AT Protocol and ActivityPub), as well as developers building reusable components like moderation tools.
Talks that others can learn from are preferred, and marketing talks will be rejected.
FOSDEM is an annual conference about free and open source software. It takes place at the ULB Solbosch Campus, Brussels, Belgium on 30 & 31 January 2027. See https://fosdem.org/.
Please distribute this CFP as widely as possible to interested parties!
We welcome technical presentations, demos, and experience reports on topics including:
We ask for a short talk abstract, a proposed duration, and a brief speaker biography. Links to your code repositories, project websites, or prior talks are also appreciated.
Submissions must be made through Pretalx (https://fosdem.org/submit) no later than Friday, November 20, 2026.
The suggested duration for a regular talk is 25 minutes (20 minutes presentation + 5 minutes Q&A). If the best format for your project or idea is a 5-to-10-minute lightning talk, we will happily accommodate lightning sessions as well. Talks are streamed and published under CC-BY, and the FOSDEM Code of Conduct applies.
Depending on the volume and variety of submissions, we may contact you to adapt your talk length to fit the final room schedule.
A programme committee with at least one reviewer from each organising community reviews all submissions jointly. Talks will be selected based on technical interest, relevance, and overall project diversity across the schedule.
If you would like to help to keep things running smoothly on the day, we’d be grateful for a few extra hands! Please get in touch via decentralised-messaging-devroom-manager at fosdem.org
On behalf of the Decentralised Messaging devroom managers, The XSF Summits, Conferences, and Meetups Team (SCAM) Daniel & Guus
along with our co-managers Andy, Holger, Matthew, and Saul
Earlier this year another XMPP app has added XMPP Providers support to its onboarding. Converse.js suggests an XMPP provider and allows to select one during account registration. That web app is hosted by many XMPP operators to provide their users a decent web experience. We are very happy about this development and further distribution. Using XMPP Providers will enhance the onboarding experience for many people.

Screenshot of XMPP Providers support in Converse.js.
To continue enabling true federation and reduce manual work, the creation of merge requests to add new providers has been automated. One simply needs to follow the steps to add a missing provider. Once an issue has been created, our bot, will create a merge request including an initial check to update the provider’s properties. Considering the growing demand of providers that want to be listed, the new automation (as you may have recognised already) will accelerate the whole process from the first request to the final inclusion. Furthermore, it allows us to focus on developing more features.
Thanks to everyone for being patient with us! It is great to see so many old XMPP services still being run and new ones constantly being established. Moreover, many thanks to the XMPP apps for supporting this service as well as using it for their onboarding. That is the best way of using XMPP Providers!
For a good user experience, apps integrating XMPP Providers are as important as the providers themselves. If you are an XMPP developer, please consider adding XMPP Providers support to your app. If you are an operator of a public XMPP service, add your service to the list.
You can also consider to run your own XMPP server and become a public provider. With our automated XMPP server setup, you can operate a fully-featured XMPP server with minimal effort.
Feel free to reach out to us if you have any questions! If you like to support XMPP Providers, please consider making a donation. Follow us and spread the word!
XMPP Providers Logo
The recent eMudhra Intermediate CA Compromise is a security threat for ejabberd if you use s2s, modules that make outgoing HTTPS requests, or TLS connections to MQTT brokers (mod_mqtt_bridge).
To check if your ejabberd is vulnerable, you can use the following code snippet (works on ejabberd 19.08 and later).
Open the debug console with:
ejabberdctl debug
Paste the following code into the debug console (the indented version of this code is available at the end of this post):
(fun() -> Host = "dv-g3b-emudhra.we-have-a-problem.com", Fail = fun(Err) -> {"unable to connect to test server", Err} end, case gen_tcp:connect(Host, 443, [binary, {active, false}]) of {ok, Sock} -> case fast_tls:tcp_to_tls(Sock, [connect, {cafile, ejabberd_option:ca_file()}]) of {ok, TSock} -> _ = fast_tls:send(TSock, list_to_binary("HEAD / HTTP/1.0\r\nHost: " ++ Host ++ "\r\n\r\n")), case fast_tls:recv(TSock, 0, 10000) of {ok, _} -> case fast_tls:get_peer_certificate(TSock) of {ok, Cert} -> FT = case fast_tls:get_cert_verify_string(fast_tls:get_verify_result(TSock), Cert) of <<"ok">> -> "vulnerable"; _ -> "ok" end, HT = case list_to_integer(erlang:system_info(otp_release)) >= 26 of true -> case httpc:request("https://" ++ Host) of {ok, _} -> "vulnerable"; _ -> "ok" end; false -> "not verified (OTP < 26)" end, "fast_tls status: " ++ FT ++ ", ssl status: " ++ HT; E4 -> Fail(E4) end; E3 -> Fail(E3) end; E2 -> Fail(E2) end; E1 -> Fail(E1) end end)().
Exit the console with Ctrl-C twice.
It returns "fast_tls status: ok, ssl status: ok" if you are not vulnerable. If either status is vulnerable, please read on.
If you are running ejabberd with the official Docker image, you can update to 26.09.1 to fix the problem. It just removes the problematic certificates from the OS; it still runs ejabberd 26.09. Pull ghcr.io/processone/ejabberd:26.09.1 and recreate your container.
To fix it manually without restarting ejabberd, you can follow this procedure (requires Erlang/OTP 25+; on older versions, restart ejabberd).
First, update the CA bundle on your OS by upgrading the ca-certificates package:
# Debian / Ubuntu
apt update && apt install --only-upgrade ca-certificates
# RHEL / Rocky / Alma / Fedora
dnf update ca-certificates
# Alpine
apk upgrade ca-certificates
If your distribution has not shipped an updated package yet, you can distrust the certificates manually: see update-ca-certificates(8) on Debian/Ubuntu, or Using shared system certificates on RHEL-based systems.
Once the OS bundle is updated, paste the following into the debug console to reload it:
public_key:cacerts_clear(), public_key:cacerts_load(), ejabberd_admin:reload_config().
Exit the console with Ctrl-C twice.
Finally, re-run the check above to verify that you are not vulnerable anymore.
If you are still vulnerable, your ejabberd may be using CA certs from a specific location. You can use the following functions in the debug console to check where to update your certs (ca_file shows the CA file actually in use; if s2s_cafile is undefined, s2s uses ca_file).
ejabberd_option:ca_file().
ejabberd_option:s2s_cafile().
If you are using some s2s CA cert per vhost, pass it as a parameter, for example:
ejabberd_option:s2s_cafile(<<"example.com">>).
Here is the check above, indented for readability:
(fun() ->
Host = "dv-g3b-emudhra.we-have-a-problem.com",
Fail = fun(Err) -> {"unable to connect to test server", Err} end,
case gen_tcp:connect(Host, 443, [binary, {active, false}]) of
{ok, Sock} ->
case fast_tls:tcp_to_tls(Sock, [connect, {cafile, ejabberd_option:ca_file()}]) of
{ok, TSock} ->
_ = fast_tls:send(TSock, list_to_binary("HEAD / HTTP/1.0\r\nHost: " ++ Host ++ "\r\n\r\n")),
case fast_tls:recv(TSock, 0, 10000) of
{ok, _} ->
case fast_tls:get_peer_certificate(TSock) of
{ok, Cert} ->
FT = case fast_tls:get_cert_verify_string(fast_tls:get_verify_result(TSock), Cert) of
<<"ok">> -> "vulnerable";
_ -> "ok"
end,
HT = case list_to_integer(erlang:system_info(otp_release)) >= 26 of
true ->
case httpc:request("https://" ++ Host) of
{ok, _} -> "vulnerable";
_ -> "ok"
end;
false ->
"not verified (OTP < 26)"
end,
"fast_tls status: " ++ FT ++ ", ssl status: " ++ HT;
E4 -> Fail(E4)
end;
E3 -> Fail(E3)
end;
E2 -> Fail(E2)
end;
E1 -> Fail(E1)
end
end)().

If you are discovering Fluux, it is an open-source XMPP client for teams, communities, and anyone who wants control over their communications. This release keeps the focus on making those conversations easier to follow.
Long conversations open more smoothly, including those with large blocks of text. Catching up on archived history is faster when thousands of messages are already loaded, and busy rooms no longer stall or crash during bursts of messages and presence updates.
History is easier to navigate, too. When a conversation contains several gaps, Fluux can show and fill each one separately. Search&aposs “Go to message” now reaches and highlights results far back in both direct chats and rooms.
The view stays steadier while you read. Reopening a room with short multi-line messages no longer makes it jump. The last message stays visible above the typing indicator and composer, and the scroll-to-bottom button appears as soon as you move away from the latest messages.
Unread badges now recover correctly after a reconnect and catch-up. A room read through to its latest message clears its badge. A read position from another device that Fluux cannot yet place no longer blocks unread counts.
The “New messages” divider now counts the messages actually below it, rather than repeating the unread total on the scroll-to-bottom badge. Joined room avatars show unread message counts, while mentions keep their own badge.
Messages from ignored room members no longer add to unread or mention badges. Messages you send from another device under a different room nickname are recognized as yours, without an extra notification or unread count.

On macOS, you can share links, documents, and images to Fluux from another app&aposs system Share menu. Fluux opens so you can choose the conversation and send the content.
Whispers to a room member can now include attachments. Before you send, Fluux explains that the file is stored on the server and that the recipient keeps a copy. When the server provides a retention period, Fluux shows that too.
Desktop file handling also gets a practical fix: attaching or opening files with non-Latin names works again.
Version 0.18.0 introduces experimental native iOS support, with push notifications, system sharing, native file transfers, and OpenPGP encryption.
Photos shared from another app open in Fluux ready to send. Large photos are reduced before sharing, with the option to send the original when it fits the server&aposs limit.
Notifications can show contact and room avatars, and OpenPGP message previews can be decrypted on the device. Those previews start enabled for accounts with OpenPGP enabled and a usable key; you can turn them off for each account in Notifications settings. The app switcher hides your conversations behind a blurred cover.
On iOS 16 or later, Siri and Shortcuts can open an existing conversation or room, or check the unread count shown on the app icon.
This support is experimental.
Subscribe to the ProcessOne newsletter to hear when TestFlight testing opens.
For wide windows, Accessibility settings now offer “Limit conversation width”, with a slider to choose a centered reading column. Small windows continue to use the full width. Presence indicators also use more distinct shapes: Away is an orange rounded square, and Do Not Disturb is a red ring.
OpenPGP conversations work better alongside Gajim, including encrypted edits, replies, receipts, and read markers. Deleted messages stay deleted after a restart and archive sync, rather than returning to the conversation or search.
On Linux desktops using XEmbed trays, closing the window keeps Fluux running in the tray. Reopening Fluux keeps your conversations on screen while it reconnects, instead of briefly showing the login form.
Download Fluux Messenger for macOS, Windows, and Linux, or visit the Fluux Messenger page to explore the app and try it in your browser.
See the full 0.18.0 release notes for the complete list of changes.
If something still feels off, tell us. Bug reports and feature requests both go to GitHub Issues.
“AI is not my area.” That was my answer when a friend suggested I give a talk about agentic workflows. I used that phrase a lot at the beginning of the year; I was pretty skeptical about the whole thing.
But the idea stuck. I started reading about what makes agentic systems hard to build, expecting to find something wild and exotic to dig into. What I found instead was a list of problems that sounded pretty familiar:
None of that was new to me. If you work with Elixir, Erlang, and the BEAM, it’s probably not new to you either.
A few months later, I was on stage at ElixirConf US giving the talk “Exploring OTP Patterns for Agentic Workflows”. This series is based on that talk, but it goes deeper into the topics I couldn’t fully cover on stage.
The goal is to help you better understand agentic systems from the perspective of a BEAM developer. Not because OTP is an AI framework, nor because AI concepts map one-to-one to elements of the BEAM ecosystem, but because many of the engineering challenges these systems present are problems the BEAM community has been solving for decades.
So the question is simple:
What do I already know that can help me understand agentic workflows?
In this chapter, we start with the first item on the list: state.
The example we’ll use throughout this chapter starts with a familiar situation: an endpoint that returns an error instead of the user’s data, and us leaning on an AI agent to debug and fix it.
GET /users/:id Hey Claude: The GET /users/:id endpoint is returning a 500 error for a valid ID. It should return the user object. Fix it.
Notice that the prompt describes what I want, not how to get there.
The agent takes this prompt as its goal. It has a codebase to investigate and a set of tools to do so. Let’s say it starts by inspecting the router and then the controller. What it does next depends on what it finds along the way.
One distinction matters for everything that follows. The model at the center of the agent, a large language model (LLM), doesn’t open files or interact directly with the system. It participates in deciding what should happen next, but it’s the tools that do the work, and their results become part of the context the model sees when it makes its next decision.
At a high level, we can represent it like this:

For this series, that’s our working definition of an agentic workflow: a workflow in which the model decides the path as it progresses, based on the context and on what it observes.
What interests us is everything behind that loop that makes it work as a system:
We already know how to reason about those problems, and that’s what this series explores.
After a few iterations, the agent has gathered information: the controller function behind the endpoint, the tests, the relevant spots in the codebase. If every iteration had to start from scratch, it would never make any progress.
But from the model’s perspective, each invocation starts with only the context it’s given. The model itself doesn’t carry anything over from one invocation to the next.
That means the system around it must preserve information about the execution: the goal, what has already happened, what work is still pending, how many steps it has taken, and so on.
In other words, the workflow has state.
Before we continue, let’s define another concept we’ve already mentioned and will need throughout this series: context.
When the model needs to decide what to do next, it needs information about what has happened so far. Maybe the failing test output, the relevant function, the original goal, and the result of the last tool call. The model receives that information as context.
The two are related, but they’re not the same thing. One way to see it:
The system has state. The model receives context derived from that state.
The state usually contains more than the model needs for a particular decision. Step counts and pending work may matter to the system without needing to be exposed to the model. And the model can only take in a limited amount of context per invocation (what’s known as its context window), so even the relevant information may need to be selected, summarized, or trimmed.
What the model sees depends on the decision we’re asking it to make. This brings us to the first interesting question of the chapter.
The answer is the essential building block of the BEAM world: a process.
A process owns its state. No other process can reach into its memory and change it; anyone who wants to interact with it has to send it a message. For a workflow, that suggests one process per execution: it starts when a goal comes in, holds the state for that run, and exits when the run is over.
But the process doesn’t just hold a value. It has to react to messages that arrive on their own schedule: a tool result, a timeout, a request to cancel.
A GenServer gives us exactly that. But be careful: the GenServer isn’t the AI agent. It’s just the process we’ll use to own and coordinate one execution of the workflow.
A naming note: agent in this series always means an AI agent, never Elixir’s Agent module. That module wouldn’t be enough here anyway: it’s a simple abstraction around state, built to hold a value, not to react to messages as they arrive.
Before writing the process, let’s look at what it has to hold. Go back to our broken endpoint. The agent has already read the router, and the model’s next decision was to do two things at once: read the controller and run the tests. Neither has come back yet. At that moment, the state of the run looks like this:
%BlogOtp.Workflow{ goal: "The GET /users/:id endpoint is returning a 500 error...", model: BlogOtp.Model.Scripted, history: [ {{:read_file, %{path: "lib/my_app_web/router.ex"}}, ~s(get "/users/:id", UserController, :show)} ], pending: %{ #Reference<...> => {:read_file, %{path: "lib/my_app_web/controllers/user_controller.ex"}}, #Reference<...> => {:run_tests, %{path: "test/my_app_web/controllers/user_controller_test.exs"}} }, steps: 3, max_steps: 10 }
Each field maps to something we’ve already talked about:
goal is the prompt we gave the agent.model is the module that makes the decisions. In this chapter it’s a scripted stand-in for an LLM, so it doesn’t need anything extra to run.history is what has happened so far: each tool call paired with its result.pending is the work requested but not answered yet. Each request gets a unique reference, so when a result arrives, we know which one it answers.steps counts the tool calls requested so far, and max_steps lets the system stop a run that goes on too long.Only goal and history end up in the context. The rest exists for the system; the model never sees it.
Here’s the process that owns that state:
defmodule BlogOtp.Workflow do use GenServer, restart: :transient require Logger defstruct [:goal, :model, history: [], pending: %{}, steps: 0, max_steps: 10] def start_link(opts), do: GenServer.start_link(__MODULE__, opts) @impl true def init(opts) do state = %__MODULE__{ goal: Keyword.fetch!(opts, :goal), model: Keyword.get(opts, :model, BlogOtp.Model.Scripted), max_steps: Keyword.get(opts, :max_steps, 10) } {:ok, state, {:continue, :next_step}} end @impl true def handle_continue(:next_step, state), do: next_step(state) @impl true def handle_info({:tool_result, ref, result}, state) when is_map_key(state.pending, ref) do {call, pending} = Map.pop(state.pending, ref) state = %{ state | pending: pending, history: [{call, result} | state.history] } next_step(state) end # A result we aren't waiting for: a duplicate, or a late arrival def handle_info({:tool_result, _ref, _result}, state), do: {:noreply, state} # Still waiting on results: keep waiting defp next_step(%{pending: pending} = state) when map_size(pending) > 0, do: {:noreply, state} # The system decides when to stop, not only the model defp next_step(%{steps: steps, max_steps: max} = state) when steps >= max, do: finish({:stopped, :max_steps}, state) defp next_step(state) do # Chapter 1 shortcut: the owner calls the model itself case state.model.decide(build_context(state)) do {:tools, calls} -> {:noreply, request(calls, state)} {:done, answer} -> finish({:done, answer}, state) end end # Nobody executes these yet: each call waits in `pending` for its result defp request(calls, state) do Enum.reduce(calls, state, fn call, state -> %{state | pending: Map.put(state.pending, make_ref(), call), steps: state.steps + 1} end) end defp finish(outcome, state) do Logger.info("Workflow finished: #{inspect(outcome)}") {:stop, :normal, state} end # What the model gets to see, in its simplest form def build_context(state) do [{:goal, state.goal} | Enum.reverse(state.history)] end end
The loop lives in next_step/1, which works like this:
pending, or a final answer, which ends the process normally.And restart: :transient tells a supervisor to restart the process only if it terminates abnormally: a run that finishes with :normal stays finished.
Two things are deliberately missing in this chapter. First, nobody executes the tools yet: requests sit in pending until someone sends back a {:tool_result, ref, result} message. Second, the owner calls the model directly, even though a model call is slow. Both are problems we’ll come back to.
The code also shows the state/context distinction from the previous section. pending, steps and max_steps exist for the system; the model never sees them. And the context is the output of build_context/1, a plain function over the state, so we can test what the model will see without calling a model.
The code also gives us serialized access to the state.
Look at pending in the example above. Suppose the controller’s contents and the test output come back at almost the same time. They arrive as messages, and the process handles them one at a time. Each handle_info/2 runs to completion against a consistent state before the next message is processed. There are no locks; nothing else can change the state in the meantime, and the context for the next model call is never built from a half-applied update.
The first result is simply recorded: pending still holds the other request, so next_step/1 keeps waiting. Only when the second result arrives does the model get to decide, with both observations in its context.
But that same property has a cost.
A GenServer handles one message at a time, and the work in this loop is slow: a model call can take seconds, while a test suite can take much longer. If the process did that work itself, it couldn’t handle anything in the meantime. Messages would pile up in its mailbox: a result, a timeout, a request to cancel, all waiting their turn.
And that’s exactly what our chapter 1 shortcut does with the model call. It works here because our scripted model answers instantly; a real one wouldn’t.
So owning the state and doing the work pull in opposite directions.
The owner process needs to stay available, so the work has to happen somewhere else. That’s why nobody executes the tools yet: the owner shouldn’t be the one doing it. We’ll come back to that.
First, there’s a more urgent question: everything above lives in memory.
“Let it crash” works because a supervisor can start a new process in a known good state. But for a GenServer, that known good state is whatever init/1 builds. For a workflow, that means starting from the beginning, which is exactly what we don’t want.
Think about our endpoint. By the time the process crashes, the model may have already made several decisions: it read the router, read the controller, ran the tests, maybe even edited the controller. Starting over means repeating all of that, paying for it again in time and in model calls. And some of that work, like editing a file, may not be something we want to do twice.
The process can own the workflow state while it’s running, but that doesn’t mean process memory has to be the only place where that state lives.
We need enough durable information to reconstruct the workflow.
One way to do that is to record its progress as an append-only log. The goal is the first entry. Each tool call the model decides on becomes another, as does each tool result. As the workflow progresses, we only ever add these entries to the end.
For our endpoint, the log might look like this when the process crashes:
{:goal, "The GET /users/:id endpoint is returning a 500 error..."}
{:decision, 1, {:read_file, %{path: "lib/my_app_web/router.ex"}}}
{:result, 1, ~s(get "/users/:id", UserController, :show)}
{:decision, 2, {:read_file, %{path: "lib/my_app_web/controllers/user_controller.ex"}}}
{:decision, 3, {:run_tests, %{path: "test/my_app_web/controllers/user_controller_test.exs"}}}
{:result, 2, ~s[def show(conn, %{"id" => id}) do ... json(conn, user) ...]}
Everything the process knew is there: the goal, every decision, every result that came back. Even the request that’s still waiting for its answer.
If that log becomes the source from which we reconstruct the workflow state, we’re using the same core idea as event sourcing.
Let’s put that idea into code. Three things change: the workflow gets an id that survives the process, each tool call gets a step id the workflow assigns, and every state change goes through a single function.
defstruct [:id, :goal, :model, history: [], pending: %{}, steps: 0, max_steps: 10] # Only updates the state: no work, no model calls, no messages defp apply_step({:goal, goal}, state), do: %{state | goal: goal} defp apply_step({:decision, step_id, call}, state) do %{state | pending: Map.put(state.pending, step_id, call), steps: state.steps + 1} end defp apply_step({:result, step_id, result}, state) do {call, pending} = Map.pop(state.pending, step_id) %{state | pending: pending, history: [{call, result} | state.history]} end
Each clause handles one kind of entry from the log. apply_step/2 never starts work, calls a model, or sends a message. That restriction will matter in a moment.
The owner is still the only one that writes:
@impl true def handle_info({:tool_result, step_id, result}, state) when is_map_key(state.pending, step_id) do state = record({:result, step_id, result}, state) next_step(state) end # Each call becomes a decision in the log, with a step id the workflow assigns defp request(calls, state) do Enum.reduce(calls, state, fn call, state -> record({:decision, state.steps + 1, call}, state) end) end # Persist first, then apply defp record(entry, state) do :ok = BlogOtp.Log.append(state.id, entry) apply_step(entry, state) end
Notice two things.
The first is step_id, which replaces the reference from the previous version. A reference created with make_ref/0 only identifies something within the running node, and the process waiting for it may be gone after a crash. A step id is just a number the workflow assigns when it records the decision, and it means the same thing to any process that reads the log.
The second is the order inside record/2. We write to the log first, then apply the entry to the in-memory state. If the process dies between those two lines, the log is ahead of memory, and memory is the part we know how to rebuild. The other way around, the process could act on a step that no restart would ever see. And if the append fails, the process crashes before acting on anything.
The same rule applies to decisions: they’re recorded before anyone starts the work. In this chapter nobody executes the tools yet, but when someone does, the decision will already be in the log.
This puts a durable write on the owner’s critical path. That’s intentional: we don’t want the workflow to act on a transition it couldn’t recover after a crash.
In this chapter, BlogOtp.Log keeps its entries in an ETS table owned by its own process. It survives a workflow crash, but not a node restart. A real implementation would write to disk or a database; the contract, append first and read in order, stays the same.
Starting a run now means creating its log first:
def start(goal, opts \\ []) do id = new_id() :ok = BlogOtp.Log.append(id, {:goal, goal}) DynamicSupervisor.start_child( BlogOtp.WorkflowSupervisor, {__MODULE__, Keyword.put(opts, :id, id)} ) end def start_link(opts) do GenServer.start_link(__MODULE__, opts, name: via(Keyword.fetch!(opts, :id))) end defp via(id), do: {:via, Registry, {BlogOtp.WorkflowRegistry, id}}
The goal is written before the owner exists; from then on, the owner is the only writer. Registering the process under the workflow id in a Registry with unique keys turns “only one writer” from an intention into a guarantee: a key points to at most one process, and a second attempt to start an owner for the same run would get {:error, {:already_started, pid}}.
Look at what the process is started with: the id, not the goal. When a supervisor restarts a child, it calls the same start function, with the same arguments, from the child’s specification. The id is all a new process needs to find out everything else.
@impl true def init(opts) do state = %__MODULE__{ id: Keyword.fetch!(opts, :id), model: Keyword.get(opts, :model, BlogOtp.Model.Scripted), max_steps: Keyword.get(opts, :max_steps, 10) } {:ok, state, {:continue, :rebuild}} end @impl true def handle_continue(:rebuild, state) do state = state.id |> BlogOtp.Log.read() |> Enum.reduce(state, &apply_step/2) next_step(state) end
When init/1 returns {:continue, :rebuild}, OTP runs handle_continue/2 before the process receives any other message, so no result can be handled while the state is still being rebuilt.
The same apply_step/2 that updates the workflow while it runs also rebuilds it after a crash. Now we can see why it had to be free of side effects: replaying the log must not run the tests again. And a new run isn’t a special case either: it’s a log with a single entry, the goal. The fold produces a state with nothing pending, and next_step/1 asks the model for its first decision.
The state is a fold over the log.
This changes what a restart means. The new process doesn’t need to remember what the old one knew. It only needs to rebuild enough of the workflow to continue from a known point. But careful!
Fold the log from our crash, and pending holds exactly one entry: step 3.
Run the tests
There’s no result for it. So next_step/1 sees something pending and waits, for a result that may never come.
We know the decision was recorded. But did the work actually run? Maybe the request was never sent. Maybe the tests were still running when the process crashed. Maybe they finished, and the result was lost before it could be recorded.
For a test suite, running it again is harmless. But imagine our agent’s last step had been:
Open a pull request
Now repeating it means a duplicate pull request. And some actions are even more delicate than that, like inserting a row into a database that must never exist twice, or charging a customer. Those are the ones where we have to be really careful.
Restarting the process gives us back the ability to continue the workflow. It doesn’t tell us whether to run an external action again.
Restart and retry are two different problems.
That leaves two questions open: where the work actually happens while the owner stays available, and what to do with a step whose outcome we don’t know. The first question is where the next chapter picks up. The second one will have to wait until we talk about failure recovery.
The complete code repository for this series will be shared in Chapter 2.
Want to learn more about agentic workflows and OTP? Get in touch with our ESL team.
The post Exploring OTP Patterns for Agentic Workflows appeared first on Erlang Solutions.
It’s that time of the year when the annual Biketober and Coffeeneuring challenges are on! This year I’m not planning on any serious miles or any particular goals for Biketober, but as always I plan on completing the Coffeeneuring challenge, and will document it here as well as on the fediverse at social.coop.
As always, I’ll start my week on Monday (per Rule 4, “As long as you define it as seven consecutive days, you can start your week any day.”), so “Week One” will be a short one since the challenge started with “Early Bird Friday” (Rule 2).
Since last year I’ve started a Smyrna based group ride that aims to be a short ride (30-45 minutes, normally 5-7 miles depending on who shows up with longer rides if we’re all experienced riders) aimed at getting new riders on bikes, so for the first week I decided to pair up my Coffeeneuring ride and the group ride and went to Cafe Lucia. Unfortunately, I think I’ll haver to stop going to this location. It’s been one of my favorite coffee spots for a while, but lately it’s taken to only playing christian praise music, which I find somewhat offensive both as music and in the sense that a coffee shop should be more welcoming and not force its owner or employees beliefs on everyone else. Believe what you want, but in a public community space there’s no reason to assume everyone else will want to hear about it.
The ride itself, however, was great. Between it being a rainy day, one of the regulars being sick, and several of the others being at the Atlanta Pride Parade, we only had three folks for the day, none of whom were new so we decided to do a slightly longer ride (~9.1 miles), ending at a local beer establishment—though I did not count the beer place per rule 4, “Only one coffee location per day counts toward official completion”.
2026-10-11 Smyrna Group Ride
A GPX track for the ride can be downloaded from the Internet Archive.
I took advantage of our weekly Smyrna Group Ride to try and get a few of our other regulars Coffeneuring, so today we stopped at Rev Coffee mid-route. Their beans are a bit expensive for what you get, but the coffee is pretty solid. It was just chilly enough to need a jacket when we started, and it was windy enough to slow us down in a few places, so the coffee was a welcome break.
2026-10-18 Smyrna Group Ride
A GPX track is available from theInternet Archive.
Security teams are good at finding problems. The difficult part is finding the right problems across a growing codebase, a changing application and the connections between them. A scanner can flag a known pattern quickly. A skilled tester can follow a complicated line of enquiry. Neither has unlimited time, and there will always be a question nobody thought to ask.
Could AI agents take on more of that investigative work? I think the answer is yes, provided we are precise about what “find” means. Agents can produce leads, test assumptions and uncover issues that established checks have missed. A security vulnerability, though, has to survive examination in the context of the real system.
That distinction matters more as agents become available to both defenders and attackers. A familiar weakness can become more relevant when agents have the time to find it, test it and follow where it leads. Defenders have a good reason to look again at parts of their systems that were too laborious to examine thoroughly.
Think about the code paths that work perfectly well in normal use but behave differently with a surprising input or an unusual amount of data. Fuzzing (testing software with unexpected or malformed inputs) helps explore these cases; agents can then read the surrounding code, try a more specific hypothesis and keep working through leads that would consume too much of an engineer’s day if checked one by one.
Some issues only become apparent when you understand how the application is used. A finding may look harmless in isolation until you establish who controls the input, which permissions are involved or what the application is meant to prevent. Agents can explore a sequence of actions and check how the application responds at each step, which may reveal a problem that would be hard to spot from an isolated code warning.
There is also the possibility that one weakness opens the way to another. A low-priority issue could expose useful information; a forgotten endpoint might give access to a service assumed to be isolated. Agents can follow a discovery into the next step, although its account of that path still needs to be checked. The full route through a system may matter more than the severity of any one weakness in it.
I would be careful about grouping all of this under one claim that “AI finds what traditional testing misses”. Code analysis tools, fuzzing tools, formal verification and agents conducting a permitted penetration test are doing different jobs. Their findings have different levels of evidence. Formal verification is also starting to gain traction, with code agents proving useful for generating the specifications it requires. The useful question is where agents give the team another line of enquiry and how that enquiry will be validated.
We have seen one version of this in our own work. Lukas Backström, a senior developer at Erlang Solutions and a member of the Ericsson Erlang/OTP core development team, built a custom workflow using Claude Code to examine the Erlang/OTP repository for security vulnerabilities, bugs and performance issues.
This codebase already receives human review, regular automated functional and performance testing, and vulnerability scanning. Even so, agents surfaced cases where expensive encoding or decoding could be performed on untrusted data that was very large or unbounded, creating a possible denial-of-service risk. The operation itself was not mysterious. Finding the relevant combination of input and behaviour across a large repository was the challenge.
Our published case study reports approximately 20 previously unknown, potentially severe security vulnerabilities, alongside around 400 critical or high findings and roughly 4,000 findings overall, most of them low-severity bugs. These are reported findings from the investigation, not a claim that thousands of confirmed security vulnerabilities were waiting to be fixed. And, this is one of the world’s most thoroughly tested code bases.
The project gives us evidence for a particular point: agents helped investigate parts of an extensively tested codebase that would have been difficult to cover manually. It does not prove that agents will outperform every scanner or security team. The details of the task and the review process made the result useful.
Lukas gave agents a verification mode, including access to a Docker container to check findings and rank their severity and confidence. He also found that a fresh context and a focused second look at an issue helped expose false positives. That separate work took substantial compute, and the agents still sometimes returned a high-confidence finding that was wrong. It could also mistake a bug for a security issue because it misunderstood the threat model.
An engineer has to ask whether an attacker can reach the relevant code, what input they control, what limits exist elsewhere and what happens in a real deployment. A persuasive explanation from an agent is a reason to investigate, not evidence that every assumption is true.
There is a second challenge once agents start finding more faults. Most teams do not need a larger pile of unverified tickets. They need to know which findings are real and which ones deserve attention first. Every result takes time to assess, and asking agents to assess it again can add to the compute cost. Finding more vulnerabilities only helps if the team has enough context to act on what matters.
The investigative agents must also be secure in its own right. It may read code, retrieve documents and run tools, so its permissions and execution environment deserve scrutiny. Untrusted material could contain instructions intended to redirect its behaviour. The team needs to decide what the agents can access and do before giving it sensitive systems to examine.
I would treat an agentic investigation as an experiment with a clear question, rather than asking agents to “find everything” and judging success by the length of its report:
So, can agentic AI find security vulnerabilities other tests miss? Yes, it can uncover leads that existing checks have not surfaced, particularly when an issue takes sustained investigation to find. Our Erlang/OTP work showed that in a codebase with established reviews, automated tests and vulnerability scanning. It was not a test-by-test comparison, so we cannot say which individual check missed each issue.
Finding a lead is only part of the answer. An experienced engineer still needs to establish whether it is a genuine security vulnerability and what should be fixed. The opportunity is to investigate more of the questions a team has never had the time to pursue, then give those findings the scrutiny they deserve.
The post Can Agentic AI Find the Security Vulnerabilities Other Tests Miss? appeared first on Erlang Solutions.

XMPP Newsletter Banner
Welcome to the XMPP Newsletter, great to have you here again! This issue covers the month of September 2026.
The XMPP Newsletter is brought to you by the XSF Communication Team and contributors of the XMPP community.
Just like any other product or project by the XSF, the Newsletter is the result of the voluntary work of its members and contributors. If you are happy with the services and software you may be using, please consider saying thanks or help these projects!
Interested in contributing to the XSF Communication Team? Read more at the bottom.
See also the XMPP Events page.
Being an elected member of the XMPP Standards Foundation signals a commitment to open standards and professional engagement in / with the XMPP community. Here, your membership helps position the XSF as a healthy organization, which in itself is valuable. It also grants voting rights on technical and administrative matters within the XSF. The application is a light-weight and free of cost process and you can use your membership to get more involved more easily, too. If you are interested in joining the XMPP Standards Foundation as a member, please apply to our 4th quarterly call for members admission before November 29, 2026, 00:00 UTC.
The XMPP Standards Foundation is calling for individuals to compose the Board of Directors and members to serve on the XMPP Council for the 2026/2027 period. If you are interested in running for the Board of Directors or the XMPP Council, please add a wiki page about your candidacy to one or both of the aforementioned sections before November 8, 2026, 00:00 UTC. Get involved in the XMPP Standards Foundation organization decisions and with the specifications the XSF publish. Note: XMPP Council members must be elected members of the XSF; however, there is no such restriction for the Board of Directors.
The XMPP Standards Foundation develops extensions to XMPP in its XEP series in addition to XMPP RFCs. Developers and other standards experts from around the world collaborate on these extensions, developing new specifications for emerging practices, and refining existing ways of doing things. Proposed by anybody, the particularly successful ones end up as Final or Active - depending on their type - while others are carefully archived as Deferred. This life cycle is described in XEP-0001, which contains the formal and canonical definitions for the types, states, and processes. Read more about the standards process. Communication around Standards and Extensions happens in the Standards Mailing List (online archive).
The XEP development process starts by writing up an idea and submitting it to the XMPP Editor. Within two weeks, the Council decides whether to accept this proposal as an Experimental XEP.
If an experimental XEP is not updated for more than twelve months, it will be moved off Experimental to Deferred. If there is another update, it will put the XEP back onto Experimental.
Last calls are issued once everyone seems satisfied with the current XEP status. After the Council decides whether the XEP seems ready, the XMPP Editor issues a Last Call for comments. The feedback gathered during the Last Call can help improve the XEP before returning it to the Council for advancement to Stable.
Please share the news on other networks:
Also check out our RSS Feed!
Looking for job offers or want to hire a professional consultant for your XMPP project? Visit our XMPP job board.
This is a community effort, and we would like to thank translators for their contributions. Volunteers and more languages are welcome! Translations of the XMPP Newsletter will be released here (with some delay):
Contributors:
Translations:
This XMPP Newsletter is produced collaboratively by the XMPP community. Each month’s newsletter issue is drafted in this simple pad. At the end of each month, the pad’s content is merged into the XSF GitHub repository. We are always happy to welcome contributors. Do not hesitate to join the discussion in our XSF Communications Team group chat (MUC) and thereby help us sustain this as a community effort. You have a project and want to spread the news? Please consider sharing your news or events here, and promote it to a large audience.
Tasks we do on a regular basis:
For this newsletter either log in here and unsubscribe or simply send an email to newsletter-leave@xmpp.org. (If you have not previously logged in, you may need to set up an account with the appropriate email address.)
This newsletter is published under CC BY-SA license.

Contents:
This release contains fixes for those security issues:
There was a reference-ordering problem in the fast_xml generator, it generated XML encoders that could reorder child elements in a different order from the one declared in the codec specification. From now, the order is the one declared in the codec specification. See details in https://github.com/processone/fast_xml/pull/53
Additionally there was an incorrect sasl2_continue declaration order in the xmpp erlang library: it declared "additional-data, text, tasks". Now it follows the ordering defined in XEP-0388 schema: "additional-data, tasks, text". See details in https://github.com/processone/xmpp/pull/112
New toplevel option captcha_pow adds a SHA-256 hashcash challenge as described in XEP-0158 in the CAPTCHA form, alongside the image challenge or on its own.
Unlike the image challenge, this does not require setting the option captcha_cmd.
This option is used only by mod_register when registering a new account using In-Band Registration, not in MUC rooms or in mod_register_web. It is disabled by default.
It is well known how to grant administrative privileges to an account: by adding that account to an acl called admin:
acl:
admin:
user: admin1@localhost
The default ejabberd configuration uses this admin ACL in many places:
announce access rule used by mod_announceconfigure access rule used by mod_configure and WebAdminapi_permissions entries used to execute API commands in WebAdmin, mod_adhoc_api, mod_http_api, ...shaper_rulesaccess_admin option in mod_mucConsequently, that admin account can administer all of ejabberd: all the global features, all the modules, in all the vhosts... For now let&aposs call it a "global admin".
If you have several vhosts, you can allow specific accounts to administer only specific vhosts. Let&aposs call them "vhost-admins". In this example admin1@localhost can execute commands on all vhosts. Additionally, localhost has a vhost-admin, second has two vhosts-admins, and third has a vhost-admin:
hosts:
- localhost
- second
- third
acl:
admin:
user: admin1@localhost
append_host_config:
localhost:
acl:
aclhostadmin:
- user: hostadmin@localhost
second:
acl:
aclhostadmin:
- user: hostadmin@second
- user: hostadmin@third
third:
acl:
aclhostadmin:
- user: hostadmin@second
api_permissions:
"vhost http access":
from: mod_http_api
who:
access:
allow:
- acl: admin
allow:
- acl: aclhostadmin
what: "*"
Example call of a vhost command by a vhost-admin:
$ curl --basic --user hostadmin@third:somepass -k \
&aposhttps://localhost:5443/api/status_num_host?host=second&status=dnd&apos
7
If a vhost-admin tries to execute an API command directed to a vhost he does not administer, or a global command (that has no host argument, and affects all ejabberd), they are rejected:
$ curl --basic --user hostadmin@third:somepass -k \
&aposhttps://localhost:5443/api/status_num_host?host=third&status=dnd&apos
{"code":32,
"message":"AccessRules: Account does not have the right to perform the operation.",
"status":"error"}
$ curl --basic --user hostadmin@third:somepass -k \
&aposhttps://localhost:5443/api/stats?name=registeredusers&apos
{"code":32,
"message":"AccessRules: Account does not have the right to perform the operation.",
"status":"error"}
force value to auth_external_user_exists_check option*_to_atomejabberd_cluster:*call operate only on known nodesacl:load_tab()ejabberd_systemd: Prefer matching over length/1mod_auth_fast: Make sure that fast tokens can be used only with method that they were created formod_invites: don&apost apply overuse limit if max_invites is infinity (#4615)mod_invites: don&apost crash in get_invite_by_invitee_t if reset_token present (#4620)mod_invites: now that Conversations is for free we remove Yaxim (#4621)mod_mix: Make access_create rule be applied when creating channelmod_mqtt: Add lower limits for pre-auth packetsmod_muc_room: Fix handling of hats request with missing xdatamod_muc_rtbl: Accept also plain account and domain JIDsmod_muc_rtbl: Fix handling of remote ban servers (#4622)mod_register: After changing password disallow password change on currently authenticated sessionsdb_serialize to mod_privacy and mod_pubsubrename_column op to ejabbrd_sql_schema update routinesrename_column compatible with older mysql versionsejabberd_sql_schema: Escape all column/table namesmod_roster serializer with info about approved fieldmake-binaries: Bump Elixir to 1.19.6make-binaries: Bump Erlang/OTP version to 28.5.0.7make-binaries: Bump Expat version to 2.8.5make-binaries: Bump JPEG version to 10make-binaries: Bump OpenSSL 3.6.4make-binaries: Bump PNG version to 1.6.58make-binaries: Bump SQLite version to 3530400make-binaries: Bump WebP version to 1.6.0Dockerfile: Workaround to get image with amd64 (#4598)https://github.com/processone/ejabberd/compare/26.07...26.09
We would like to thank for the security reports provided by:
the contributions to the source code by:
fast_xml and xmpp XML child orderingand the translation by:
And also to all the people contributing in the ejabberd chatroom, issue tracker...
Customers of the ejabberd Business Edition, in addition to all those bugfixes, also get the following changes:
mod_applepush service (to change sandbox connection endpoint)When using multihost schema:
ALTER TABLE push_gate CHANGE COLUMN user username text NOT NULL;
CREATE INDEX i_push_gate_username_server_host USING BTREE ON `push_gate`(username(191), server_host(191));
Otherwise:
ALTER TABLE push_gate RENAME COLUMN user TO username;
CREATE INDEX i_push_gate_username USING BTREE ON `push_gate`(username(191));
When using multihost schema:
ALTER TABLE push_gate RENAME COLUMN "user" TO "username";
CREATE INDEX i_push_gate_token_server_host ON "push_gate" USING btree ("username", "server_host");
Otherwise:
ALTER TABLE push_gate RENAME COLUMN "user" TO "username";
CREATE INDEX i_push_gate_token ON "push_gate" USING btree ("username");
As usual, the release is tagged in the Git source code repository on GitHub.
The source package and installers are available in ejabberd Downloads page. To check the *.asc signature files, see How to verify ProcessOne downloads integrity.
For convenience, there are alternative download locations like the ejabberd DEB/RPM Packages Repository and the GitHub Release / Tags.
The ecs container image is available in docker.io/ejabberd/ecs and ghcr.io/processone/ecs. The alternative ejabberd container image is available in ghcr.io/processone/ejabberd.
If you consider that you&aposve found a bug, please search or fill a bug report on GitHub Issues.
No sample library — just equations, PCM, and a small OS-specific playback bridge.
Early game consoles had little memory or storage to spare for recorded sound. Their music relied on a small number of voices and a limited vocabulary: simple waveforms, noise, and envelopes. Composers turned those restrictions into instantly recognizable melodies.
Chiptune grew out of these constraints. The term covers a broad family of music and music-making practices rooted in programmable sound chips.
It is often called “8-bit music,” although the label is imprecise: not every chiptune machine was 8-bit, and not every modern track with a retro timbre runs on vintage hardware. The important idea for us is the working method. Instead of asking an audio player to reproduce a recorded instrument, we describe a small signal generator and tell it which frequencies to produce over time. The hardware limitations helped shape the art; today we can choose similar limitations because they are fun.
This project builds a chiptune-inspired synthesizer directly in Erlang. It is not an emulator for a particular console or sound chip. It borrows the useful ingredients—elementary waveforms, noise, short envelopes, and a small set of instruments—and renders them as ordinary digital audio. A bass note is not a WAV file. A kick drum is not a hidden sample. Every instrument is a mathematical function evaluated while the song plays.
The project was inspired in part by the 2020 Erlang Solutions article “The sound of Erlang: How to use Erlang as an instrument”. That article develops sound from first principles, writes raw audio to a file, and plays it with ffplay. We will keep the first-principles spirit but take a different route: multiple simultaneous tracks, several synthesized instruments, chunked rendering, and live playback through the operating system’s audio API.
By the end, you will know how a value such as a4 becomes 440 Hz, how Erlang turns that frequency into signed 16-bit samples, and how those samples reach the speakers.
First, hear the small hand-written demo we will use for the walkthrough. This clip records the synthesizer’s output:
You need Erlang/OTP on PATH, including erl.exe and escript.exe. The playback bridge on Windows uses the powershell.exe and built-in C# compiler available on modern Windows. No FFmpeg, external synthesizer, or audio library is required. Open PowerShell in the repository root and play the small hand-written demo:
.\run-windows.bat demo_song
On macOS, Erlang’s erl and escript commands must be on PATH. The launcher also needs the Apple Command Line Tools to compile the bundled Core Audio bridge. If they are missing, install them with xcode-select --install. Run the equivalent command in Terminal:
./run-macos.sh demo_song
Both launchers compile the application, start a non-interactive Erlang VM, synthesize the song from its Erlang data, and stream it to the default audio output device. They accept any compiled song module with the interface we will examine below.
Before isolating tracks, ask the module what it contains:
On Windows:
.\run-windows.bat demo_song tracks
On macOS:
./run-macos.sh demo_song tracks
The output will be:
[{1, kick}, {2, snare}, {3, closed_hat}, {4, bass}, {5, pad}, {6, lead}]
Now we can listen to the lead by itself or remove the percussion from the mix:
On Windows:
.\run-windows.bat demo_song solo 6 .\run-windows.bat demo_song mute 1,2,3
On macOS:
./run-macos.sh demo_song solo 6 ./run-macos.sh demo_song mute 1,2,3
These controls are intentionally small. They are enough to make the synthesizer explorable without turning a blog-sized project into a workstation.
The playback path is compact enough to follow from beginning to end:
| Location | Responsibility |
|---|---|
src/demo_song.erl | A readable score used for the walkthrough |
src/song_*.erl | Larger song modules generated from MIDI files |
src/music.erl | The small public API: play a song or list its tracks |
src/music_synth.erl | Scheduling, pitch, instruments, mixing, and PCM rendering |
src/music_player.erl | Selects the platform bridge and streams rendered chunks |
priv/wave_out.ps1 | PowerShell/C# bridge to the native Windows waveOut API |
priv/audio_out.c | C bridge to macOS Core Audio’s Audio Queue API |
import/import.py | Optional Standard MIDI File to Erlang module converter |
There are three useful boundaries here. Song modules describe what to play. music_synth calculates what the waveform is. The player and its platform bridge decide how bytes reach an audio device.
A song module exports just two functions. This excerpt shows the tempo and percussion tracks from demo_song:
-module(demo_song).
-export([bpm/0, tracks/0]).
bpm() -> 120.
tracks() ->
[{kick, lists:seq(0, 30, 2)},
{snare, lists:seq(1, 31, 2)},
{closed_hat, [B / 2 || B <- lists:seq(0, 63)]},
...].
“`
Each track is {Preset, Events}. A percussion event is simply a beat number. At 120 beats per minute, the kick events above land at beats 0, 2, 4, and so on, while the snare occupies the alternating beats. The closed hat list uses half-beats, which creates the faster pulse running across the demo.
Pitched instruments use {Beat, Duration, Notes}:
{bass, [{0, 0.45, c2}, {1, 0.45, c2}, {2, 0.45, g2}]}
{pad, [{0, 4, {c4, e4, g4}},
{4, 4, {a3, c4, e4}}]}
“`
Notes may be one note atom or a non-empty tuple. A tuple schedules its notes together, giving us a chord without a second data structure. Both Beat and Duration are measured in beats and can have whole or fractional values.
The generated song_01_slay_the_evil through song_18_infinite_darkness modules follow exactly the same contract. Their scores are much larger, but they are still ordinary data returned by functions.
The public music module keeps callers away from the internal details:
music:play(demo_song).
music:play(demo_song, {solo, 6}).
music:play(demo_song, {mute, [1, 2, 3]}).
music:tracks(demo_song).
“`
Track selection happens before synthesis. music_synth:prepare/2 either keeps all tracks, selects one numbered track, or removes the requested track numbers. It then expands the remaining events into voices: one for each sounding note or percussion hit. A chord creates several voices with the same start time.
music_player.erl is intentionally small. Its two public functions first ask the synthesizer to prepare the whole score, then pass the resulting state to open/1:
play(Song) ->
open(music_synth:prepare(Song)).
play(Song, Selection) ->
case music_synth:prepare(Song, Selection) of
{error, _} = Error -> Error;
Synth -> open(Synth)
end.
“`
Once prepared, open/1 selects the platform bridge and starts playback. The bridge requests chunks as its audio buffers become available. Sample indexes determine when notes belong in the score; the audio device paces those samples in real time. We will follow the synthesis steps first, then return to the bridge protocol.
The note atom a4 becomes MIDI note 69, which corresponds to 440 Hz. In twelve-tone equal temperament, moving one semitone multiplies a frequency by the twelfth root of two. Using A4 as the reference, any MIDI note number Midi can be converted with:
frequency = 440 × 2^((Midi - 69) / 12)
“`
The synthesizer parses atoms such as c4, fs4, and a4, converts the letter, optional s for sharp, and octave into a MIDI number, and applies that formula:
Midi = (Octave + 1) * 12 + Base + Sharp, 440.0 * math:pow(2.0, (Midi - 69) / 12).
“`
For example, c4 becomes MIDI note 60 and approximately 261.63 Hz.
To turn that frequency into audio, we need samples. Sound is changing air pressure; digital audio represents that change as a sequence of numbers. At 44,100 samples per second, sample index SampleI occurs at time T in seconds:
T = SampleI / 44100
“`
A sine wave of frequency F can be sampled with:
sin(2 × pi × F × T)
“`
The result moves between -1 and 1. Repeating the calculation at consecutive values of T produces the waveform. Higher frequencies complete more cycles per second and sound higher.

A note becomes audio data. Every point in the final waveform is calculated for its position in time.
The center of the project is the tone/6 family in music_synth.erl. Its clauses are our instruments. Each receives the time since the voice began, its gate duration, its frequency, and where useful the absolute sample index and a deterministic seed. The gate duration is the time the note is held before its release begins; both it and the elapsed time are measured in seconds here.
The melodic presets start with elementary periodic waveforms:
tone(bass, T, G, F, _, _) -> 0.32 * square(?PI2 * F * T) * envelope(T, G, 0.005, 0.04); tone(lead, T, G, F, _, _) -> 0.22 * saw(?PI2 * F * T) * envelope(T, G, 0.01, 0.12); tone(pad, T, G, F, _, _) -> 0.12 * triangle(?PI2 * F * T) * envelope(T, G, 0.18, 0.50).
“`
The ?PI2 macro represents 2 × pi. A square wave flips between two levels and gives the bass a hollow, buzzy sound. A sawtooth ramps and jumps, producing the brighter lead. A triangle wave changes more gently and suits the softer pad.
The piano, organ, and strings presets combine basic waveforms. “Piano” adds sine waves at the fundamental, twice the frequency, and three times the frequency, then applies exponential decay. “Organ” uses a related harmonic mixture without the same decay. Strings combine a saw and a slightly detuned triangle. These are impressions, not physical models of real instruments, and their simplicity is part of the chiptune character.
For percussion, the kick feeds a rapidly falling phase into sin and damps it with exp(-9 × T). The tom uses a gentler downward sweep. The snare adds a short 180 Hz body to deterministic pseudo-random noise; the closed hat is an even shorter burst of noise. The cymbal combines noise with a high square wave. A handful of arithmetic expressions becomes a recognizable drum kit.
One more function keeps notes from clicking abruptly at their edges:
envelope(T, _Gate, Attack, _Release) when T < Attack -> T / Attack; envelope(T, Gate, _Attack, _Release) when T < Gate -> 1.0; envelope(T, Gate, _Attack, Release) -> max(0.0, 1.0 - (T - Gate) / Release).
“`
This is a compact attack–sustain–release envelope. During attack, amplitude rises from zero. It remains at full level while the note is gated, then falls after release begins. Different attack and release values make the same oscillator feel percussive, plucked, or slow and pad-like.
Tempo determines when each voice begins. beat_sample/2 converts a beat position to an absolute sample index:
beat_sample(Beat, Bpm) -> round(Beat * 60 * ?RATE / Bpm).
“`
Here ?RATE is 44,100. At 120 BPM, one beat lasts half a second, or 22,050 samples; beat 4 begins at sample 88,200.
A pitched event records three positions: Start, the first sample of the note; Gate, the end of its written duration; and End, the end of its release tail. The renderer advances a sample cursor instead of calling timer:sleep/1 for each note. This keeps note positions fixed even if rendering pauses briefly, although playback still needs a steady supply of audio to avoid gaps.
Multiple tracks do not require separate Erlang processes. prepare_tracks/2 gathers all selected tracks into one list of voices. Notes and chords can overlap because voices whose sample ranges intersect are active together.
Preparation turns events into voice tuples containing the preset, start, gate, end, frequency, and seed. Percussion receives a frequency of zero because its formula does not need the pitch argument. Every preset also has a short tail, allowing its release or decay to continue after the gate closes.
The renderer does not allocate the whole song as one giant list. It works in chunks of 4,410 samples—one tenth of a second at 44.1 kHz:
Count = min(?CHUNK, Total - Pos),
Last = Pos + Count,
Active = [V || V = {_, Start, _, End, _, _} <- Voices,
Start < Last, End > Pos].
“`
Only voices overlapping the current chunk are considered. For each sample index, the renderer evaluates those voices and sums their amplitudes. The sum is scaled and clamped to the valid range from -1 to 1. Clamping prevents an overflowing mix from wrapping into a radically different value, although a heavily clipped mix can still sound distorted.
Finally, each floating-point sample becomes a signed 16-bit little-endian integer:
pcm(X) -> <<(round(X * 32767)):16/little-signed>>.
“`
The resulting binary is mono linear PCM: 44,100 samples per second, two bytes per sample, 88,200 bytes per second. It contains no file header because we are sending it to a device configured with the matching format, not saving a WAV file.
Erlang can calculate the audio portably, but it still needs an operating-system API to make speakers move. music_player.erl chooses a deliberately small adapter: wave_out.ps1 on Windows or audio_out.c on macOS.

The synthesizer is platform-neutral; this diagram shows the Windows branch of the final bridge.
The player first calls music_synth:prepare/1 or prepare/2. It then checks the operating system and starts a TCP listener bound to 127.0.0.1 on a temporary port. Binding to loopback keeps this private protocol on the local machine.
Next, open_port/2 launches the selected bridge with the temporary port number. Here an Erlang port manages the external process; the PCM itself travels over the loopback TCP socket. The socket uses Erlang’s {packet, 4} mode, so each message receives a four-byte length prefix and arrives as one binary frame.
The PowerShell script uses Add-Type to compile a small embedded C# class. That class connects back to Erlang and opens the default waveform-audio output device through waveOutOpen. Its declared format matches the renderer: PCM, one channel, 44,100 Hz, 16 bits.
On macOS, run-macos.sh compiles the bundled C bridge with Apple Clang into the ignored _build directory. The bridge connects back to Erlang and creates an output queue with Core Audio’s Audio Queue Services. It declares the same mono, 44,100 Hz, signed 16-bit PCM format, so the Erlang renderer sends identical binaries on both systems.

The macOS bridge uses the same PCM format and request protocol as the Windows bridge.
Four native buffers act as playback slots. For every free slot, the bridge sends an R (“ready”) frame. Erlang responds with A followed by one PCM chunk. When the native API makes a buffer reusable, the bridge requests another. This credit-based exchange prevents Erlang from sending audio faster than the fixed buffer pool can accept it.
When the renderer reaches the end, Erlang sends E. The bridge lets all active buffers drain, replies with D, and closes. An X frame carries a bridge error back to Erlang. The protocol is tiny, but it gives the two runtimes explicit flow control and a clean ending. The macOS bridge asks Audio Queue Services to stop non-immediately and waits for its running-state notification before sending D, so buffered audio is not cut off.
The macOS launcher rebuilds the native bridge when its C source changes. The platform boundary remains music_synth:render/1, which returns either {PCM, NextState} or done. A future Linux adapter could consume those same chunks, handle buffering and errors, and drain at the end without changing the score or synthesizer.
Writing demo_song.erl by hand is useful for learning, but a complete arrangement can contain thousands of note events. import/import.py converts Standard MIDI files into modules with the same bpm/0 and tracks/0 interface.
The larger song_*.erl modules were generated from MIDI files in HydroGene’s free chiptune collection on itch.io, which includes rendered music and its source MIDI files.
Here is the synthesizer playing an imported arrangement, song_01_slay_the_evil. It uses the same instruments as the hand-written demo, with a larger score:
The importer uses only Python’s standard library. It reads MIDI formats 0 and 1, tracks tempo, note-on and note-off events, program changes, chords, and percussion on MIDI channel 10. General MIDI programs are mapped onto the small preset collection rather than reproduced as samples.
You can validate the files in import/input without writing anything:
python3 import/import.py --check
To generate compilable Erlang modules in src, use:
python3 import/import.py --output src
On Windows, use python if that is the name of your Python 3 command.
Existing destinations are skipped unless —overwrite is supplied. Importing is an optional composition workflow; neither Python nor a MIDI parser participates when a song plays. Once generated, a song is simply Erlang source data, and the same mathematical instruments render it.
Follow one note and every transformation is visible: a4 becomes MIDI 69, MIDI 69 becomes 440 Hz, 440 Hz drives an oscillator, an envelope shapes its amplitude, voices add together, floats become little-endian integers, and buffered PCM reaches the speakers.
Chiptune began in a world where small sound vocabularies were unavoidable. Recreating that economy in Erlang is a reminder that a musical instrument does not have to be a large framework. Sometimes it can be a few functions, a clock, and 44,100 samples per second.
To hear how much one function matters, change the lead’s saw call to square in music_synth.erl, then run .\run-windows.bat demo_song solo 6 on Windows or ./run-macos.sh demo_song solo 6 on macOS. Keep the notes the same and listen to how the instrument changes. Next, try a longer attack or a shorter release: the score stays familiar while the sound becomes your own.
Erlang Solutions, “The sound of Erlang: How to use Erlang as an instrument”, 2020.
Kenneth B. McAlpine, Bits and Pieces: A History of Chiptunes (book), Oxford University Press, 2018.
The post A Live Chiptune Synthesizer in Erlang appeared first on Erlang Solutions.
The Ignite Realtime community is proud to announce that Smack 4.5.0 has been released.
This is a new major release after 4.4 nearly two years ago, with many bug fixes and improvements. Smack’s modular connection architecture is now deemed stable and users of the legacy XMPPTCPConnection should switch over to a modern architecture. The modular connection architecture abstracts the underlying connection mechanism, for example TCP, BOSH, and WebSockets, transporting the raw bytes of the XMPP stream, from the higher level XML layer. This allows, among other things, for a connection to transparently switch between TCP and WebSocket connections.
As always, this new Smack release is available on Maven Central.
3 posts - 3 participants
We are pleased to announce a new minor release from our stable branch.
This is a relatively small release, as far as change counts go, but several of the bug fixes are important in their own ways. One is notable for causing problems on BSD platforms - we strongly recommend that BSD users upgrade.
We have also switched our TLS profile source over to tlsref.org, which is the new home for the configuration recommendations we use, previously published by Mozilla.
As part of this change, we have added a new configuration option: tls_profile_version.
It selects which version of the recommendations to use. For the 13.0.x series
this will remain pinned at a default of “5.7” (the latest recommendations when
we released 13.0.0), but you are free to change it to a higher version or set
it to "latest". This mechanism allows us to safely make new TLS
configuration changes available in the future to people who want them, even in
our stable branches, without risking interoperability issues for deployments
that prefer stability.
Some minor memory leaks were also fixed in this release, which would be noticeable mainly on large or busy deployments.
A summary of changes in this release:
As usual, download instructions for many platforms can be found on our download page
If you have any questions, comments or other issues with this release, let us know!
There will be a XMPP Sprint in London from the 6th to the 8th of November at The Mitre in Greenwich.
The sprint is organised by Matthew Fennell and co-organised by Polarian.
If you are interested in attending please do book quickly and add your details onto the wiki page, if you do not have an account please contact one of the organisers and we can do it for you:
Also any operators interested in improving the operator space and would like to attend the sprint please get into contact with Polarian, who is an XMPP operator and interested in working on operator related issues.
OpMeet are online meetups/conferences targeted at XMPP Operators.
The first OpMeet will be on the 10th October 2026, more information on the XMPP Wiki. This will be the first OpMeet, so expect it to be a little rough around the edges.
CfP (Call for Participation) or what we are calling CfI (Call for Ideas) has been open for a few months, but was lost to the mailing list.
An updated announcement has been posted, CfP/CfI are open until 1st October 2026.
The following (non-exhaustive) list of things we are interested in:
Ideas for discussions. Have an operator adjacent issue? Suggest a session to discuss this issue with other operators.
Hackrooms, have a specific project others might be interested in discussing or helping out with during a live coding session? Suggest it!
Talks, this is the big one. We will aim to record these talks and publish them. However we can make no promises here. If you have an interesting topic which is operator related, or even the wider XMPP topics, please do suggest it.
Anything else you can think of which could be interesting to XMPP operators, feel free to suggest, the worst case is the idea gets refused!
To make suggestions please contact Polarian:
On mobile (and worse), baseline XMPP is a little more painful than it should be. It has a lot of round-trips, where the client sends a request and waits - patiently - for the answer before it can continue.
Baseline XMPP actually has 9 of these before you can send and receive messages, and while Openfire has dropped some of these for a while (we’ve supported Direct TLS, for clients, for ever), others haven’t yet made it here. Silly, as I wrote SASL2 some time ago, which is a framework for amortising some of the start-up requests into the authentication.
Others are Bind2 - which pulls resource binding into SASL2 and also allows multiple other “connection setup” things to work - and FAST, which hands out tokens to allow reauthentication which is, well, FAST. This gets us down to 4 round-trips - halving connection time.
Thanks to a supporting grant from the NLNet Foundation, these are now all in our “main” branch, and undergoing their final testing before we make a release, so will be in our nightly builds from now on. I’ve been lucky enough to spend three weeks doing (sometimes literal!) field testing over slow links, so I’ve seen first hand that fortune really favours the bold with these extensions.
We’ve also improved security, with Channel Bindings and Downgrade Protection - also in the nightlies, and added new SCRAM variants like SCRAM-SHA-512-PLUS. Sorry Alexey, we’ve not done the SHA3 variant quite yet, but our FAST implementation does support the new HT2-* family, including the HT2-SHA3- variants.
We even found another round-trip to save, with Initial Authentication Pipelining.
There is also supporting work to make all this happen - like improvements to message archiving, and improvements to our sister project, the XMPP Interop Testing framework, so we can do live testing. This is based on Smack, so that, too now has SASL2, Bind2, and FAST support in its latest Alpha release, 4.6.0-alpha1.
We’d welcome people trying this code out - it’s a lot of exciting new features, and should be highly beneficial to most users of Openfire, and indeed client developers using Smack. It’s still nightlies, not releases, but we’re keen to move this forward as, erm, FAST as we can.
Yeah, I’m not even sorry.
For other release announcements and news follow us on Mastodon or X
2 posts - 2 participants
Earlier today, version 1.8.0 has been released. This release adds tests for exciting new XMPP functionality!
Classic XMPP connection setup takes a lot of round-trips: authenticate, bind a resource, enable carbons, kick off stream management, sync the archive. Each one is its own request. Fine on wifi, annoying on a shaky mobile connection.
XMPP offers various XEPs to fix that. SASL2 (XEP-0388) turns authentication into a single extensible envelope instead of a fixed sequence of steps, and everything else here builds on it. Bind 2 (XEP-0386) folds resource binding and feature enablement into that same envelope, so a client can walk away with a fully set-up session in (almost) one shot. And FAST (XEP-0484) lets a client swap its password for a short-lived, rotating token and reconnect to the server, authenticated, in a single round-trip, no SCRAM handshake required.
Put together, a client can go from opening a socket to fully authenticated and ready to send in essentially one round-trip. That’s also exactly the kind of multi-XEP choreography where servers tend to disagree on the details, so we’re glad to have it covered.
Give the new tests a try, and let us know what you find!
Splash image courtesy of Conny Schneider, Unsplash

XMPP Newsletter Banner
Welcome to the XMPP Newsletter, great to have you here again! This issue covers the month of August 2026.
The XMPP Newsletter is brought to you by the XSF Communication Team and contributors of the XMPP community.
Just like any other product or project by the XSF, the Newsletter is the result of the voluntary work of its members and contributors. If you are happy with the services and software you may be using, please consider saying thanks or help these projects!
Interested in contributing to the XSF Communication Team? Read more at the bottom.
The XMPP Standards Foundation develops extensions to XMPP in its XEP series in addition to XMPP RFCs. Developers and other standards experts from around the world collaborate on these extensions, developing new specifications for emerging practices, and refining existing ways of doing things. Proposed by anybody, the particularly successful ones end up as Final or Active - depending on their type - while others are carefully archived as Deferred. This life cycle is described in XEP-0001, which contains the formal and canonical definitions for the types, states, and processes. Read more about the standards process. Communication around Standards and Extensions happens in the Standards Mailing List (online archive).
The XEP development process starts by writing up an idea and submitting it to the XMPP Editor. Within two weeks, the Council decides whether to accept this proposal as an Experimental XEP.
If an experimental XEP is not updated for more than twelve months, it will be moved off Experimental to Deferred. If there is another update, it will put the XEP back onto Experimental.
Last calls are issued once everyone seems satisfied with the current XEP status. After the Council decides whether the XEP seems ready, the XMPP Editor issues a Last Call for comments. The feedback gathered during the Last Call can help improve the XEP before returning it to the Council for advancement to Stable.
Please share the news on other networks:
Also check out our RSS Feed!
Looking for job offers or want to hire a professional consultant for your XMPP project? Visit our XMPP job board.
This is a community effort, and we would like to thank translators for their contributions. Volunteers and more languages are welcome! Translations of the XMPP Newsletter will be released here (with some delay):
Contributors:
Translations:
This XMPP Newsletter is produced collaboratively by the XMPP community. Each month’s newsletter issue is drafted in this simple pad. At the end of each month, the pad’s content is merged into the XSF GitHub repository. We are always happy to welcome contributors. Do not hesitate to join the discussion in our XSF Communications Team group chat (MUC) and thereby help us sustain this as a community effort. You have a project and want to spread the news? Please consider sharing your news or events here, and promote it to a large audience.
Tasks we do on a regular basis:
For this newsletter either log in here and unsubscribe or simply send an email to newsletter-leave@xmpp.org. (If you have not previously logged in, you may need to set up an account with the appropriate email address.)
This newsletter is published under CC BY-SA license.

The largest single piece of that work is where you stopped reading, and how Fluux shares that position with your other clients. The rest is spread thin on purpose: badges that would not clear, a "New messages" line that kept coming back, a conversation that moved while you were reading it, failures that never said why.
Every XMPP client on your account should agree on where you stopped reading. Fluux publishes that position with XEP-0490, and this release closes the cases where the publication used to stall.
There used to be more than one way to answer "how many messages are unread here", and the answers could disagree. Now there is one: the messages that sit after your last read position. Every surface that shows a number reads that same number.
Scrolling is how you navigate a conversation. It should never happen on its own.
Silence is the worst answer an app can give.
Fluux Messenger 0.17.3 is available for macOS, Windows and Linux, or directly in your browser, from the Fluux Messenger page. If you upgrade and something still feels off, tell us: bug reports and feature requests both go to GitHub Issues.
Distributed systems need async message delivery across nodes. Phoenix provides Phoenix PubSub for this, with pluggable adapters for different backends — officially PG2 and Redis.
This post walks through implementing a Phoenix PubSub adapter backed by EventStore, an Elixir event sourcing library that persists events to PostgreSQL as an append-only log.
Using EventStore as a PubSub backend has a few advantages over the default PG2 adapter:
The tradeoffs are the need for storage and the additional latency of a database round-trip per broadcast, making it best suited for lower-throughput messaging where persistence and cross-node decoupling matter more than raw speed. This implementation is a proof of concept — no load tests were performed.
A full implementation of the adapter can be found on Github.
A Phoenix PubSub adapter must implement a few callbacks specified in Phoenix.PubSub.Adapter:
node_name(adapter_name)
Returns the node name as an atom or binary. Used mainly by Phoenix.Tracker. In most cases:
def node_name(nil), do: node() def node_name(configured_name), do: configured_name
child_spec(keyword)
Generates the child spec for the adapter. GenServer provides a default; this rarely needs overriding.
broadcast(adapter_name, topic, message, dispatcher)
Called when a message is broadcast through Phoenix.PubSub.broadcast. The adapter_name is the PubSub name with .Adapter appended (e.g. MyApp.PubSub → MyApp.PubSub.Adapter). The dispatcher module handles local delivery via dispatch/3.
direct_broadcast(adapter_name, node_name, topic, message, dispatcher)
Same as broadcast/4 with an additional node_name — the message should only reach subscribers on that node.
This section walks through a possible implementation of a Phoenix PubSub adapter that uses EventStore to distribute messages between nodes. This gives a solution that does not depend on Erlang/Elixir distribution, and an event log is stored in case further analysis is needed.
Phoenix.PubSub uses Elixir’s Registry for subscriptions — each subscribe call registers an entry under the topic key. When broadcast is called, the framework invokes the adapter callback to distribute the message, then handles local dispatch.
The adapter’s job is to get the message to other nodes. For direct_broadcast, only subscribers on the target node should receive it.
The adapter is a GenServer that joins the PubSub supervision tree. An eventstore option selects which EventStore module to use (in case you have multiple):
{Phoenix.PubSub,
[name: MyApp.PubSub,
adapter: Phoenix.PubSub.EventStore,
eventstore: MyApp.EventStore]
}
The GenServer stores the EventStore module and the PubSub name in state — both are needed later:
defmodule Phoenix.PubSub.EventStore do @behaviour Phoenix.PubSub.Adapter use GenServer def start_link(opts) do GenServer.start_link(__MODULE__, opts, name: opts[:adapter_name]) end def init(opts) do {:ok, %{ eventstore: opts[:eventstore], pubsub_name: opts[:name] }} end #... implementation will come here ...# end
Note the difference between opts[:name] and opts[:adapter_name]. The former is the name of the PubSub as a whole and is reserved for the Registry. Publishers use it when broadcasting messages. opts[:adapter_name] can be used as the name of the GenServer.
The GenServer appends a new event to the EventStore when broadcast is called:
def broadcast(server, topic, message, dispatcher, metadata \\ %{}) do metadata = Map.put(metadata, :dispatcher, dispatcher) GenServer.call(server, {:broadcast, topic, message, metadata}) end def handle_call( {:broadcast, topic, message, metadata}, _from_pid, %{id: id, eventstore: eventstore, serializer: serializer, pubsub_name: pubsub_name} = state ) do event = %EventStore.EventData{ # ... constructed below } res = eventstore.append_to_stream(topic, :any_version, [event]) # For direct_broadcast targeting the current node, the framework does not # call local dispatch, so the adapter must do it. For regular broadcast, # the framework handles local dispatch after adapter.broadcast returns :ok. current_node = to_string(node()) destination_node = Map.get(metadata, :destination_node) if destination_node == current_node do dispatcher = Map.get(metadata, :dispatcher, Phoenix.PubSub) Phoenix.PubSub.local_broadcast(pubsub_name, topic, message, dispatcher) end {:reply, res, state} end
direct_broadcast/5 is a thin wrapper that sets destination_node in the metadata before delegating to broadcast/5:
def direct_broadcast(server, node_name, topic, message, dispatcher) do metadata = %{ destination_node: to_string(node_name), source_node: to_string(node()) } broadcast(server, topic, message, dispatcher, metadata) end
source_node is stored in the event metadata for auditing. Routing is handled downstream by comparing destination_node against the current node.
The key decision is how to wrap the message inside %EventStore.EventData{}. Serialization is handled by a pluggable module (defaulting to Phoenix.PubSub.EventStore.Serializer.Base64) so the adapter is not tied to a specific encoding. The default serializer base64-encodes :erlang.term_to_binary/1 output — this is necessary because EventStore stores data as JSON and raw binaries would be invalid, and because JSON cannot distinguish atoms from strings so a round-trip through term serialization preserves type fidelity.
event = %EventStore.EventData{ event_type: to_string(serializer), data: serializer.serialize(message) }
A custom serializer can be provided via the serializer option as long as it implements serialize/1 and deserialize/1.
Now that events are in the event store, any subscribed process will receive them. The GenServer must subscribe to all topics ("$all"). If the event store is also used for another purpose, it’s best to have a separate one for PubSub. The subscription is set up via handle_continue/2, which runs immediately after init/1 completes, before any other messages can be processed.
def handle_continue(:subscribe, %{eventstore: eventstore} = state) do eventstore.subscribe("$all") {:noreply, state} end def handle_info({:subscribed, _subscription}, state), do: {:noreply, state}
A transient subscription is used since previous messages are not needed. The event store replies with a {:subscribed, subscription} message, which must also be handled. After this, the server will start receiving {:events, events} messages.
To avoid dispatching a local message twice (once from broadcast and once when the event arrives back from EventStore), a unique ID is added to the process state:
def init(opts) do {:ok, %{ id: generate_unique_id(opts), eventstore: opts[:eventstore], pubsub_name: opts[:name], serializer: opts[:serializer] || Phoenix.PubSub.EventStore.Serializer.Base64 }, {:continue, :subscribe}} end defp generate_unique_id(opts) do unique_id_fn = opts[:unique_id_fn] || fn _name -> UUID.uuid4() end unique_id_fn.(opts[:name]) end
A custom ID generator can be provided via unique_id_fn — a function that receives the PubSub name and returns a unique string. Useful when UUID is unavailable or when a deterministic ID is needed for testing.
The id is added to the event’s metadata field as source_id, keeping it separate from the message data. Serialization is delegated to the configurable serializer module. The handle_call for :broadcast becomes:
event = %EventStore.EventData{ event_type: to_string(serializer), data: serializer.serialize(message), metadata: Map.put(metadata, :source_id, id) }
Where the value of id and serializer come from the state, and metadata already contains dispatcher and any destination_node for direct broadcasts. When an event arrives back, source_id identifies the origin node so duplicates can be skipped:
def handle_info({:events, events}, state) do Enum.each(events, &local_broadcast_event(&1, state)) {:noreply, state} end defp local_broadcast_event( %EventStore.RecordedEvent{ data: data, metadata: metadata, stream_uuid: topic, eventbies_type: event_type }, %{id: id, serializer: serializer, pubsub_name: pubsub_name} = _state ) do current_node = to_string(node()) %{source_id: source_id, destination_node: destination_node, dispatcher: dispatcher} = convert_metadata_keys_to_atoms(metadata) is_destination? = is_nil(destination_node) or destination_node == current_node if not is_nil(dispatcher) and is_destination? and source_id != id and event_type == to_string(serializer) do Phoenix.PubSub.local_broadcast( pubsub_name, topic, serializer.deserialize(data), maybe_convert_to_existing_atom(dispatcher) ) end end
That’s it — a complete implementation of Phoenix PubSub using EventStore, including support for direct_broadcast via the destination_node metadata field and pluggable serialization.
The complete implementation can be found at esl/phoenix_pubsub_eventstore.
Need help building reliable distributed systems with Elixir? Get in touch with our team.
The post Implementing a Phoenix PubSub Adapter with EventStore appeared first on Erlang Solutions.
When I need to configure something in a complicated way, I find myself reviewing embedded languages supported by the server to create a flexible configuration. In Redis, you can improve the performance of requests, in Nginx, you can improve the handling of incoming requests, FreeSwitch offers alternatives for performing the same tasks using different embedded languages. Even in a software like TheGimp, you can add your own code to edit images.
Among the embedded languages, JavaScript and Lua are the most commonly used languages. JavaScript is very well known to the Erlang community because it was integrated (as a port, it is not implemented on top of Erlang) in popular products such as CouchDB and Riak. But I think the more exciting option, raised by Erlang Co-Creator, Robert Virding, is to implement Lua on top of Erlang, which can be used as an embedded language.
Why? Let’s take a look.
Usually, when tasked with fitting the definition of a behaviour we would like to configure, we would create an algorithm in a simple language such as Lua. This saves us from performing activities like:
This kind of implementation is used frequently. It is easy to think of examples you’re likely to come across in day-to-day life. For example, supermarket offers which have multiple dependencies, commissions for salespeople which might feature variable ranges and percentages based on the type of sale, amount of sale or tax brackets, even SMS, emails or HTTP requests could be considered examples.
To demonstrate this, let’s look at an example of developing a load balancer. This is a simple project using cowboy, and luerl as dependencies, and depending on the headers and other information from the HTTP request, we can send it to the different web servers we have available and configured.
Based on the above premise we can write the configuration as follows:
{load_balancer, [
{servers, [
{odin, "1.1.1.1", [
{in, method, [post]},
{'>', <<"content-length">>, 10000},
{in, <<"accept">>, [<<"json">>]}
]},
{thor, "1.1.1.2", [
{in, method, [get, post]},
{'==', http_version, <<"2">>}
]},
{balder, "1.1.1.3", [
{in, method, [get, post]}
]}
]}
]}.
As you can see, we have to define a 3-tuple system for the rules with the operation in the first element and the two operators as the following elements inside of the tuple. In addition, we are occasionally handling the second element as a header name (if it is a binary), but at other times it’s the method we use to perform the request (using the atom “method”) and other times still, the HTTP version is used to gather the information.
The problem is that we have no closed specifications. We could add more elements or even change the meaning of them. What if we want to use logical modifiers like “and” and “or” to join the checks instead of assuming they are always using “and”? This change will add more complexity to our configuration and more complexity means more possibilities for making mistakes.
At the moment, if the configuration is wrong or adds something that is not granted, it is up to us to trigger the corresponding error and point to where it is to make it easier to fix. As you can imagine, that is not an easy thing to do if you are handling it during runtime.
It’s not unconventional to think about configuration in terms of a specific code. At this point, Lua code could be put in charge of the definition because it is based on Lua semantics.
We only need the information for the configuration and running of the snippet to give us the desired behaviour we want to plug into the correct place. For example, the previous configuration could be written as:
local odin = "1.1.1.1"
local thor = "1.1.1.2"
local balder = "1.1.1.3"
local method = http.method()
local size = tonumber(http.header("content-length")) or 0
local accept = config.split(http.header("accept") or "", ", ")
local httpver = http.version()
if method == "post" and size > 10000 and config.member("json", accept) then
return odin
elseif config.member(method, {"get", "post"}) and httpver == "2" then
return thor
elseif config.member(method, {"get", "post"}) then
return balder
end
As you can see, we are able to optimise and fix the code to suit our needs, it is shorter and clearer than the original configuration and, most importantly, we can now test and check to be sure it is compiling correctly.
The important thing to keep in mind is that the configuration code must include the functions which are going to be needed to handle the request. In the example above, we are using functions like http_version(), http_header("...") or even split(...) and member(...). These functions should be provided to the interpreter.
Of course, the interpreter also has other functions available, we only need to provide the specific functions that are required for our business logic.
In addition to improving the performance, we also improve security because we can use luerl_sandbox:init() to sandbox the function calls the code will not be able to access functions that are not explicitly exposed to it.
Inserting the Lua code into the configuration can be a little tricky. To avoid this, I recommend putting these scripts into the priv directory as a normal Lua file (using the extension .lua ) – (this one is more of a code formatting of priv and .lua). This could even be done inside of a database if we are handling the configuration in an automated way using a key/value storage configuration such as etcd.
The most important thing to keep in mind before running that code is to have a specific task which helps you to parse it and ensure the code is correct. One solution is to conduct a testing phase to ensure that the configuration is not breaking or negatively impacting other parts of the system.
For example, in the previous code, we would write a couple of libraries, one called utils for the functions needed for strings and tables and another called http needed for the HTTP functions. An example would be:
-module(luerl_lib_http). -export([load/1, install/1, put_request/2]). -include_lib("luerl/include/luerl.hrl"). -define(REQUEST, http_request). load(St) -> luerl:load_module([<<"http">>], luerl_lib_http, St). install(St) -> luerl_heap:alloc_table(table(), St). put_request(Request, St) -> luerl:put_private(?REQUEST, Request, St). table() -> [ {<<"method">>, #erl_func{code = fun method/2}}, {<<"version">>, #erl_func{code = fun version/2}}, {<<"header">>, #erl_func{code = fun header/2}} ]. method(_Args, St) -> #{method := Method} = request(St), {[Method], St}. version(_Args, St) -> #{version := Version} = request(St), {[Version], St}. header([Name|_], St) when is_binary(Name) -> #{headers := Headers} = request(St), {[maps:get(Name, Headers, nil)], St}; header(Args, St) -> luerl_lib:badarg_error(<<"header">>, Args, St). request(St) -> luerl:get_private(?REQUEST, St).
defmodule LuerlLib.Http do require Record Record.defrecord(:erl_func, Record.extract(:erl_func, from_lib: "luerl/include/luerl.hrl")) @private_key :http_request def load(state), do: :luerl.load_module(["http"], __MODULE__, state) def install(state), do: :luerl_heap.alloc_table(exports(), state) def put_request(state, request), do: :luerl.put_private(@private_key, request, state) defp exports do [ {"method", erl_func(code: &method/2)}, {"version", erl_func(code: &version/2)}, {"header", erl_func(code: &header/2)} ] end defp method(_args, state), do: {[request(state).method], state} defp version(_args, state), do: {[request(state).version], state} defp header([name | _], state) when is_binary(name), do: {[Map.get(request(state).headers, name)], state} defp header(args, state), do: :luerl_lib.badarg_error("header", args, state) defp request(state), do: :luerl.get_private(@private_key, state) end
-module(luerl_lib_config). -export([load/1, install/1]). -include_lib("luerl/include/luerl.hrl"). load(St) -> luerl:load_module([<<"config">>], luerl_lib_config, St). install(St) -> luerl_heap:alloc_table(table(), St). table() -> [ {<<"split">>, #erl_func{code = fun split/2}}, {<<"member">>, #erl_func{code = fun member/2}} ]. member([Entry, #tref{}=Table], St) -> #table{a = Array} = luerl_heap:get_table(Table, St), Result = array:foldl(fun (_, V, false) when V =:= Entry -> true; (_, _, Acc) -> Acc end, false, Array), {[Result], St}; member(Args, St) -> luerl_lib:badarg_error(<<"member">>, Args, St). split([String, Sep], St) when is_binary(String), is_binary(Sep) -> {Tref, St1} = luerl:encode(string:split(String, Sep, all), St), {[Tref], St1}; split(Args, St) -> luerl_lib:badarg_error(<<"split">>, Args, St).
defmodule LuerlLib.Config do require Record Record.defrecord(:erl_func, Record.extract(:erl_func, from_lib: "luerl/include/luerl.hrl")) Record.defrecord(:table, Record.extract(:table, from_lib: "luerl/include/luerl.hrl")) def load(state), do: :luerl.load_module(["config"], __MODULE__, state) def install(state), do: :luerl_heap.alloc_table(exports(), state) defp exports do [ {"split", erl_func(code: &split/2)}, {"member", erl_func(code: &member/2)} ] end defp split([string, separator | _], state) when is_binary(string) and is_binary(separator) do {table_ref, state} = :luerl.encode(String.split(string, separator), state) {[table_ref], state} end defp split(args, state), do: :luerl_lib.badarg_error("split", args, state) defp member([entry, table_ref | _], state) when Record.is_record(table_ref, :tref) do table(a: array) = :luerl_heap.get_table(table_ref, state) found = :array.sparse_foldl(fn _index, value, acc -> acc or value === entry end, false, array) {[found], state} end defp member(args, state), do: :luerl_lib.badarg_error("member", args, state) end
As you can see, we are implementing the functions we need and making them available to our Lua interface under the config and http packages. To load these functions, we have to run the load function which is exported in both modules. NOTE: install is used as a callback function in load_module
A great benefit of doing things this way is the “compile once, and ready many” approach. The config does not have to be loaded for every request. If we put it all together in our application, it would something like like the following
Add a build_request function and its helpers to your luerl_lib_http and LuerlLib.Http modules.
-export([load/1, install/1, put_request/2, build_request/1]). % add export build_request(CowboyReq) -> #{method => string:lowercase(cowboy_req:method(CowboyReq)), version => version_to_binary(cowboy_req:version(CowboyReq)), headers => cowboy_req:headers(CowboyReq)}. version_to_binary('HTTP/1.0') -> <<"1.0">>; version_to_binary('HTTP/1.1') -> <<"1.1">>; version_to_binary('HTTP/2') -> <<"2">>.
def build_request(cowboy_req) do %{ method: cowboy_req |> :cowboy_req.method() |> String.downcase(), version: version_to_binary(:cowboy_req.version(cowboy_req)), headers: :cowboy_req.headers(cowboy_req) } end defp version_to_binary(:"HTTP/1.0"), do: "1.0" defp version_to_binary(:"HTTP/1.1"), do: "1.1" defp version_to_binary(:"HTTP/2"), do: "2"
Add handler modules for handling the request
-module(luerl_lib_handler). -export([init/2]). init(CowboyReq, {Form, St}) -> LuaReq = luerl_lib_http:build_request(CowboyReq), St1 = luerl_lib_http:put_request(LuaReq, St), Reply = case luerl:call_chunk(Form, St1) of {ok, [], _St2} -> cowboy_req:reply(404, #{}, <<"no matching route">>, CowboyReq); {ok, Rets, St2} -> [Backend] = luerl:decode_list(Rets, St2), cowboy_req:reply(200, #{}, Backend, CowboyReq); {lua_error, _Reason, _St2} -> cowboy_req:reply(500, #{}, <<"routing error">>, CowboyReq) end, {ok, Reply, {Form, St}}.
defmodule LuerlLib.Handler do def init(cowboy_req, {form, state}) do lua_req = LuerlLib.Http.build_request(cowboy_req) state1 = LuerlLib.Http.put_request(state, lua_req) reply = case :luerl.call_chunk(form, state1) do {:ok, [], _state2} -> :cowboy_req.reply(404, %{}, "no matching route", cowboy_req) {:ok, returns, state2} -> [backend] = :luerl.decode_list(returns, state2) :cowboy_req.reply(200, %{}, backend, cowboy_req) {:lua_error, _reason, _state2} -> :cowboy_req.reply(500, %{}, "routing error", cowboy_req) end {:ok, reply, {form, state}} end end
With all of that defined, we can now call the relevant functions from our application startup:
-module(luerl_lib_app). -behaviour(application). -export([start/2, stop/1]). start(_StartType, _StartArgs) -> {ok, Form, St} = build_lua_state(), Dispatch = cowboy_router:compile([{'_', [{'_', luerl_lib_handler, {Form, St}}]}]), {ok, _} = cowboy:start_clear(luerl_lib_listener, [{port, 8080}], #{env => #{dispatch => Dispatch}}), luerl_lib_sup:start_link(). stop(_State) -> ok = cowboy:stop_listener(luerl_lib_listener). build_lua_state() -> St0 = luerl_lib_http:load(luerl_lib_config:load(luerl_sandbox:init())), luerl:loadfile("priv/config.lua", St0).
Supervisor
-module(luerl_lib_sup).
-behaviour(supervisor).
-export([start_link/0, init/1]).
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
init([]) ->
{ok, {#{strategy => one_for_one, intensity => 1, period => 5}, []}}.
defmodule LuerlLib.Application do use Application def start(_type, _args) do state = :luerl_sandbox.init() |> LuerlLib.Config.load() |> LuerlLib.Http.load() {:ok, form, state} = :luerl.loadfile(~c"priv/config.lua", state) dispatch = :cowboy_router.compile([{:_, [{:_, LuerlLib.Handler, {form, state}}]}]) {:ok, _} = :cowboy.start_clear(:luerl_lib_listener, [port: 8080], %{env: %{dispatch: dispatch}}) LuerlLib.Supervisor.start_link() end end
Notice that we load the file once (with the lines below) and pass it down to the handler, it helps us obtain the forms that we need and the handler will always execute based on that initially loaded configuration.
{ok, Form, St1} = luerl:loadfile("priv/config.lua", St0).
{:ok, form, state} = :luerl.loadfile(String.to_charlist(path), state)
Lua scales up because it is built on top of Erlang. This means Lua is using the same processes as Erlang does, it also means we are not using ports to communicate with the Lua interpreter, we have the Lua interpreter running on Erlang.
This makes a great difference in comparison to JavaScript because if you are handling millions of requests and all of them require a JavaScript snippet, this can cause a bottleneck very quickly if you have to limit the number of ports or requests.
On the other hand, Lua is using native Erlang functions when it calls the functions we want to provide for the interpreter. That makes a clear improvement, saving us from performing data serialisation or transformation.
Using a language for flexible configuration gives us the possibility to create an easy interface to provide configurations, reduce the amount of code we need to write, and improve the maintenance without jeopardising the performance of the system. At the moment, you can use Lua as we have explained during the article or you can jump into PHP if you need to process text or templates on top of Erlang or Elixir.
Alternatively you can join the community and provide other solutions which help us build better fit-for-purpose software. Get in touch if you need help building your system with one of these solutions.
The post How to use Lua for flexible configurations in Erlang and Elixir appeared first on Erlang Solutions.
UPDATE: The “Jabber/XMPP: 25 Years of Digital Independence” talk by Daniel Gultsch has been accepted for LinuxDays. Note that if you are unable to come to see it in person, it should be possible to watch it online.
Members of the community will operate an XMPP-themed stand at the upcoming LinuxDays conference in Prague – if you are around, you are welcome to come visit. Similarly, if you know of someone who might be interested in XMPP or whom you would like to introduce to it, Czech-speaking or not, please pass on the information to them.
Two XMPP talks have also been submitted for the conference, you can support them in a vote which takes place over the first week of September.
The conference takes place over the first weekend of October, 3rd and 4th, at the Faculty of Information Technology of the Czech Technical University and is free to attend (participants are asked to register, if possible). You can find more information at the conference’s website (in Czech).