Is it your community or not?

    Is it your community or not?

    Armada communities already run without a host. Here is how to own the rest: the app you load, the relay your messages travel over, and the server your calls go through.

    Derek Ross

    I have been running a homelab since the early 2000s. Twenty years of hosting my own infrastructure at home: media servers, document servers, backups, whatever else I did not want living on someone else's box. Not because any of it was the convenient option. Because when someone else runs the server, someone else decides what it does and how long it stays up.

    Chat has the same shape and higher stakes, because chat is where a community actually lives. If the app, the relay, and the voice server all belong to one company, then that company is your community's landlord, whatever the license says. You can be a very well treated tenant right up until you are not.

    Armada is built so you can evict the landlord. Here is how to do it, piece by piece, and how to decide which pieces are worth your time.

    What you actually depend on today

    Start with an honest inventory, because "self-host everything" is a slogan and the useful version is knowing which piece does what.

    An Armada community is already serverless. There is no server that owns your membership list, your channels, or your history. Every message is encrypted before it leaves your device and published as an ordinary Nostr event. The machines that carry it are storing sealed envelopes. They cannot read them, and neither can we.

    So self-hosting Armada is not about hiding your messages. That is already done. It is about availability and independence. Three pieces still sit on infrastructure Soapbox operates:

    1. The web app at armada.buzz. A static site. If we go down, for whatever reason, the people loading the web app from us are stuck.
    2. The relays. These are the machines that store and pass along messages. The app ships with ours as the defaults. Your community's own traffic goes to whatever relays you add, but out of the box, the defaults are ours.
    3. The voice server. Starting a call goes through a server that hands out the ticket to join a room. It learns nothing about your community by design, but it is our uptime, not yours.

    You can take back all three, today. The third takes the most work to run, and I will come back to it.

    None of the three addresses are baked into the app. It reads them from configuration at runtime, which is what makes everything below possible, and why the official mobile app keeps working against a stack we have never seen.

    Pick your pieces. You do not need all of them.

    Before the how-to, the part nobody selling you infrastructure would lead with: you probably should not run all of this, and Armada is built on Nostr specifically so you do not have to.

    An Armada community travels as ordinary Nostr events, the kind called gift wraps (kind 1059 if you ever need to ask an operator). Not a proprietary protocol, not a special server, not something only our relay understands. Any Nostr relay will carry your community, and there are hundreds of them run by people who have no connection to Soapbox and no idea your community exists. nostr.watch is the directory. Pick a few, add them in Settings, done. Worth checking that the ones you pick accept those events and hang on to them rather than expiring them quickly, since that varies by operator.

    So the realistic version of this post is a menu:

    • Host the frontend, use existing relays. Your domain, your build, your defaults, someone else's storage. No OpenSearch, no unit files, no backups. This is the one most communities should pick, and it takes an afternoon.
    • Host the frontend and a relay. The whole road is yours. That is section 3, and it is a real database with real operational weight.
    • Add voice. One more box, and the one piece that genuinely has to be a server. It is the shortest install here and the only one you cannot skip if you want calls on your own infrastructure.
    • Host nothing. Use armada.buzz and add whichever relays you like. Your community still has no owner, and your messages are still unreadable to everyone carrying them.

    Spreading a community across several independent relays is also just better availability than one server you maintain alone. One relay going down does not take your community with it. The reason to run your own is that you want it, or you have a requirement nobody else's relay meets, not that the default is weak.

    One thing to install first

    Armada's own code does not live on GitHub. It lives on Nostr, the same network the messages travel over, so its addresses start with nostr:// instead of https://. Git does not understand those on its own. Install ngit once and it does, and from then on git clone behaves exactly the way you expect. If git over Nostr is new to you, we wrote up the longer version.

    That covers two of the three things below: the client, and the voice broker. The relay is a separate project, hosted on GitLab, and clones the ordinary way.

    Hosting the frontend

    The web client is a static site. The repo ships a Dockerfile that builds it and serves it with nginx on port 80. Put your own TLS in front of it, the same way you would in front of anything else at home.

    Get the code

    git clone nostr://soapbox.pub/relay.ngit.dev/armada
    cd armada

    Settings are baked in at build time, as Docker build args. Four of them matter:

    Build argWhat it does
    VITE_APP_NAMEWhat the app calls itself.
    VITE_APP_RELAYSThe relays a fresh user starts with. They can change these in Settings.
    VITE_SEARCH_RELAYSWhere search queries go.
    VITE_PLATFORM_RELAYSRelays pinned to this build. Empty by default. This is where you put your own relay.

    The rest, media upload servers and similar, have sane defaults and are listed in the repo's README if you want them.

    A build looks like this:

    Build the app

    docker build \
      --build-arg VITE_APP_NAME="Example Chat" \
      --build-arg VITE_APP_RELAYS="wss://relay.example.com" \
      --build-arg VITE_SEARCH_RELAYS="wss://relay.example.com" \
      --build-arg VITE_PLATFORM_RELAYS="wss://relay.example.com" \
      -t example-chat .

    VITE_PLATFORM_RELAYS deserves a note. It is empty in the shipped APK and desktop builds on purpose: a fresh client arrives with no baked-in servers and the user adds their own. For a self-hosted deployment serving one community, pinning your own relay here is exactly what it is for. Do not ever pin ws://localhost. It means nothing on a phone.

    Use the nginx config that ships in the repo rather than writing your own. It gets two things right that are unpleasant to discover later: files under /assets/ return a plain 404 when they are missing instead of quietly serving the app's HTML in their place, which is what keeps a browser tab left open across a deploy from ending up on a broken screen, and its security headers are tuned for a client that holds your private key in the browser. If you do rewrite it, carry both over.

    One more: microphones only work on HTTPS. localhost is fine while you are testing. Anything else needs a real certificate.

    Running your own relay with ditto-relay

    There is no group server to install. A relay for Armada is just a Nostr relay, and the one we run is ditto-relay: AGPL-3.0 like the client, written for Bun, storing events in OpenSearch. It knows nothing about how your community is organised, because none of that reaches it in a form it could read.

    Set expectations before you start. ditto-relay ships no Dockerfile and no compose file, so you are writing the service definition yourself, and you are installing and running OpenSearch 2.x alongside it. If that is a dealbreaker, this is the section to skip: go back to nostr.watch and use relays that already exist. Your community does not care which ones.

    Install Bun and an OpenSearch node, then clone the relay, run bun install, and copy .env.example to .env:

    Get the relay

    git clone https://gitlab.com/soapbox-pub/ditto-relay
    cd ditto-relay

    The settings that matter:

    KeyNotes
    RELAY_URLRequired. The public wss:// address of your relay. It will not start without one.
    NOSTR_NSECRequired. A key for the relay itself. Must be the nsec... form, not hex.
    OPENSEARCH_NODEWhere OpenSearch is. Defaults to http://localhost:9200.
    PORTDefaults to 13131.

    Two of those catch people out. A hex private key in NOSTR_NSEC looks like it should work and does not. And the port is 13131, not the 8000 you will see in one example in the README; trust .env.example.

    Leave AUTH_KINDS alone. Its default means a client has to identify itself before it can even ask for community messages, which keeps a passerby from hoovering up sealed envelopes they cannot open anyway.

    Then bun start. Wrap it in a systemd unit, put nginx or Caddy in front for TLS, and set IP_HEADER to whatever header your proxy adds, so rate limiting counts real visitors rather than counting your proxy over and over.

    Running your own voice

    Voice is the one part that needs a real server running somewhere, because audio cannot travel the way messages do. It is two programs. The first is LiveKit, the ordinary open-source media server, run stock with no fork and no patches.

    The second is a small token broker sitting in front of it, and it exists because LiveKit has no idea what an Armada community is. Before you can join a call, something has to check that you actually hold the key to that channel and then hand LiveKit a ticket saying so. Your client proves it holds the key, the broker checks that signature, and it issues a short-lived ticket good for one room. It never learns which community the room belongs to or who you are, and the audio is encrypted between the people on the call, so LiveKit forwards sound it cannot listen to.

    That blindness has a cost worth knowing before you run one. Because the broker cannot tell your rooms from anyone else's, anyone who finds yours can use it. It is an open service by design, and bounding that is deliberately crude, because every precise tool would require knowing who is calling. Three things do the work: a rate limit per caller address, a cap on how many people can be in one room, and how long a ticket stays valid. The room cap ships unset, which is the right default while you are serving people you know and worth changing the day you are not.

    The setup is one box and about ten minutes. armada-av is the broker we run, and it ships a deployment that brings up all three pieces: the broker, one LiveKit node, and Caddy, which fetches your TLS certificate for you.

    Bring up voice

    git clone nostr://chad@chadwick.site/relay.ngit.dev/armada-av
    cd armada-av/deploy/single
    cp .env.example .env
    docker compose up -d

    Three settings in that .env file. Everything else has a working default:

    SettingWhat it is
    AV_DOMAINThe domain this box answers on, like av.example.com. No https://.
    LIVEKIT_API_KEYGenerated, see below.
    LIVEKIT_API_SECRETSame.

    Generate that pair with:

    Generate the LiveKit keys

    docker run --rm livekit/livekit-server generate-keys

    Before you start it, point the domain at the box and open four ports: 80 and 443 for the web side, then UDP 7882 and TCP 7881 for the audio itself. Nothing else needs to be reachable. Then open Armada, go to Settings and then Voice, and give it https://av.example.com. That is the whole install.

    Three things that bite:

    • AV_DOMAIN has to be the name people actually reach, exactly. Calls are authorised against it, so a wrong value rejects every attempt to join, and it will not look like a domain problem when it happens. A value with a path on the end is refused at startup rather than quietly trimmed.
    • Open TCP 7881, not just the UDP port. It is the fallback for people on networks that block UDP, and leaving it shut is the usual reason one person keeps dropping out while everyone else is fine.
    • If you skip the shipped setup and wire your own proxy, the LiveKit address you hand clients is a bare wss://av.example.com, never with /rtc on the end. The client appends that itself.

    The payoff

    Here is what you have when you are done: a web app on your domain, carrying your defaults, backed by a relay you administer, carrying sealed traffic that neither we nor you can read from the outside, and your calls going through your own box. Nobody at Soapbox is in that path, and nobody at Soapbox can take it away from you. The answer to the question in the title is yes, all the way down, with no asterisk on the calls.

    Or you stop after the frontend, point it at relays from nostr.watch, and get most of that for an afternoon of work and no database to babysit. That is the honest recommendation for most communities. The menu is the feature.

    And the official Armada app on Android still works with all of it. Not a fork, not a special build, the app you install from the normal place. Nothing about a backend is baked into it. Your users add your relay in Settings and they are on your stack.

    Two caveats on that, and neither is small.

    Deep links do not follow you to your domain.

    An invite link on chat.example.com opens in a browser rather than jumping straight into the app, because the installed app only claims links on armada.buzz. That is not a setting you can flip. Making it work means editing the app's manifest to your own domain, rebuilding it, signing it with your own key, and publishing a file at your domain that vouches for that key. At which point you are shipping and distributing your own build of Armada, with everything that implies: your users install it outside the normal channel, and updating it is now your job. The stock app still talks to your relay perfectly well. It just costs your members one extra tap on an invite. This is Android; on iOS the app does not claim links at all yet.

    And running infrastructure is running infrastructure, if you choose to.

    This one is opt-in, and it is the caveat that goes away entirely if you use relays that already exist. OpenSearch does not arrive with ditto-relay: you install, secure, tune and back up a real search cluster, on top of writing the relay's service definition by hand. Voice adds a box you keep patched and a certificate that has to keep renewing. Armada is serverless by default precisely so that most communities never need any of it. Self-host because you want the independence, not because the default is lacking.

    Armada is AGPL-3.0, and the client is the same nostr:// address you cloned at the top. Read it, run it, break it, tell us what broke.

    Soapbox is funded by grants and donations, not ads or data sales.

    Everything we build is open source and belongs to the community. Help us keep it that way.

    Learn how we use funds →