refactor(geoip): reconcile geoip system (#31)

This commit is contained in:
hampus-fluxer
2026-01-05 23:19:05 +01:00
committed by GitHub
parent 5d047b2856
commit 2e007b5076
86 changed files with 982 additions and 2648 deletions

View File

@@ -18,8 +18,6 @@ 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
@@ -152,7 +150,6 @@ CLAMAV_PORT=3310
TENOR_API_KEY=
YOUTUBE_API_KEY=
IPINFO_TOKEN=
SECRET_KEY_BASE=
GATEWAY_RPC_SECRET=

View File

@@ -28,13 +28,6 @@
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

View File

@@ -145,19 +145,6 @@ services:
- 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

View File

@@ -1,34 +0,0 @@
/*
* 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

@@ -1,305 +0,0 @@
/*
* 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

@@ -1,95 +0,0 @@
/*
* 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

@@ -1,82 +0,0 @@
/*
* 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
}

View File

@@ -1,154 +0,0 @@
/*
* 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()
}

View File

@@ -1,154 +0,0 @@
#!/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 ""