initial commit

This commit is contained in:
Hampus Kraft
2026-01-01 20:42:59 +00:00
commit 2f557eda8c
9029 changed files with 1490197 additions and 0 deletions

169
dev/.env.example Normal file
View File

@@ -0,0 +1,169 @@
NODE_ENV=development
FLUXER_API_PUBLIC_ENDPOINT=http://127.0.0.1:8088/api
FLUXER_API_CLIENT_ENDPOINT=
FLUXER_APP_ENDPOINT=http://localhost:8088
FLUXER_GATEWAY_ENDPOINT=ws://127.0.0.1:8088/gateway
FLUXER_MEDIA_ENDPOINT=http://127.0.0.1:8088/media
FLUXER_CDN_ENDPOINT=https://fluxerstatic.com
FLUXER_MARKETING_ENDPOINT=http://127.0.0.1:8088
FLUXER_ADMIN_ENDPOINT=http://127.0.0.1:8088
FLUXER_INVITE_ENDPOINT=http://fluxer.gg
FLUXER_GIFT_ENDPOINT=http://fluxer.gift
FLUXER_API_HOST=api:8080
FLUXER_API_PORT=8080
FLUXER_GATEWAY_WS_PORT=8080
FLUXER_GATEWAY_RPC_PORT=8081
FLUXER_MEDIA_PROXY_PORT=8080
FLUXER_ADMIN_PORT=8080
FLUXER_MARKETING_PORT=8080
FLUXER_GEOIP_PORT=8080
GEOIP_HOST=geoip:8080
FLUXER_PATH_GATEWAY=/gateway
FLUXER_PATH_ADMIN=/admin
FLUXER_PATH_MARKETING=/marketing
API_HOST=api:8080
FLUXER_GATEWAY_RPC_HOST=
FLUXER_GATEWAY_PUSH_ENABLED=false
FLUXER_GATEWAY_PUSH_USER_GUILD_SETTINGS_CACHE_MB=1024
FLUXER_GATEWAY_PUSH_SUBSCRIPTIONS_CACHE_MB=1024
FLUXER_GATEWAY_PUSH_BLOCKED_IDS_CACHE_MB=1024
FLUXER_GATEWAY_IDENTIFY_RATE_LIMIT_ENABLED=false
FLUXER_MEDIA_PROXY_HOST=
MEDIA_PROXY_ENDPOINT=
VAPID_PUBLIC_KEY=
VAPID_PRIVATE_KEY=
VAPID_EMAIL=support@fluxer.app
SUDO_MODE_SECRET=
PASSKEYS_ENABLED=true
PASSKEY_RP_NAME=Fluxer
PASSKEY_RP_ID=127.0.0.1
PASSKEY_ALLOWED_ORIGINS=http://127.0.0.1:8088,http://localhost:8088
ADMIN_OAUTH2_CLIENT_ID=
ADMIN_OAUTH2_CLIENT_SECRET=
ADMIN_OAUTH2_AUTO_CREATE=false
ADMIN_OAUTH2_REDIRECT_URI=http://127.0.0.1:8088/admin/oauth2_callback
RELEASE_CHANNEL=stable
DATABASE_URL=postgresql://postgres:postgres@postgres:5432/fluxer
REDIS_URL=redis://redis:6379
CASSANDRA_HOSTS=cassandra
CASSANDRA_KEYSPACE=fluxer
CASSANDRA_LOCAL_DC=datacenter1
CASSANDRA_USERNAME=cassandra
CASSANDRA_PASSWORD=cassandra
AWS_S3_ENDPOINT=http://minio:9000
AWS_ACCESS_KEY_ID=minioadmin
AWS_SECRET_ACCESS_KEY=minioadmin
AWS_S3_BUCKET_CDN=fluxer
AWS_S3_BUCKET_UPLOADS=fluxer-uploads
AWS_S3_BUCKET_DOWNLOADS=fluxer-downloads
AWS_S3_BUCKET_REPORTS=fluxer-reports
AWS_S3_BUCKET_HARVESTS=fluxer-harvests
R2_S3_ENDPOINT=http://minio:9000
R2_ACCESS_KEY_ID=minioadmin
R2_SECRET_ACCESS_KEY=minioadmin
METRICS_MODE=noop
CLICKHOUSE_URL=http://clickhouse:8123
CLICKHOUSE_DATABASE=fluxer_metrics
CLICKHOUSE_USER=fluxer
CLICKHOUSE_PASSWORD=fluxer_dev
ANOMALY_DETECTION_ENABLED=true
ANOMALY_WINDOW_SIZE=100
ANOMALY_ZSCORE_THRESHOLD=3.0
ANOMALY_CHECK_INTERVAL_SECS=60
ANOMALY_COOLDOWN_SECS=300
ANOMALY_ERROR_RATE_THRESHOLD=0.05
ALERT_WEBHOOK_URL=
EMAIL_ENABLED=false
SENDGRID_FROM_EMAIL=noreply@fluxer.app
SENDGRID_FROM_NAME=Fluxer
SENDGRID_API_KEY=
SENDGRID_WEBHOOK_PUBLIC_KEY=
SMS_ENABLED=false
TWILIO_ACCOUNT_SID=
TWILIO_AUTH_TOKEN=
TWILIO_VERIFY_SERVICE_SID=
CAPTCHA_ENABLED=true
CAPTCHA_PRIMARY_PROVIDER=turnstile
HCAPTCHA_SITE_KEY=10000000-ffff-ffff-ffff-000000000001
HCAPTCHA_PUBLIC_SITE_KEY=10000000-ffff-ffff-ffff-000000000001
HCAPTCHA_SECRET_KEY=0x0000000000000000000000000000000000000000
TURNSTILE_SITE_KEY=1x00000000000000000000AA
TURNSTILE_PUBLIC_SITE_KEY=1x00000000000000000000AA
TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA
SEARCH_ENABLED=true
MEILISEARCH_URL=http://meilisearch:7700
MEILISEARCH_API_KEY=masterKey
STRIPE_ENABLED=false
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
STRIPE_PRICE_ID_MONTHLY_USD=
STRIPE_PRICE_ID_MONTHLY_EUR=
STRIPE_PRICE_ID_YEARLY_USD=
STRIPE_PRICE_ID_YEARLY_EUR=
STRIPE_PRICE_ID_VISIONARY_USD=
STRIPE_PRICE_ID_VISIONARY_EUR=
STRIPE_PRICE_ID_GIFT_VISIONARY_USD=
STRIPE_PRICE_ID_GIFT_VISIONARY_EUR=
STRIPE_PRICE_ID_GIFT_1_MONTH_USD=
STRIPE_PRICE_ID_GIFT_1_MONTH_EUR=
STRIPE_PRICE_ID_GIFT_1_YEAR_USD=
STRIPE_PRICE_ID_GIFT_1_YEAR_EUR=
CLOUDFLARE_PURGE_ENABLED=false
CLOUDFLARE_ZONE_ID=
CLOUDFLARE_API_TOKEN=
CLOUDFLARE_TUNNEL_TOKEN=
VOICE_ENABLED=true
LIVEKIT_API_KEY=
LIVEKIT_API_SECRET=
LIVEKIT_WEBHOOK_URL=http://api:8080/webhooks/livekit
LIVEKIT_AUTO_CREATE_DUMMY_DATA=true
CLAMAV_ENABLED=false
CLAMAV_HOST=clamav
CLAMAV_PORT=3310
TENOR_API_KEY=
YOUTUBE_API_KEY=
IPINFO_TOKEN=
SECRET_KEY_BASE=
GATEWAY_RPC_SECRET=
GATEWAY_ADMIN_SECRET=
ERLANG_COOKIE=fluxer_dev_cookie
MEDIA_PROXY_SECRET_KEY=
SELF_HOSTED=false
AUTO_JOIN_INVITE_CODE=
FLUXER_VISIONARIES_GUILD_ID=
FLUXER_OPERATORS_GUILD_ID=
GIT_SHA=dev
BUILD_TIMESTAMP=

66
dev/Caddyfile.dev Normal file
View File

@@ -0,0 +1,66 @@
:8088 {
encode zstd gzip
@api path /api/*
handle @api {
handle_path /api/* {
reverse_proxy api:8080
}
}
@media path /media/*
handle @media {
handle_path /media/* {
reverse_proxy media:8080
}
}
@s3 path /s3/*
handle @s3 {
handle_path /s3/* {
reverse_proxy minio:9000
}
}
@admin path /admin /admin/*
handle @admin {
uri strip_prefix /admin
reverse_proxy admin:8080
}
@geoip path /geoip/*
handle @geoip {
handle_path /geoip/* {
reverse_proxy geoip:8080
}
}
@marketing path /marketing /marketing/*
handle @marketing {
uri strip_prefix /marketing
reverse_proxy marketing:8080
}
@gateway path /gateway /gateway/*
handle @gateway {
uri strip_prefix /gateway
reverse_proxy gateway:8080
}
@livekit path /livekit /livekit/*
handle @livekit {
handle_path /livekit/* {
reverse_proxy livekit:7880
}
}
@metrics path /metrics /metrics/*
handle @metrics {
uri strip_prefix /metrics
reverse_proxy metrics:8080
}
handle {
reverse_proxy host.docker.internal:3000
}
}

160
dev/compose.data.yaml Normal file
View File

@@ -0,0 +1,160 @@
services:
postgres:
image: postgres:17
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: fluxer
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- fluxer-shared
restart: on-failure
cassandra:
image: scylladb/scylla:latest
command: --smp 1 --memory 512M --overprovisioned 1 --developer-mode 1 --api-address 0.0.0.0
ports:
- '9042:9042'
volumes:
- scylla_data:/var/lib/scylla
networks:
- fluxer-shared
restart: on-failure
healthcheck:
test: ['CMD-SHELL', 'cqlsh -e "describe cluster"']
interval: 30s
timeout: 10s
retries: 5
start_period: 90s
redis:
image: valkey/valkey:latest
volumes:
- redis_data:/data
command: valkey-server --save 60 1 --loglevel warning
networks:
- fluxer-shared
restart: on-failure
minio:
image: minio/minio
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
volumes:
- minio_data:/data
networks:
- fluxer-shared
restart: on-failure
healthcheck:
test: ['CMD', 'mc', 'ready', 'local']
interval: 5s
timeout: 5s
retries: 5
minio-setup:
image: minio/mc
depends_on:
minio:
condition: service_healthy
entrypoint: >
/bin/sh -c "
mc alias set minio http://minio:9000 minioadmin minioadmin;
mc mb --ignore-existing minio/fluxer-metrics;
mc mb --ignore-existing minio/fluxer-uploads;
exit 0;
"
networks:
- fluxer-shared
restart: 'no'
clamav:
image: clamav/clamav:latest
volumes:
- clamav_data:/var/lib/clamav
environment:
CLAMAV_NO_FRESHCLAMD: 'false'
CLAMAV_NO_CLAMD: 'false'
CLAMAV_NO_MILTERD: 'true'
networks:
- fluxer-shared
restart: on-failure
healthcheck:
test: ['CMD', '/usr/local/bin/clamdcheck.sh']
interval: 30s
timeout: 10s
retries: 5
start_period: 300s
meilisearch:
image: getmeili/meilisearch:v1.25.0
volumes:
- meilisearch_data:/meili_data
environment:
MEILI_ENV: development
MEILI_MASTER_KEY: masterKey
networks:
- fluxer-shared
restart: on-failure
livekit:
image: livekit/livekit-server:latest
command: --config /etc/livekit.yaml --dev
env_file:
- ./.env
volumes:
- ./livekit.yaml:/etc/livekit.yaml:ro
ports:
- '7880:7880'
- '7882:7882/udp'
- '7999:7999/udp'
networks:
- fluxer-shared
restart: on-failure
clickhouse:
image: clickhouse/clickhouse-server:24.8
hostname: clickhouse
profiles:
- clickhouse
environment:
- CLICKHOUSE_DB=fluxer_metrics
- CLICKHOUSE_USER=fluxer
- CLICKHOUSE_PASSWORD=fluxer_dev
- CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1
volumes:
- clickhouse_data:/var/lib/clickhouse
- clickhouse_logs:/var/log/clickhouse-server
networks:
- fluxer-shared
ports:
- '8123:8123'
- '9000:9000'
restart: on-failure
healthcheck:
test: ['CMD', 'clickhouse-client', '--query', 'SELECT 1']
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
ulimits:
nofile:
soft: 262144
hard: 262144
networks:
fluxer-shared:
name: fluxer-shared
external: true
volumes:
postgres_data:
scylla_data:
redis_data:
minio_data:
clamav_data:
meilisearch_data:
clickhouse_data:
clickhouse_logs:

398
dev/compose.yaml Normal file
View File

@@ -0,0 +1,398 @@
services:
caddy:
image: caddy:2
ports:
- '8088:8088'
volumes:
- ./Caddyfile.dev:/etc/caddy/Caddyfile:ro
- ../fluxer_app/dist:/app/dist:ro
networks:
- fluxer-shared
extra_hosts:
- 'host.docker.internal:host-gateway'
restart: on-failure
cloudflared:
image: cloudflare/cloudflared:latest
command: tunnel --no-autoupdate run --token ${CLOUDFLARE_TUNNEL_TOKEN}
env_file:
- ./.env
networks:
- fluxer-shared
restart: on-failure
api:
image: node:24-bookworm-slim
working_dir: /workspace
command: bash -lc "corepack enable pnpm && CI=true pnpm install && npx tsx watch --clear-screen=false src/App.ts"
env_file:
- ./.env
environment:
- CI=true
- VAPID_PUBLIC_KEY=BJHAPp7Xg4oeN_D6-EVu0D-bDyPDwFFJiLn7CzkUjUvaG_F-keQGpA_-RiNugCosTPhhdvdrn4mEOh-_1Bt35V8
- FLUXER_METRICS_HOST=metrics:8080
volumes:
- ../fluxer_api:/workspace
- api_node_modules:/workspace/node_modules
networks:
- fluxer-shared
restart: on-failure
worker:
image: node:24-bookworm-slim
working_dir: /workspace
command: bash -lc "corepack enable pnpm && CI=true pnpm install && npm run dev:worker"
env_file:
- ./.env
environment:
- CI=true
- FLUXER_METRICS_HOST=metrics:8080
volumes:
- ../fluxer_api:/workspace
- api_node_modules:/workspace/node_modules
networks:
- fluxer-shared
restart: on-failure
depends_on:
- postgres
- redis
- cassandra
media:
build:
context: ../fluxer_media_proxy
dockerfile: Dockerfile
target: build
working_dir: /workspace
command: >
bash -lc "
corepack enable pnpm &&
CI=true pnpm install &&
pnpm dev
"
user: root
env_file:
- ./.env
environment:
- CI=true
- NODE_ENV=development
- FLUXER_METRICS_HOST=metrics:8080
volumes:
- ../fluxer_media_proxy:/workspace
- media_node_modules:/workspace/node_modules
networks:
- fluxer-shared
restart: on-failure
admin:
build:
context: ../fluxer_admin
dockerfile: Dockerfile.dev
working_dir: /workspace
env_file:
- ./.env
environment:
- PORT=8080
- APP_MODE=admin
- FLUXER_METRICS_HOST=metrics:8080
volumes:
- admin_build:/workspace/build
networks:
- fluxer-shared
restart: on-failure
develop:
watch:
- action: rebuild
path: ../fluxer_admin/src
- action: rebuild
path: ../fluxer_admin/tailwind.css
marketing:
build:
context: ../fluxer_marketing
dockerfile: Dockerfile.dev
working_dir: /workspace
env_file:
- ./.env
environment:
- PORT=8080
- FLUXER_METRICS_HOST=metrics:8080
volumes:
- marketing_build:/workspace/build
networks:
- fluxer-shared
restart: on-failure
develop:
watch:
- action: rebuild
path: ../fluxer_marketing/src
- action: rebuild
path: ../fluxer_marketing/tailwind.css
docs:
image: node:24-bookworm-slim
working_dir: /workspace
command: bash -lc "corepack enable pnpm && CI=true pnpm install && pnpm dev"
env_file:
- ./.env
environment:
- CI=true
- NODE_ENV=development
volumes:
- ../fluxer_docs:/workspace
- docs_node_modules:/workspace/node_modules
networks:
- fluxer-shared
restart: on-failure
geoip:
image: golang:1.25.5
working_dir: /workspace
command: bash -c "mkdir -p /data && if [ ! -f /data/ipinfo_lite.mmdb ] && [ -n \"$$IPINFO_TOKEN\" ]; then echo 'Downloading GeoIP database...'; curl -fsSL -o /data/ipinfo_lite.mmdb \"https://ipinfo.io/data/ipinfo_lite.mmdb?token=$$IPINFO_TOKEN\" && echo 'GeoIP database downloaded'; fi && go run ."
env_file:
- ./.env
volumes:
- ../fluxer_geoip:/workspace
- ./geoip_data:/data
networks:
- fluxer-shared
restart: on-failure
gateway:
image: erlang:28-slim
working_dir: /workspace
command: bash -c "apt-get update && apt-get install -y --no-install-recommends build-essential linux-libc-dev curl ca-certificates gettext-base git && curl -fsSL https://github.com/erlang/rebar3/releases/download/3.24.0/rebar3 -o /usr/local/bin/rebar3 && chmod +x /usr/local/bin/rebar3 && rebar3 compile && exec ./docker-entrypoint.sh"
hostname: gateway
env_file:
- ./.env
environment:
- RELEASE_NODE=fluxer_gateway@gateway
- LOGGER_LEVEL=debug
- CLUSTER_NAME=fluxer_gateway
- CLUSTER_DISCOVERY_DNS=gateway
- NODE_COOKIE=fluxer_dev_cookie
- VAPID_PUBLIC_KEY=BJHAPp7Xg4oeN_D6-EVu0D-bDyPDwFFJiLn7CzkUjUvaG_F-keQGpA_-RiNugCosTPhhdvdrn4mEOh-_1Bt35V8
- VAPID_PRIVATE_KEY=Ze8J4aSmwV5B77zz9NzTU_IdyFyR1hMiKaYF2G61Y-E
- VAPID_EMAIL=support@fluxer.app
- FLUXER_METRICS_HOST=metrics:8080
volumes:
- ../fluxer_gateway:/workspace
- gateway_build:/workspace/_build
networks:
- fluxer-shared
restart: on-failure
postgres:
image: postgres:17
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: fluxer
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- fluxer-shared
restart: on-failure
cassandra:
image: scylladb/scylla:latest
command: --smp 1 --memory 512M --overprovisioned 1 --developer-mode 1 --api-address 0.0.0.0
ports:
- '9042:9042'
volumes:
- scylla_data:/var/lib/scylla
networks:
- fluxer-shared
restart: on-failure
healthcheck:
test: ['CMD-SHELL', 'cqlsh -e "describe cluster"']
interval: 30s
timeout: 10s
retries: 5
start_period: 90s
redis:
image: valkey/valkey:latest
volumes:
- redis_data:/data
command: valkey-server --save 60 1 --loglevel warning
networks:
- fluxer-shared
restart: on-failure
minio:
image: minio/minio
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
volumes:
- minio_data:/data
networks:
- fluxer-shared
restart: on-failure
healthcheck:
test: ['CMD', 'mc', 'ready', 'local']
interval: 5s
timeout: 5s
retries: 5
minio-setup:
image: minio/mc
depends_on:
minio:
condition: service_healthy
entrypoint: >
/bin/sh -c "
mc alias set minio http://minio:9000 minioadmin minioadmin;
mc mb --ignore-existing minio/fluxer-metrics;
mc mb --ignore-existing minio/fluxer-uploads;
exit 0;
"
networks:
- fluxer-shared
restart: 'no'
clamav:
image: clamav/clamav:latest
volumes:
- clamav_data:/var/lib/clamav
environment:
CLAMAV_NO_FRESHCLAMD: 'false'
CLAMAV_NO_CLAMD: 'false'
CLAMAV_NO_MILTERD: 'true'
networks:
- fluxer-shared
restart: on-failure
healthcheck:
test: ['CMD', '/usr/local/bin/clamdcheck.sh']
interval: 30s
timeout: 10s
retries: 5
start_period: 300s
meilisearch:
image: getmeili/meilisearch:v1.25.0
volumes:
- meilisearch_data:/meili_data
environment:
MEILI_ENV: development
MEILI_MASTER_KEY: masterKey
networks:
- fluxer-shared
restart: on-failure
clickhouse:
image: clickhouse/clickhouse-server:24.8
hostname: clickhouse
profiles:
- clickhouse
environment:
- CLICKHOUSE_DB=fluxer_metrics
- CLICKHOUSE_USER=fluxer
- CLICKHOUSE_PASSWORD=fluxer_dev
- CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1
volumes:
- clickhouse_data:/var/lib/clickhouse
- clickhouse_logs:/var/log/clickhouse-server
networks:
- fluxer-shared
ports:
- '8123:8123'
- '9000:9000'
restart: on-failure
healthcheck:
test: ['CMD', 'clickhouse-client', '--query', 'SELECT 1']
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
ulimits:
nofile:
soft: 262144
hard: 262144
metrics:
build:
context: ../fluxer_metrics
dockerfile: Dockerfile
env_file:
- ./.env
environment:
- METRICS_PORT=8080
- METRICS_MODE=${METRICS_MODE:-noop}
- CLICKHOUSE_URL=http://clickhouse:8123
- CLICKHOUSE_DATABASE=fluxer_metrics
- CLICKHOUSE_USER=fluxer
- CLICKHOUSE_PASSWORD=fluxer_dev
- ANOMALY_DETECTION_ENABLED=true
- FLUXER_ADMIN_ENDPOINT=${FLUXER_ADMIN_ENDPOINT:-}
networks:
- fluxer-shared
restart: on-failure
metrics-clickhouse:
extends:
service: metrics
profiles:
- clickhouse
environment:
- METRICS_MODE=clickhouse
depends_on:
clickhouse:
condition: service_healthy
cassandra-migrate:
image: debian:bookworm-slim
command:
[
'bash',
'-lc',
'apt-get update && apt-get install -y dnsutils && sleep 30 && /cassandra-migrate --host cassandra --username cassandra --password cassandra up',
]
working_dir: /workspace
volumes:
- ../scripts/cassandra-migrate/target/release/cassandra-migrate:/cassandra-migrate
- ../fluxer_devops/cassandra/migrations:/workspace/fluxer_devops/cassandra/migrations
networks:
- fluxer-shared
depends_on:
cassandra:
condition: service_healthy
restart: 'no'
livekit:
image: livekit/livekit-server:latest
command: --config /etc/livekit.yaml --dev
env_file:
- ./.env
volumes:
- ./livekit.yaml:/etc/livekit.yaml:ro
ports:
- '7880:7880'
- '7882:7882/udp'
- '7999:7999/udp'
networks:
- fluxer-shared
restart: on-failure
networks:
fluxer-shared:
name: fluxer-shared
external: true
volumes:
postgres_data:
scylla_data:
redis_data:
minio_data:
clamav_data:
meilisearch_data:
clickhouse_data:
clickhouse_logs:
api_node_modules:
media_node_modules:
admin_build:
marketing_build:
gateway_build:
docs_node_modules:

34
dev/main.go Normal file
View File

@@ -0,0 +1,34 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
package main
import (
"fmt"
"os"
"fluxer.dev/dev/pkg/commands"
)
func main() {
if err := commands.NewRootCmd().Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

View File

@@ -0,0 +1,305 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
package commands
import (
"fmt"
"os"
"os/exec"
"strings"
"github.com/spf13/cobra"
"fluxer.dev/dev/pkg/integrations"
"fluxer.dev/dev/pkg/utils"
)
const (
defaultComposeFile = "dev/compose.yaml"
defaultEnvFile = "dev/.env"
)
// NewRootCmd creates the root command
func NewRootCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "devctl",
Short: "Fluxer development control tool",
Long: "Docker Compose wrapper and development utilities for Fluxer.",
}
cmd.AddCommand(
NewUpCmd(),
NewDownCmd(),
NewRestartCmd(),
NewLogsCmd(),
NewPsCmd(),
NewExecCmd(),
NewShellCmd(),
NewLivekitSyncCmd(),
NewGeoIPDownloadCmd(),
NewEnsureNetworkCmd(),
)
return cmd
}
// NewUpCmd starts services
func NewUpCmd() *cobra.Command {
var detach bool
var build bool
cmd := &cobra.Command{
Use: "up [services...]",
Short: "Start services",
Long: "Start all or specific services using docker compose",
RunE: func(cmd *cobra.Command, services []string) error {
args := []string{"--env-file", defaultEnvFile, "-f", defaultComposeFile, "up"}
if detach {
args = append(args, "-d")
}
if build {
args = append(args, "--build")
}
args = append(args, services...)
return runDockerCompose(args...)
},
}
cmd.Flags().BoolVarP(&detach, "detach", "d", true, "Run in background")
cmd.Flags().BoolVar(&build, "build", false, "Build images before starting")
return cmd
}
// NewDownCmd stops and removes containers
func NewDownCmd() *cobra.Command {
var volumes bool
cmd := &cobra.Command{
Use: "down",
Short: "Stop and remove containers",
RunE: func(cmd *cobra.Command, args []string) error {
dcArgs := []string{"--env-file", defaultEnvFile, "-f", defaultComposeFile, "down"}
if volumes {
dcArgs = append(dcArgs, "-v")
}
return runDockerCompose(dcArgs...)
},
}
cmd.Flags().BoolVarP(&volumes, "volumes", "v", false, "Remove volumes")
return cmd
}
// NewRestartCmd restarts services
func NewRestartCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "restart [services...]",
Short: "Restart services",
RunE: func(cmd *cobra.Command, services []string) error {
args := []string{"--env-file", defaultEnvFile, "-f", defaultComposeFile, "restart"}
args = append(args, services...)
return runDockerCompose(args...)
},
}
return cmd
}
// NewLogsCmd shows service logs
func NewLogsCmd() *cobra.Command {
var follow bool
var tail string
cmd := &cobra.Command{
Use: "logs [services...]",
Short: "Show service logs",
RunE: func(cmd *cobra.Command, services []string) error {
args := []string{"--env-file", defaultEnvFile, "-f", defaultComposeFile, "logs"}
if follow {
args = append(args, "-f")
}
if tail != "" {
args = append(args, "--tail", tail)
}
args = append(args, services...)
return runDockerCompose(args...)
},
}
cmd.Flags().BoolVarP(&follow, "follow", "f", true, "Follow log output")
cmd.Flags().StringVarP(&tail, "tail", "n", "100", "Number of lines to show from the end")
return cmd
}
// NewPsCmd lists containers
func NewPsCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "ps",
Short: "List containers",
RunE: func(cmd *cobra.Command, args []string) error {
return runDockerCompose("--env-file", defaultEnvFile, "-f", defaultComposeFile, "ps")
},
}
return cmd
}
// NewExecCmd executes a command in a running container
func NewExecCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "exec SERVICE COMMAND...",
Short: "Execute a command in a running container",
Args: cobra.MinimumNArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
dcArgs := []string{"--env-file", defaultEnvFile, "-f", defaultComposeFile, "exec"}
dcArgs = append(dcArgs, args...)
return runDockerCompose(dcArgs...)
},
}
return cmd
}
// NewShellCmd opens a shell in a container
func NewShellCmd() *cobra.Command {
var shell string
cmd := &cobra.Command{
Use: "sh SERVICE",
Short: "Open a shell in a container",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
service := args[0]
return runDockerCompose("--env-file", defaultEnvFile, "-f", defaultComposeFile, "exec", service, shell)
},
}
cmd.Flags().StringVar(&shell, "shell", "sh", "Shell to use (sh, bash, etc.)")
return cmd
}
// NewLivekitSyncCmd syncs LiveKit configuration
func NewLivekitSyncCmd() *cobra.Command {
var envPath string
var outputPath string
cmd := &cobra.Command{
Use: "livekit-sync",
Short: "Generate LiveKit configuration from environment variables",
RunE: func(cmd *cobra.Command, args []string) error {
env, err := utils.ParseEnvFile(envPath)
if err != nil {
return fmt.Errorf("failed to read env file: %w", err)
}
written, err := integrations.WriteLivekitFileFromEnv(outputPath, env)
if err != nil {
return err
}
if !written {
fmt.Println("⚠️ Voice/LiveKit is disabled - no config generated")
return nil
}
fmt.Printf("✅ LiveKit config written to %s\n", outputPath)
return nil
},
}
cmd.Flags().StringVarP(&envPath, "env", "e", defaultEnvFile, "Environment file path")
cmd.Flags().StringVarP(&outputPath, "output", "o", "dev/livekit.yaml", "Output path")
return cmd
}
// NewGeoIPDownloadCmd downloads GeoIP database
func NewGeoIPDownloadCmd() *cobra.Command {
var token string
var envPath string
cmd := &cobra.Command{
Use: "geoip-download",
Short: "Download GeoIP database from IPInfo",
RunE: func(cmd *cobra.Command, args []string) error {
return integrations.DownloadGeoIP(token, envPath)
},
}
cmd.Flags().StringVar(&token, "token", "", "IPInfo API token")
cmd.Flags().StringVarP(&envPath, "env", "e", defaultEnvFile, "Env file to read token from")
return cmd
}
// NewEnsureNetworkCmd ensures the Docker network exists
func NewEnsureNetworkCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "ensure-network",
Short: "Ensure the fluxer-shared Docker network exists",
RunE: func(cmd *cobra.Command, args []string) error {
return ensureNetwork()
},
}
return cmd
}
// runDockerCompose runs a docker compose command
func runDockerCompose(args ...string) error {
cmd := exec.Command("docker", append([]string{"compose"}, args...)...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
return cmd.Run()
}
// ensureNetwork ensures the fluxer-shared network exists
func ensureNetwork() error {
checkCmd := exec.Command("docker", "network", "ls", "--format", "{{.Name}}")
output, err := checkCmd.Output()
if err != nil {
return fmt.Errorf("failed to list networks: %w", err)
}
networks := strings.Split(strings.TrimSpace(string(output)), "\n")
for _, net := range networks {
if net == "fluxer-shared" {
fmt.Println("✅ fluxer-shared network already exists")
return nil
}
}
fmt.Println("Creating fluxer-shared network...")
createCmd := exec.Command("docker", "network", "create", "fluxer-shared")
createCmd.Stdout = os.Stdout
createCmd.Stderr = os.Stderr
if err := createCmd.Run(); err != nil {
return fmt.Errorf("failed to create network: %w", err)
}
fmt.Println("✅ fluxer-shared network created")
return nil
}

View File

@@ -0,0 +1,95 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
package integrations
import (
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"fluxer.dev/dev/pkg/utils"
)
const (
DefaultGeoIPDir = "dev/geoip"
DefaultGeoIPFile = "country_asn.mmdb"
)
// DownloadGeoIP downloads the GeoIP database from IPInfo
func DownloadGeoIP(tokenFlag, envPath string) error {
token := strings.TrimSpace(tokenFlag)
if token == "" {
token = strings.TrimSpace(os.Getenv("IPINFO_TOKEN"))
}
if token == "" && envPath != "" {
env, err := utils.ParseEnvFile(envPath)
if err == nil {
token = strings.TrimSpace(env["IPINFO_TOKEN"])
}
}
if token == "" {
return errors.New("IPInfo token required; provide via --token, IPINFO_TOKEN env var, or the config/env")
}
if err := os.MkdirAll(DefaultGeoIPDir, 0o755); err != nil {
return err
}
outPath := filepath.Join(DefaultGeoIPDir, DefaultGeoIPFile)
u := fmt.Sprintf("https://ipinfo.io/data/free/country_asn.mmdb?token=%s", url.QueryEscape(token))
fmt.Printf("Downloading GeoIP database to %s...\n", outPath)
resp, err := http.Get(u)
if err != nil {
return fmt.Errorf("failed to download GeoIP db: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
return fmt.Errorf("unexpected response (%d): %s", resp.StatusCode, strings.TrimSpace(string(body)))
}
f, err := os.Create(outPath)
if err != nil {
return err
}
defer f.Close()
n, err := io.Copy(f, resp.Body)
if err != nil {
return err
}
if n == 0 {
return errors.New("downloaded GeoIP file is empty; check your IPInfo token")
}
fmt.Printf("✅ GeoIP database downloaded (%d bytes).\n", n)
fmt.Println()
fmt.Println("If you're running a GeoIP service container, restart it so it picks up the new database.")
return nil
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
package integrations
import (
"fmt"
"os"
"path/filepath"
"strings"
)
// WriteLivekitFileFromEnv writes LiveKit configuration from environment variables
func WriteLivekitFileFromEnv(path string, env map[string]string) (bool, error) {
voiceEnabled := strings.ToLower(strings.TrimSpace(env["VOICE_ENABLED"])) == "true"
if !voiceEnabled {
return false, nil
}
apiKey := strings.TrimSpace(env["LIVEKIT_API_KEY"])
apiSecret := strings.TrimSpace(env["LIVEKIT_API_SECRET"])
webhookURL := strings.TrimSpace(env["LIVEKIT_WEBHOOK_URL"])
if apiKey == "" || apiSecret == "" || webhookURL == "" {
return false, nil
}
redisURL := strings.TrimSpace(env["REDIS_URL"])
redisAddr := strings.TrimPrefix(redisURL, "redis://")
if redisAddr == "" {
redisAddr = "redis:6379"
}
yaml := fmt.Sprintf(`port: 7880
redis:
address: "%s"
db: 0
keys:
"%s": "%s"
rtc:
tcp_port: 7881
webhook:
api_key: "%s"
urls:
- "%s"
room:
auto_create: true
max_participants: 100
empty_timeout: 300
development: true
`, redisAddr, apiKey, apiSecret, apiKey, webhookURL)
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return false, err
}
if err := os.WriteFile(path, []byte(yaml), 0o600); err != nil {
return false, err
}
return true, nil
}

154
dev/pkg/utils/helpers.go Normal file
View File

@@ -0,0 +1,154 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
package utils
import (
"bufio"
"crypto/rand"
"encoding/base32"
"fmt"
"net/url"
"os"
"strings"
"time"
)
// FileExists checks if a file exists at the given path
func FileExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
// BoolString converts a boolean to a string ("true" or "false")
func BoolString(b bool) string {
if b {
return "true"
}
return "false"
}
// FirstNonZeroInt returns the first non-zero integer from the provided values,
// or the default value if all are zero
func FirstNonZeroInt(values ...int) int {
for _, v := range values {
if v != 0 {
return v
}
}
return 0
}
// DefaultString returns the value if non-empty, otherwise returns the default
func DefaultString(value, defaultValue string) string {
if strings.TrimSpace(value) == "" {
return defaultValue
}
return value
}
// RandomString generates a random alphanumeric string of the given length
func RandomString(length int) string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
b := make([]byte, length)
if _, err := rand.Read(b); err != nil {
panic(err)
}
for i := range b {
b[i] = charset[int(b[i])%len(charset)]
}
return string(b)
}
// RandomBase32 generates a random base32-encoded string (without padding)
func RandomBase32(byteLength int) string {
b := make([]byte, byteLength)
if _, err := rand.Read(b); err != nil {
panic(err)
}
return strings.TrimRight(base32.StdEncoding.EncodeToString(b), "=")
}
// GenerateSnowflake generates a snowflake ID
// Format: timestamp (42 bits) + worker ID (10 bits) + sequence (12 bits)
func GenerateSnowflake() string {
const fluxerEpoch = 1420070400000
timestamp := time.Now().UnixMilli() - fluxerEpoch
workerID := int64(0)
sequence := int64(0)
snowflake := (timestamp << 22) | (workerID << 12) | sequence
return fmt.Sprintf("%d", snowflake)
}
// ValidateURL validates that a string is a valid URL
func ValidateURL(urlStr string) error {
if urlStr == "" {
return fmt.Errorf("URL cannot be empty")
}
parsedURL, err := url.Parse(urlStr)
if err != nil {
return fmt.Errorf("invalid URL: %w", err)
}
if parsedURL.Scheme == "" {
return fmt.Errorf("URL must have a scheme (http:// or https://)")
}
if parsedURL.Host == "" {
return fmt.Errorf("URL must have a host")
}
return nil
}
// ParseEnvFile parses a .env file and returns a map of key-value pairs
func ParseEnvFile(path string) (map[string]string, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
env := make(map[string]string)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue
}
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
if len(value) >= 2 {
if (value[0] == '"' && value[len(value)-1] == '"') ||
(value[0] == '\'' && value[len(value)-1] == '\'') {
value = value[1 : len(value)-1]
}
}
env[key] = value
}
return env, scanner.Err()
}

154
dev/setup.sh Executable file
View File

@@ -0,0 +1,154 @@
#!/usr/bin/env bash
# Copyright (C) 2026 Fluxer Contributors
#
# This file is part of Fluxer.
#
# Fluxer is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Fluxer is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
set -euo pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
check_command() {
if command -v "$1" &> /dev/null; then
echo -e "${GREEN}[OK]${NC} $1 is installed"
return 0
else
echo -e "${RED}[MISSING]${NC} $1 is not installed"
return 1
fi
}
check_node_version() {
if command -v node &> /dev/null; then
NODE_VERSION=$(node -v | sed 's/v//' | cut -d. -f1)
if [ "$NODE_VERSION" -ge 20 ]; then
echo -e "${GREEN}[OK]${NC} Node.js $(node -v) is installed"
return 0
else
echo -e "${YELLOW}[WARN]${NC} Node.js $(node -v) is installed, but v20+ is recommended"
return 0
fi
else
echo -e "${RED}[MISSING]${NC} Node.js is not installed"
return 1
fi
}
echo "=== Fluxer Development Setup ==="
echo ""
echo "Checking prerequisites..."
echo ""
MISSING=0
check_node_version || MISSING=1
check_command pnpm || MISSING=1
check_command docker || MISSING=1
check_command rustc || echo -e "${YELLOW}[OPTIONAL]${NC} Rust is not installed (needed for fluxer_app WASM modules)"
check_command wasm-pack || echo -e "${YELLOW}[OPTIONAL]${NC} wasm-pack is not installed (needed for fluxer_app WASM modules)"
check_command go || echo -e "${YELLOW}[OPTIONAL]${NC} Go is not installed (needed for fluxer_geoip)"
echo ""
if [ "$MISSING" -eq 1 ]; then
echo -e "${RED}Some required dependencies are missing. Please install them before continuing.${NC}"
exit 1
fi
echo "Creating Docker network if needed..."
if docker network inspect fluxer-shared &> /dev/null; then
echo -e "${GREEN}[OK]${NC} Docker network 'fluxer-shared' already exists"
else
docker network create fluxer-shared
echo -e "${GREEN}[OK]${NC} Created Docker network 'fluxer-shared'"
fi
echo ""
if [ ! -f "$SCRIPT_DIR/.env" ]; then
echo "Creating .env from .env.example..."
cp "$SCRIPT_DIR/.env.example" "$SCRIPT_DIR/.env"
echo -e "${GREEN}[OK]${NC} Created .env file"
else
echo -e "${GREEN}[OK]${NC} .env file already exists"
fi
mkdir -p "$SCRIPT_DIR/geoip_data"
if [ ! -f "$SCRIPT_DIR/geoip_data/ipinfo_lite.mmdb" ]; then
echo -e "${YELLOW}[INFO]${NC} GeoIP database not found."
echo " Set IPINFO_TOKEN in .env and run the geoip service to download it,"
echo " or manually download ipinfo_lite.mmdb to dev/geoip_data/"
else
echo -e "${GREEN}[OK]${NC} GeoIP database exists"
fi
if [ ! -f "$SCRIPT_DIR/livekit.yaml" ]; then
echo "Creating default livekit.yaml..."
cat > "$SCRIPT_DIR/livekit.yaml" << 'EOF'
port: 7880
redis:
address: 'redis:6379'
db: 0
keys:
'e1dG953yAoJPIsK1dzfTWAKMNE9gmnPL': 'rCtIICXHtAwSAJ4glb11jARcXCCgMTGvvTKLIlpD0pEoANLgjCNPD1Ysm8uWhQTB'
rtc:
tcp_port: 7881
webhook:
api_key: 'e1dG953yAoJPIsK1dzfTWAKMNE9gmnPL'
urls:
- 'http://api:8080/webhooks/livekit'
room:
auto_create: true
max_participants: 100
empty_timeout: 300
development: true
EOF
echo -e "${GREEN}[OK]${NC} Created livekit.yaml"
else
echo -e "${GREEN}[OK]${NC} livekit.yaml already exists"
fi
echo ""
echo "=== Setup Complete ==="
echo ""
echo "Next steps:"
echo ""
echo "1. Start data stores:"
echo " docker compose -f compose.data.yaml up -d"
echo ""
echo "2. Start app services:"
echo " docker compose up -d api worker media gateway admin marketing docs geoip metrics caddy"
echo ""
echo "3. Run the frontend on your host machine:"
echo " cd ../fluxer_app && pnpm install && pnpm dev"
echo ""
echo "4. Access the app at: http://localhost:8088"
echo ""
echo "Optional: Start Cloudflare tunnel:"
echo " docker compose up -d cloudflared"
echo ""