Self-hosting Fluxer: findings, bugs, and patches from a real deployment

(#1037) Feature Shipped self-hosting
I've been running a self-hosted Fluxer instance for about a week now, built from the refactor branch. (Commit https://github.com/fluxerapp/fluxer/commit/848269a4d4df7349acfc861ff926b17fe4c4a548 at the time I post this, edits will likely follow.) The self-hosting docs are still TBD, so I wanted to share what I found in case others go down this path. This covers build issues, runtime bugs (some of which likely affect the upstream instance too), and SSO integration. >[!NOTE] >The root causes I describe in these posts are my assumptions, they are more pointers for maintainers than well thought out fixes that could be submitted as PRs. I don't feel like I know enough of the architecture for that. Think of these as: "I had an issue, this is what I changed to fix it." Setup: Source build from refactor, behind Traefik reverse proxy, with Valkey, NATS, Meilisearch, and LiveKit. SQLite for the database. I'll post each issue as a separate reply below so they have their own threads. Here's the summary:

Build/Dockerfile issues

  1. Dockerfile missing 16+ workspace package.json COPYs — pnpm install fails
  2. .dockerignore excludes files needed at build time — **/build glob and emojis.json
  3. No wasm32 target for Rust — apt-installed rustc doesn't include it, wasm-pack fails
  4. ENTRYPOINT points to root workspace — no start script there
  5. rspack.config.mjs hardcodes CDN publicPath — self-hosted builds must serve bundles from origin
  6. CSP directives missing static_cdn_domain — emoji/fonts/icons blocked
  7. Admin CSS not built — missing build step in Dockerfile
  8. tsgo --noEmit fails — locale modules don't exist until lingui:compile runs

Runtime bugs (likely upstream too)

  1. Voice states missing from READY payload — guild_data.erl reads from guild process (always empty) instead of voice server process
  2. LiveKit webhooks not configured in livekit.example.yaml — join/leave events never reach the server
  3. VoiceReconciliationWorker is dead code — never instantiated

SSO/OIDC issues

  1. URLSearchParams body serialized as '{}' — token exchange sends empty body
  2. client_secret missing from token exchange — getSsoConfig() omits it by default
  3. Basic auth incompatible with some IdPs — Pocket ID ignores it when client_id is in body
  4. SSO users treated as "unclaimed" — no password = unclaimed in upstream logic
  5. SSO callback route not in auth guard allowlist — redirects to /login before processing

22 comments

Sign in with Fluxer to comment and vote.
Comment by @mgabor3141
RexSystem 1 vote originally by @mgabor3141 on GitHub OP

1: Dockerfile build fixes

The Dockerfile on refactor needs several fixes to produce a working build: Missing workspace package.json COPYs — 16 packages are missing from the deps stage, causing pnpm install to fail:
for pkg in date_utils elasticsearch_search geo_utils geoip i18n kv_client limits \
  list_utils locale markdown_parser media_proxy_utils meilisearch_search \
  mime_utils nats number_utils openapi time; do
  # Add before the fluxer_app COPY line:
  # COPY packages/${pkg}/package.json ./packages/${pkg}/
done
.dockerignore excludes build-time files:
  • **/build excludes fluxer_app/scripts/build/ (rspack config). Fix: add !fluxer_app/scripts/build
  • /fluxer_app/src/data/emojis.json is explicitly ignored but needed at build time. Fix: remove that line
No wasm32 target: The apt-installed rustc doesn't include wasm32-unknown-unknown, so wasm-pack fails. Fix: install via rustup instead:
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \
    --default-toolchain stable --profile minimal \
    && rustup target add wasm32-unknown-unknown \
    && cargo install wasm-pack
Wrong ENTRYPOINT: ENTRYPOINT ["pnpm", "start"] targets the root workspace which has no start script. Fix:
ENTRYPOINT ["pnpm", "--filter", "fluxer_server", "start"]
Missing admin CSS build step: Add after the marketing CSS build:
RUN pnpm --filter admin build:css
tsgo --noEmit fails at build time because lingui locale .mjs files don't exist yet. Fix: remove tsgo --noEmit && from fluxer_app/package.json build script. FLUXER_CONFIG not available in app-build stage: rspack reads config.json to derive endpoint URLs. Inject a minimal build-time config via build arg:
FROM deps AS app-build
ARG FLUXER_BUILD_CONFIG="{}"
RUN echo "$FLUXER_BUILD_CONFIG" > /tmp/fluxer-build-config.json
ENV FLUXER_CONFIG=/tmp/fluxer-build-config.json
Then pass it in compose:
build:
  args:
    FLUXER_BUILD_CONFIG: |
      { "domain": { "base_domain": "your.domain", "static_cdn_domain": "fluxerstatic.com", "public_scheme": "https", "public_port": 443 } }
Comment by @mgabor3141
RexSystem 1 vote originally by @mgabor3141 on GitHub OP

2: Self-hosted asset serving and CSP

Note
I ended up using the Fluxer CDN instead, so this point is not that relevant. See the CDN fix below.
Problem: rspack.config.mjs sets publicPath to ${CDN_ENDPOINT}/ in production. Since the CDN hosts the upstream builds, self-hosted instances must serve JS/CSS bundles from their own origin. Fix in rspack.config.mjs:
- publicPath: isProduction ? `${CDN_ENDPOINT}/` : '/'
+ publicPath: '/'
Static assets (emoji SVGs, fonts, icons) can still be served from fluxerstatic.com — they're not instance-specific. But the CSP directives in ServiceInitializer.tsx don't include static_cdn_domain, so browsers block them. Fix in fluxer_server/src/ServiceInitializer.tsx:
const staticCdnHost = new URL(requireValue(config.endpoints.static_cdn, 'endpoints.static_cdn')).origin;
Then add staticCdnHost to imgSrc, styleSrc, and fontSrc arrays.
Comment by @mgabor3141
RexSystem 1 vote originally by @mgabor3141 on GitHub OP

3: Voice states missing from READY payload (likely upstream bug)

Symptom: After connecting (or refreshing the page), the channel sidebar shows 0 users in voice channels. You only see who's in a voice channel after you join it yourself. The client logs confirm: Initialized voice states from connection open {guildCount: 2, totalVoiceStates: 0}. Root cause: guild_voice_server.erl is a separate process that holds the authoritative voice state. It's always started for every guild (guild.erl:76). All voice mutations (join, leave, confirm) go through resolve_voice_pid() which routes to this process. But guild_data:get_guild_state/2 — which builds the READY payload — runs inside the guild process and reads voice states from its own state:
VoiceStates = guild_voice:get_voice_states_list(State),
The guild process's voice_states map is always empty because all mutations go to the voice server process. The fallback handler in guild_voice_handler.erl is effectively dead code since resolve_voice_pid always finds the voice server via ETS. This is likely an upstream bug too — the voice server process was presumably split out from the guild process to reduce contention, but get_guild_state was never updated to fetch from the new location. Fix in fluxer_gateway/src/guild/guild_data.erl:
- VoiceStates = guild_voice:get_voice_states_list(State),
+ VoiceStates = fetch_voice_states_from_server(State),
Add the new function:
-spec fetch_voice_states_from_server(guild_state()) -> [map()].
fetch_voice_states_from_server(State) ->
    case maps:get(voice_server_pid, State, undefined) of
        Pid when is_pid(Pid) ->
            try gen_server:call(Pid, {get_voice_states_list}, 5000) of
                VoiceStates when is_list(VoiceStates) -> VoiceStates;
                _ -> []
            catch
                exit:{timeout, _} -> [];
                exit:{noproc, _} -> [];
                exit:{normal, _} -> []
            end;
        _ ->
            guild_voice:get_voice_states_list(State)
    end.
The voice server already has a {get_voice_states_list} handler (guild_voice_server.erl:203), so this just wires it up.
Comment by @mgabor3141
RexSystem 1 vote originally by @mgabor3141 on GitHub OP

4: LiveKit webhooks not configured

Symptom: Users appear stuck in voice channels after leaving. Rejoining shows duplicate entries. The Fluxer server logs show zero webhook events. Root cause: config/livekit.example.yaml has no webhook section. Without it, LiveKit never sends participant_joined / participant_left / room_finished events to the Fluxer server. The server has a full LiveKitWebhookService that handles these events and updates gateway state, but it never receives anything. Fix — add to livekit.yaml:
webhook:
  api_key: '<your_livekit_api_key>'
  urls:
    - https://your.domain/api/webhooks/livekit
The api_key must match one of the keys in the keys: section. The URL must be reachable from the LiveKit container. Related: VoiceReconciliationWorker in packages/api/src/voice/VoiceReconciliationWorker.tsx is designed to be a safety net that periodically cross-references LiveKit participants with gateway voice states and cleans up ghosts. However, it's never imported or instantiated anywhere in WorkerDependencies.tsx — it's dead code. Wiring it up would add resilience against missed webhooks.
Comment by @mgabor3141
RexSystem 1 vote originally by @mgabor3141 on GitHub OP

5: SSO/OIDC token exchange broken (3 compounding bugs)

Setting up SSO with an OIDC provider (tested with Pocket ID) fails at the token exchange step. Three bugs compound: 1. URLSearchParams body serialized as '{}' packages/http_client/src/HttpClientRequestInternals.tsx — resolveRequestBody() JSON-stringifies non-string bodies. JSON.stringify(new URLSearchParams(...)) produces '{}', so the token exchange POST body is empty.
+ if (body instanceof URLSearchParams) {
+     return body.toString();
+ }
  if (typeof body === 'string') {
2. client_secret not included in token exchange packages/api/src/auth/services/SsoService.tsx calls getSsoConfig() without { includeSecret: true }, so clientSecret is always undefined.
- await this.instanceConfigRepository.getSsoConfig()
+ await this.instanceConfigRepository.getSsoConfig({ includeSecret: true })
3. Basic auth header ignored by some IdPs Upstream sends client_id in the POST body and client_secret via Authorization: Basic header. Some IdPs (e.g. Pocket ID) only parse Basic auth when client_id is absent from the body. Since it's present, the Basic auth is ignored entirely. Fix: send client_secret in the POST body instead:
- const encoded = Buffer.from(`${config.clientId}:${config.clientSecret}`, 'utf8').toString('base64');
- headers['Authorization'] = `Basic ${encoded}`;
+ body.set('client_secret', config.clientSecret);
Additional SSO fixes needed:
  • SSO users treated as "unclaimed" — User.tsx considers users with passwordHash === null && !isBot as unclaimed. SSO-provisioned users have no password but are legitimate. Fix: add && !this._traits.has('sso') to the condition.
  • SSO callback route blocked by auth guard — RootComponent.tsx redirects unauthenticated users to /login before the callback page at /auth/sso/callback can process the authorization code. Fix: add the path to the allowlist alongside the login route.
Comment by @mgabor3141
RexSystem 1 vote originally by @mgabor3141 on GitHub OP

6: Config architecture and gotchas for self-hosters

Erlang gateway can't use env overrides. The Node.js server supports FLUXER_CONFIG__ prefixed env vars for config overrides (double-underscore separated path). The Erlang gateway reads config.json directly with no env substitution. Any config value the gateway needs must be in the JSON file — env overrides won't reach it. Practical consequence: if you set a NATS auth token via env vars, the Node.js server authenticates fine but the gateway silently fails to connect. Easiest workaround is to run NATS without auth — it's only on the internal Docker network anyway. Git LFS will break your clone. The repo uses LFS for static assets (fluxer_static/). If you run git lfs install (even accidentally), it enables the smudge filter globally and every subsequent git operation tries to download from a private LFS store and hangs indefinitely. Clone with:
GIT_LFS_SKIP_SMUDGE=1 git clone --depth 1 --branch refactor https://github.com/fluxerapp/fluxer.git
Or pass -c filter.lfs.smudge= -c filter.lfs.required=false to every git command. Related: the upstream Dockerfile has a COPY fluxer_static/ ... line that copies LFS pointer files (not actual assets). Remove it — static assets (emoji SVGs, fonts, icons) are served from fluxerstatic.com CDN and aren't instance-specific. SQLite path must be absolute. pnpm changes the working directory, so a relative sqlite_path resolves to the wrong location and you get a silent empty database. Use the full container path:
"sqlite_path": "/usr/src/app/data/fluxer.db"
First registered user gets admin. The first account created automatically receives wildcard admin ACLs (*). The admin panel is at /admin. Once SSO is configured and ready, the LocalAuthMiddleware blocks the /auth/register endpoint entirely (SSO_REQUIRED), so new users can only be provisioned via your IdP. LiveKit needs special handling. See Reply 7 for details, but the short version: don't use Docker port mappings for the UDP range (it can crash your system), use host networking instead, and plan for dynamic IP if you're on a residential connection.
Comment by @mgabor3141
RexSystem 1 vote originally by @mgabor3141 on GitHub OP

7: LiveKit deployment — port mapping crash, host networking, and dynamic IP

Docker port mapping crash with UDP ranges

The upstream compose.yaml maps LiveKit ports like this:
ports:
  - '7881:7881'
  - '3478:3478/udp'
  - '50000-50100:50000-50100/udp'
That 50000-50100 range creates 101 individual iptables/nftables rules in the Docker proxy. On my system this caused the entire Docker networking stack to hang — all containers lost connectivity and the host became unresponsive. Had to hard reboot. This will hit anyone whose Docker setup uses iptables-based port mapping (the default).

Solution: host networking

LiveKit works much better with network_mode: host. It binds directly to the host's interfaces, avoids the port mapping overhead entirely, and also solves NAT hairpinning issues. With Docker's bridge networking, WebRTC clients on the same LAN as the server couldn't connect — the ICE candidates advertised the external IP, but hairpin NAT back through the router failed. Host networking eliminates this since LiveKit sees the real network interfaces and can advertise both internal and external IPs correctly.
fluxer-livekit:
  image: livekit/livekit-server:v1.9.11
  container_name: fluxer-livekit
  restart: unless-stopped
  network_mode: host
  command: ["--config", "/etc/livekit/livekit.yaml"]
  volumes:
    - ./config/livekit.yaml:/etc/livekit/livekit.yaml:ro
In the LiveKit config, restrict which interfaces/subnets it listens on so it picks up the right IP:
rtc:
  tcp_port: 7881
  udp_port: 7882
  use_external_ip: true
  node_ip: <your-public-ip>
  interfaces:
    includes:
      - br0          # your LAN-facing interface
  ips:
    includes:
      - 192.168.0.0/24   # your LAN subnet

turn:
  enabled: true
  udp_port: 3479   # remapped from 3478 to avoid conflicts (e.g. Nextcloud Talk)
Since it's on the host network, the LiveKit signaling endpoint needs to go through Traefik using the host IP rather than the Docker service name. The wss:// signaling URL in Fluxer config points to a Traefik route that proxies to 127.0.0.1:7880.

Dynamic IP and the node_ip problem

node_ip in the LiveKit config tells clients which IP to send media to. If you're on a residential connection with a dynamic IP, this value goes stale when your IP changes. LiveKit reads the config once at startup and has no mechanism to detect IP changes. My workaround uses three scripts: Entrypoint — resolves a DDNS hostname to an IP at startup:
#!/bin/sh
RESOLVED_IP=$(getent hosts "$LIVEKIT_NODE_HOSTNAME" | awk '{print $1; exit}')
echo "$RESOLVED_IP" > /tmp/node_ip
sed "s|node_ip:.*|node_ip: $RESOLVED_IP|" /etc/livekit/livekit.yaml > /tmp/livekit.yaml
exec /livekit-server --config /tmp/livekit.yaml
Healthcheck — detects when the IP has changed since startup:
#!/bin/sh
wget -qO- http://127.0.0.1:7880 > /dev/null 2>&1 || exit 1

if [ -n "$LIVEKIT_NODE_HOSTNAME" ] && [ -f /tmp/node_ip ]; then
  CURRENT_IP=$(getent hosts "$LIVEKIT_NODE_HOSTNAME" | awk '{print $1; exit}')
  STARTUP_IP=$(cat /tmp/node_ip)
  if [ -n "$CURRENT_IP" ] && [ "$CURRENT_IP" != "$STARTUP_IP" ]; then
    echo "IP changed: $STARTUP_IP -> $CURRENT_IP"
    exit 1
  fi
fi
Autoheal — a container like willfarrell/autoheal watches for unhealthy containers and restarts them. When the healthcheck fails due to IP change, autoheal restarts LiveKit, which re-runs the entrypoint and picks up the new IP.
labels:
  - autoheal=true
healthcheck:
  test: ["CMD-SHELL", "/bin/sh /etc/livekit/healthcheck.sh"]
  interval: 30s
  timeout: 5s
  retries: 3
This is a hack — there's a window between IP change and restart where voice is broken. A proper fix would be LiveKit supporting periodic IP re-resolution or a reload signal, but this works well enough for a home setup where IP changes are infrequent.

Port forwarding summary

With host networking, forward these on your router to the Fluxer host:
PortProtocolPurpose
3479UDPTURN/STUN
7881TCPICE TCP fallback
7882UDPPrimary media (RTP/RTCP)
Note: the upstream default uses 50000-50100/udp as the RTP range. With a single udp_port instead, LiveKit multiplexes all media over one port. This is fine for a small instance and avoids the port range mapping issue entirely.
Comment by @mgabor3141
RexSystem 1 vote edited originally by @mgabor3141 on GitHub OP
I got the desktop app (fluxer_desktop) working with a self-hosted instance, including SSO login via an external OIDC provider with passkey support. The short version: point the desktop app at your instance via settings.json, fix a few build issues in fluxer_desktop, and add a fluxer:// deep link flow so the OIDC callback can get back from the browser to the Electron app. Full writeup with all the details is on issue #458. Branch with all changes: feat/desktop-custom-instance-url-clean.
Comment by @yak3d
RexSystem 1 vote edited originally by @yak3d on GitHub 2 replies
@mgabor3141 are all of these going to be converted into issues? Do you need help doing that? Would love to contribute into getting this to be self hosting friendly. I recently deployed it onto Hetzner with Terraform, and a few code changes needed to be made especially around CDN usage.
Comment by @mgabor3141
RexSystem 1 vote originally by @mgabor3141 on GitHub OP
I don't think that would help the maintainers just yet. I'd say let's let them cook with the refactor for now. PRs for external contributors are intentionally disabled too. I'm just putting these somewhere so that it's saved, but I feel like it is too soon for taking these any further. For all we know these might already be fixed on someone's local branch. I mean you are welcome to help, I'm just trying to be conscious of the fact that the maintainers will likely tell us what their plans are and how we can help very soon.
Comment by @yak3d
RexSystem 1 vote originally by @yak3d on GitHub
Yeah I was kind of thinking that as well, good call.
Comment by @cootason
RexSystem 1 vote originally by @cootason on GitHub 1 reply
I managed to get mine working but needed a lot of work because of my limitations stuck behind a isp firewall that blocks everything going out so ended up going the cloudflare tunnel route. and renting a playit.gg tunnel / port for my video and voice calling. after that I was able to muck around and run a lot of modifications test out OBS custom livestream channels with WHIP Low latency integration. added a public discovery page and im currently editing the category and game name into those custom livestream channels. I did it for fun to see what was possible each channel got its own unique token and ingest. so youd have the standard voice rooms with video calls and screenshare then a obs livestream channel. project has a lot of potential. im sure on a much larger scale that would be mind blowing having community and livestreaming in one place. where it matters. used codex to assist a lot. had my old man streaming a game using the video call mode so for the most part this application is brilliant. cant wait to see where fluxer ends up.
Comment by @cootason
RexSystem 1 vote originally by @cootason on GitHub
I changed it from using channels for the live-streams to creating a profile page for each account with its own obs injest and token, communities has a fixed community live-streams section which shows all the streams active by community members, taking live-streams from being blasted on 100 custom broadcast channels directly to the user profile. making management a lot easier. community still has the same discord style channels etc calling and screen share. just something I'm experimenting with. also built and added a custom live-streaming stack in a creator page on the rail menu. this change allows a user to be a member of multiple communities and there livestreams will appear in any guild or group they are in. subject to a toggle if there are specific groups you dont want your livestream to appear in, and theres a live rail menu so users who have opted to have there stream available for everyone to watch can be viewed from the live page as well.
Comment by @Mar0xy
RexSystem 1 vote originally by @Mar0xy on GitHub 2 replies
Gonna leave this here for anyone interested in setting up Cassandra as I had to experiment with this for 3 hours

Setup Guide

1. Add Service to Compose

Here we will use the bitnamilegacy image as all the other cassandra images require you to directly have a full on cassandra config to get them to work.
cassandra:
    image: bitnamilegacy/cassandra:latest
    container_name: cassandra
    ports:
      - '7000:7000'
      - '7199:7199'
      - '9042:9042'
    environment:
      CASSANDRA_DATACENTER: dc1
      CASSANDRA_ENABLE_RPC: true
      CASSANDRA_USER: username
      CASSANDRA_PASSWORD: password
      CASSANDRA_PASSWORD_SEEDER: yes
    volumes:
      - cassandra_data:/bitnami/cassandra
also add the volume to the volumes section as cassandra_data:

2. Configure cassandra in the Fluxer Config

"database": {
    "backend": "cassandra",
    "cassandra": {
      "hosts": ["cassandra"],
      "local_dc": "datacenter1",
      "username": "user",
      "password": "password",
      "keyspace": "fluxer"
    }
  },

3. Prepare Cassandra

Start just the cassandra instance and wait for it to be fully booted (takes about 30-40 seconds) then run the following commands:
docker compose exec -it cassandra cqlsh -u user -p password
CREATE KEYSPACE fluxer WITH replication = { 'class': 'SimpleStrategy', 'replication_factor': 1 };
this sets up the keyspace used in the config with basically the simplest of replication options

4. Run Migrations

Now we will get to the hard part as the migration script is not included in the docker image you will have to run all migrations yourself one by one in the shell by going through each file in fluxer_devops/cassandra/migrations

5. Start Fluxer

After all this is done fluxer should be able to start without issues

Caveats

Cassandra tends to eat a lot of RAM so make sure you either host it on a separate machine if possible or you have a beefy server otherwise it will Exited with code 137 while fluxer is up and you get the dreaded loading/splash screen

Benefits

You no longer have to deal with the KV SQLite file meaning you can actually easily modify the table entries and etc like adding visionary to a user by running this query
UPDATE fluxer.users SET premium_type = 2, premium_lifetime_sequence = visionarynumber, premium_since = toTimestamp(now()), premium_until = null, premium_billing_cycle = null, premium_will_cancel = false WHERE user_id = userid;

RAM usage improvement

OOTB Cassandra takes up as much RAM as it can (in my case it was 8GB) you can limit the usage by adding this into the compose: environments section on the cassandra service:
JAVA_TOOL_OPTIONS: -Xmn2G -XX:+UseG1GC -Xms2G -Xmx2G
new deploy section in the service:
    deploy:
      resources:
        limits:
          memory: "2GB"
This should limit cassandra to 2GB of ram with the downside of some API stuff taking a bit longer to load
Comment by @mgabor3141
RexSystem 1 vote originally by @mgabor3141 on GitHub OP
Have you noticed meaningful difference over sqlite? How many users?
Comment by @Mar0xy
RexSystem 1 vote originally by @Mar0xy on GitHub
There are no real differences between cassandra and sqlite besides the fact that cassandra is better for redundancy/replication/scaling and that it isn't KV based like mentioned previously which makes managing the DB directly much easier. In terms of users my instance only has me and a friend as well as 2 bot accounts and it has been working fine so far after I applied a few more changes like setting this env variable CASSANDRA_CFG_YAML_DISK_ACCESS_MODE: mmap_index_only
Comment by @mgabor3141
RexSystem 1 vote originally by @mgabor3141 on GitHub OP

9: S3 data_dir resolves relative to fluxer_server/, not the project root — uploads silently written to ephemeral container layer

Symptom: All avatars, banners, and attachments return 404 after a container rebuild. The S3 bucket directories exist on the persistent volume but are empty. Root cause: The S3 service's data_dir defaults to ./data/s3 (a relative path). Since the entrypoint is pnpm --filter fluxer_server start, pnpm sets the cwd to /usr/src/app/fluxer_server/. This means ./data/s3 resolves to /usr/src/app/fluxer_server/data/s3/ — inside the container's writable layer — instead of /usr/src/app/data/s3/ on the bind mount. Uploads succeed and the app works fine, but the files are silently written to the ephemeral container filesystem. They survive restarts but are permanently lost on container rebuild (docker compose up --build, image update, etc.). This is the same class of bug as the sqlite_path issue — the Dockerfile's ENTRYPOINT uses pnpm --filter, which changes the working directory, breaking all relative path defaults. Fix: Set an absolute path for services.s3.data_dir in config.json:
"services": {
    "s3": {
        "data_dir": "/usr/src/app/data/s3"
    },
    ...
}
Note: The only other relative-path default in the schema is services.queue.data_dir (./data/queue), but it appears unused in the self-hosted setup since the queue is backed by NATS JetStream.
Comment by @Mar0xy
RexSystem 1 vote originally by @Mar0xy on GitHub

Broken Klipy Categories/Gif Picker

There is currently an issue where in the packages/api/src/klipy/KlipyService.tsx file it tries to just grab the gif as a webm but this can fail when the webm array is missing or if the webm array is missing the url/dims values causing the gif picker for example to not show the categories. This can be fixed by adding a fallback to make it use the gif array instead if either of the two things mentioned above are missing on lines 332-335 here is a the return snippet fully for easy implementation
return {
	id: normalizedSlug,
	title: input.title,
	url: normalizedUrl,
	src: input.media_formats.webm?.url ?? input.media_formats.gif.url,
	proxy_src: this.mediaService.getExternalMediaProxyURL(input.media_formats.webm?.url ?? input.media_formats.gif.url),
	width: input.media_formats.webm?.dims?.[0] ?? input.media_formats.gif.dims[0],
	height: input.media_formats.webm?.dims?.[1] ?? input.media_formats.gif.dims[1],
};
Comment by @Takalele
RexSystem 1 vote originally by @Takalele on GitHub
if anyone is searching for how to get the Progressive Web App (PWA) working, just replace const staticCdnEndpoint = normalizeEndpoint(staticCdnEndpointRaw); with const staticCdnEndpoint = normalizeEndpoint(staticCdnEndpointRaw) || 'https://fluxerstatic.com'; (2 times, line 28 and 78) in the file fluxer_app/scripts/build/rspack/static-files.mjs, then rebuild and redeploy the container.