973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 1 | # Malice |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 2 | |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 3 | Malice is a hosted Matrix client with a Rust backend built on macltok and a |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 4 | Svelte frontend. The Nix package serves both the API and the built frontend. |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 5 | |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 6 | ## NixOS module |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 7 | |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 8 | Add this repository to your NixOS flake inputs, then include its module: |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 9 | |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 10 | ```nix |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 11 | # In your flake.nix |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 12 | inputs.malice.url = "git+file:///absolute/path/to/malice"; |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 13 | |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 14 | # In the modules list for your nixosSystem: |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 15 | malice.nixosModules.default |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 16 | ``` |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 17 | |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 18 | Use a published Git URL instead of the local path if you deploy from another |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 19 | machine. For a local Git flake, add new source files to Git before rebuilding; |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 20 | Nix does not include untracked files. |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 21 | |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 22 | Enable the service in your NixOS configuration. For access from another machine, |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 23 | put it behind an HTTPS reverse proxy; the service binds to localhost and uses |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 24 | Secure session cookies by default: |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 25 | |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 26 | ```nix |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 27 | { |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 28 | services.malice.enable = true; |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 29 | # Optional: services.malice.port = 3000; |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 30 | |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 31 | services.nginx.enable = true; |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 32 | networking.firewall.allowedTCPPorts = [ 80 443 ]; |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 33 | services.nginx.virtualHosts."chat.example.org" = { |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 34 | enableACME = true; |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 35 | forceSSL = true; |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 36 | locations."/" = { |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 37 | proxyPass = "http://127.0.0.1:3000"; |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 38 | extraConfig = '' |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 39 | proxy_set_header Host $host; |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 40 | proxy_buffering off; # Keep event-stream updates timely. |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 41 | ''; |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 42 | }; |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 43 | }; |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 44 | security.acme.acceptTerms = true; |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 45 | security.acme.defaults.email = "you@example.org"; |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 46 | } |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 47 | ``` |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 48 | |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 49 | Replace the domain and ACME email, then rebuild your NixOS configuration as |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 50 | usual. Check `systemctl status malice` and visit `https://chat.example.org/`. |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 51 | The module stores the master key and sessions in `/var/lib/malice`; back up that |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 52 | entire directory together. Its `listenAddress`, `port`, `package`, and |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 53 | `openFirewall` options can be overridden. Do not expose the HTTP listener |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 54 | without a trusted TLS proxy. |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 55 | |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 56 | ## Client behavior |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 57 | |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 58 | The home list shows non-space rooms (channels); Matrix spaces have their own |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 59 | navigation and show their advertised child rooms. A Matrix homeserver is the |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 60 | network address used at sign-in, not a space. Sending a message returns a queued |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 61 | command, then the timeline shows whether delivery is queued, sending, confirmed |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 62 | by the server, or failed. Opening a room loads its timeline; scrolling up loads |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 63 | older history. Images and avatars are served through authenticated, bounded |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 64 | media endpoints; SVG content is not rendered. Link previews use URL text only |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 65 | and do not fetch third-party pages. |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 66 | |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 67 | The Security panel accepts incoming device-verification requests and supports |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 68 | emoji/number comparison, confirmation, rejection, and recovery-key entry. |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 69 | Recovery keys and encryption state are per Matrix account; preserve the local |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 70 | session data when migrating an installation. |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 71 | |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 72 | ## Development |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 73 | |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 74 | From the repository root: |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 75 | |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 76 | ```sh |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 77 | nix develop |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 78 | ``` |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 79 | |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 80 | The dev shell links `web/node_modules` from a nix build of `web/package-lock.json`, |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 81 | so there is no install step. After changing dependencies in `web/package.json`, |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 82 | refresh the lockfile with `npm --prefix web install --package-lock-only` and |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 83 | re-enter the shell. |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 84 | |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 85 | For live frontend changes, start these in separate terminals inside `nix develop`: |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 86 | |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 87 | ```sh |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 88 | MALICE_SESSION_COOKIE_SECURE=false cargo run --bin malice |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 89 | npm --prefix web run dev |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 90 | ``` |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 91 | |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 92 | Open `http://127.0.0.1:5173/`. Vite proxies `/api` to the Rust server on port |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 93 | 3000. The cookie override is for local HTTP development only. By default, |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 94 | Malice accepts HTTPS homeservers; set `MALICE_ALLOW_HTTP_HOMESERVER=true` only |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 95 | when testing against a trusted local HTTP homeserver. |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 96 | |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 97 | To serve the built frontend directly from Rust instead of using Vite: |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 98 | |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 99 | ```sh |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 100 | npm --prefix web run build |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 101 | MALICE_SESSION_COOKIE_SECURE=false cargo run --bin malice |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 102 | ``` |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 103 | |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 104 | Open `http://127.0.0.1:3000/`. Rust uses `web/dist` unless `MALICE_WEB_DIR` |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 105 | points at another built frontend directory. Local session data defaults to |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 106 | `local-data/`; set `MALICE_DATA_DIR` to use another directory. Keep its |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 107 | `master.key` and session files private. |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 108 | |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 109 | ### Storybook |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 110 | |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 111 | Component stories live next to their components as `web/src/**/*.stories.ts`. |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 112 | Storybook runs inside `nix develop`: |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 113 | |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 114 | ```sh |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 115 | cd web |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 116 | npm run storybook |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 117 | npm run build-storybook |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 118 | ``` |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 119 | |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 120 | Open `http://localhost:6006/`. While it runs, `@storybook/addon-mcp` serves an |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 121 | MCP endpoint at `http://localhost:6006/mcp`, which the repository `.mcp.json` |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 122 | registers for Claude Code in this project only. |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 123 | |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 124 | ## Testing |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 125 | |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 126 | In `nix develop`: |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 127 | |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 128 | ```sh |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 129 | cargo fmt -- --check |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 130 | cargo clippy --all-targets -- -D warnings |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 131 | cargo test --all-targets |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 132 | npm --prefix web run check |
dffd63b58af4 | atagen | 2026-09-28 09:10 UTC | 133 | npm --prefix web test |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 134 | npm --prefix web run build |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 135 | ``` |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 136 | |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 137 | The Rust integration test uses a mocked Matrix homeserver; it does not require |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 138 | live account credentials. To verify the deployable package and NixOS module: |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 139 | |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 140 | ```sh |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 141 | nix build .#server |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 142 | nix flake check |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 143 | ``` |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 144 | |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 145 | `nix build` creates `result/bin/malice` with the frontend bundled. On a NixOS |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 146 | host, `curl -fsS http://127.0.0.1:3000/api/health` checks the local backend; |
973f8fbdaf80 | atagen | 2026-09-28 09:10 UTC | 147 | opening the proxied URL checks the frontend and TLS setup. |