Self-hosted service
rmyndharis/OpenWA avatar
rmyndharis/OpenWA

OpenWA documents a container-escape risk in its own compose file and tells you how to switch it off

OpenWA is a self-hosted WhatsApp API gateway for applications that need to send messages and manage sessions on their own server.

16,805 stars3,951 forksTypeScriptMIT

At a glance

What is it?
OpenWA is a self-hosted HTTP gateway that lets an application send WhatsApp messages and manage sessions from its own server, with a dashboard, pluggable storage and cache backends, and API keys that can be scoped to specific sessions and chats. It is MIT licensed at version 0.27.1. Two things in its configuration are worth reading closely. Its compose file states in a comment that the container socket proxy cannot scope what a container is created with, so a compromised container could bind-mount the host. And its environment template explains that an empty assignment pins a setting just as hard as a filled one.
Who is it for?
This is the right shape if you need messaging from your own infrastructure rather than from someone else's account, and the key-scoping model is more careful than most gateways of this kind offer, down to refusing an unmapped privacy identifier rather than guessing. Two things to weigh before deploying.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on November 13, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • rmyndharis/OpenWA16,805 ★ · +4,677
Star history
Repository2026-07-282026-11-13
rmyndharis/OpenWA12.1k16.8k

4,677 Stars in the last 108 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

The compose file states that its socket proxy cannot scope what a container is created with

There is a comment block in this project's production compose file that reads like a security advisory, and it is worth quoting.

It names a sidecar container that is the only one with access to the container socket, mounted read-only. It is attached to an internal network with outbound access denied, because all it needs is the local socket.

Then it lists the container operations that sidecar is permitted to perform: health, system information, listing, inspecting, creating, starting and stopping containers, pulling images, creating volumes, and one gate that admits any non-read request when enabled.

And then it says two consequences of the pinned proxy version, both accepted and written down elsewhere: one configuration flag is dead and never read, so it is not set, and as a side effect that operation is admitted to enabled paths; and the proxy cannot scope container-create payloads.

The consequence of that second one is stated in plain words: a compromised API container could create containers with host bind-mounts.

So the project mounts the container socket, puts a proxy in front of it, narrows the network, and then tells you in its own configuration file that the proxy cannot stop a compromised container from mounting the host filesystem.

The mitigation is given too. If you are not using the built-in datastore orchestration, disable the sidecar entirely through a compose override file. And the note says the project's own application never issues those destructive operations; its teardown is stop-only.

That is an unusually candid disclosure, and it tells you the risk is known, bounded and switchable. It does not tell you it is gone.

An empty assignment in the environment file pins a setting just as hard as a filled one

The environment template has a comment block at the top explaining why almost everything in it is commented out, and the reason is a footgun most projects never document.

Configuration comes from three layers, and each supplies only what the previous one left unset: the process environment first, then the file, then a generated file the dashboard writes.

code
process environment  >  .env  >  data/.env.generated   (Dashboard > Infrastructure)

So a value present in the file wins over everything the dashboard saves.

That alone would be an inconvenience. The part worth reading is the next sentence. An empty assignment pins just as hard as a filled one. A password key written with nothing after the equals sign is a set-to-empty value, not an absent one, so it shadows whatever password the dashboard provisioned.

So the failure mode is a dashboard control that moves, saves, and reports success while the running value never changes. That is the worst kind of configuration bug, because the interface tells you it worked.

The project's response is a test. A specification file fails if a dashboard-owned key is shipped uncommented, which is how the entire list is kept commented out in the first place.

So the consequence is that the file you copy is deliberately not a working configuration. Copy it, leave everything commented, and manage those settings through the dashboard or by hand, but do not uncomment a key to see what it does.

Message media is returned to callers and is not written to your storage backend

One sentence in the feature description states a data-handling behaviour that is easy to skim and important to know.

It says the storage backend is the live store for status media and, when chat-media archiving is enabled, archived chat media, while other message media is returned inline to API and webhook consumers.

Read that against the rest of the configuration. You can select a database engine, and you can select a storage backend with local and object-storage options. The natural assumption is that selecting a storage backend means your media lands there.

Only partly. Status media lands there, and chat media does only when chat-media archiving is enabled; other message media is passed through.

That is a defensible design. Persisting media means handling lifecycle, deletion, quota and the question of what happens when someone deletes a chat. Passing it through means the consumer receives it and stores it, or does not.

The consequence is that if you are relying on the gateway as your message archive without chat-media archiving enabled, the media is not in it. You have to persist media at the consumer, which for a webhook means writing it in your handler. Of the media, scripts/backup.sh archives only the local media directory, so an S3 bucket needs a backup of its own.

Key scoping covers the REST surface but not the other three, and chat-scoped keys cannot receive events

The scoping model in this project is the most carefully specified thing on the front page.

There are two scopes. A key can be limited to chosen sessions, and separately to chosen chats. The two are independent, so a key can have either, both or neither.

The REST surface is covered. The documentation is precise about what happens: a key with no sessions selected reaches everything including sessions created later; a key with sessions selected gets a specific status code for a request naming any other session, and its filtered list routes return only its own rows. Chat scoping narrows further, and every route not explicitly marked safe refuses a chat-scoped key, including routes added in later releases, with the refusal as the default.

What a chat-scoped key can still do inside its chats is more than reading and sending. It can delete or clear a chat, leave or rename a group, and block the contact. What it cannot do is change group membership: adding, removing, promoting or demoting, answering join requests, and reading or resetting an invite link all stay closed.

Some surfaces sit outside the session-named routes: the key-management routes and the queue dashboard return 403 to a session-scoped key, and a chat-scoped key cannot reach the queue dashboard.

A chat-scoped key can subscribe to the `/events` WebSocket, which delivers only events for its chats.

So the consequence is that the model is well designed and covers more than one surface. An agent with a chat-scoped key reads its stored messages through `GET /sessions/{sessionId}/messages?chatId=` and is notified of its own chats over the WebSocket.

Identity is matched through a mapping table and an unmapped identifier is refused rather than guessed

There is one detail in the scoping section that shows how closely this project has read the platform's behaviour, and it is worth isolating.

WhatsApp identifies contacts in two ways: a phone-number form and a privacy identifier form. This project matches allowlists through a mapping table between them.

The stated rules are three. A contact allowlisted by phone number also matches the same person's privacy identifier once the table maps them. An unmapped privacy identifier is refused rather than guessed. And the digits of a privacy identifier are never treated as a phone number, so one does not admit the other.

That last rule is the important one. A privacy identifier can have digits in it, and a naive implementation that strips the domain suffix and looks up the digits would treat a privacy identifier as a phone number and match the wrong person.

So the consequence is that the allowlist fails closed. An identity the project cannot positively resolve is refused rather than allowed, which is the correct direction for an access control and is the opposite of what a convenience-oriented implementation would do.

The image build pins the builder to the host platform and explains the arm64 failure it avoids

The container build has a comment block about multi-architecture builds that names a specific failure, and it is a good example of a build file that explains itself.

The builder stage is pinned to the build host's architecture rather than the target's. The reason given is that this stage only produces architecture-independent output, the compiled server code and the static dashboard, so it never needs to run emulated for a target it is not building for.

Without that, on a multi-architecture build the whole dependency install and the front-end build would run under emulation for the 64-bit ARM leg. The comment says that is slow, and that it is also where an optional native dependency for that architecture fails to install, with the error quoted.

Then the base image is pinned by digest rather than by tag, so every build starts from the same immutable base.

And then there is a job in the security-scan workflow that checks the pin has not drifted from what the tag serves. The rule is specific in both directions: the job fails if they differ and the tag has served the newer image for seven days or more, or for three days or more when the pin itself was last changed at least twenty-eight days ago.

So the consequence is that this project treats a stale base-image pin as a build failure rather than as a warning, and the thresholds are written down so the rule is auditable rather than a matter of judgement.

Nine check scripts verify that the generated client still matches the server

The manifest has many scripts, and nine of them are consistency checks rather than build steps.

There is one that compares documented versions against something. One that checks the ignore file for the container build against what it should contain. One that checks the deployment chart's behaviour. One that checks an audit implementation.

Then five that all concern the same thing: a generated client library, checked for its routes, its coverage, its events, its documentation and its data shapes.

And two that export an interface description and then check it.

That is a strong position for a project whose main value is that its client library matches its server, because a hand-written client drifts the moment the server adds a route. Here there are five independent checks that fail if it does.

The tree agrees. There is a client directory, a checked-in interface description file at the top level, a changelog, and a version-checking script that compares what the documentation says against the code.

So the consequence is that if you use the generated client, it is held to the server by the build rather than by discipline, which is a meaningfully different guarantee from the one most hand-maintained clients offer.

Editorial conclusion

This is the right shape if you need messaging from your own infrastructure rather than from someone else's account, and the key-scoping model is more careful than most gateways of this kind offer, down to refusing an unmapped privacy identifier rather than guessing. Two things to weigh before deploying. That it mounts the container socket, which is the single most privileged thing a container stack can do, and that the project's own comment says the proxy in front of it cannot constrain what a created container can bind-mount. And that message media other than status media is returned inline rather than written to your storage backend unless chat-media archiving is enabled, so otherwise you have to retain it yourself.

Frequently asked questions

What is OpenWA?

A free, open-source gateway that exposes a self-hosted HTTP API for WhatsApp, so an application can send messages and manage sessions from its own server rather than through a hosted provider. It is MIT licensed, written in TypeScript on a component server framework, and requires Node 22.19 or newer. It ships a React dashboard for session, webhook and key management, supports multiple sessions on one instance, and has pluggable database, media-storage and cache backends selected by configuration.

how to use openwa

It is container-native and the front page describes the Compose file as production-ready with no configuration. Configuration comes from three layers in a fixed order: the process environment first, then an environment file, then a file the dashboard generates. Status media and, when chat-media archiving is enabled, archived chat media are stored in the storage backend; other message media is returned inline to API and webhook callers, so if you need it retained you persist it at the consumer.

Is there an open source WhatsApp bot available?

This project is not a bot but the gateway layer one would sit on. It exposes sessions over an HTTP API, and the front page lists official integrations packaged as sandboxed plugins, including a helpdesk product and a chatbot builder, with community adapters listed separately in the documentation. An automation-platform integration is also listed as community nodes rather than as official plugins.

Is WhatsApp open source software?

No, and this project does not claim otherwise. It is an independent gateway that sits alongside the messaging service and drives it through a session, under the MIT licence. The front page's claim is about no licensing fees and no feature locks on the gateway itself.

Official sources

  1. Official documentation
  2. Official README
  3. Project repository
  4. Release notes
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/rmyndharis-openwa.svg)](https://hysenlabs.com/projects/rmyndharis-openwa)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/rmyndharis-openwa.svg)](https://hysenlabs.com/projects/rmyndharis-openwa)

Was this useful?