diff --git a/README.md b/README.md index 12bda1a..ce3be65 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,8 @@ A Discord style chat app. Servers with text and voice channels, direct messages, - SignalR for chat, presence and call signaling (ringing, accepting, hanging up). - LiveKit's server SDK issues the access tokens clients use to join a voice/video room. - FluentValidation on the request DTOs, wired in through a minimal API filter. -- Storage goes through an S3 compatible bucket, reusing the SeaweedFS instance from the Musify stack instead of running a second one. -- Zitadel for OIDC/OAuth2 login, also shared with Musify so a single account works across both apps. +- Storage goes through an S3 compatible bucket, reusing the shared SeaweedFS instance from the Infrastructure stack instead of running a second one. +- Zitadel for OIDC/OAuth2 login, also shared through Infrastructure so a single account works across every project. **Frontend**: React 19 with TypeScript, built with Vite. - Zustand for the small bits of client state: current view, active call, toasts, presence. @@ -31,7 +31,10 @@ A Discord style chat app. Servers with text and voice channels, direct messages, - `@livekit/components-react` plus `livekit-client` for the actual call UI and media handling. - Plain CSS, no component library. -**Infrastructure**: Docker Compose for local development. Postgres and LiveKit run in containers; Zitadel and the S3 storage are the ones already running for Musify, so this stack only adds what's specific to Ping. +**Infrastructure**: Docker Compose for local development. LiveKit runs in a +container here; Postgres, Zitadel and the S3 storage are shared services +provided by the separate `Infrastructure` git repository, used by other +projects too, so this stack only adds what's specific to Ping. ## Project layout @@ -43,13 +46,22 @@ backend/deploy/ Compose files and env templates for local development ## Running it locally -The backend expects Musify's Zitadel and SeaweedFS containers to already be running, since it reuses them instead of standing up its own copy. With that in place: +The backend expects the shared `Infrastructure` stack to already be running +(see its own README for the exact command), since it reuses its +Postgres/Zitadel/SeaweedFS instead of standing up its own copies. With that +in place: ```bash cp backend/deploy/.env.example backend/deploy/.env -# edit backend/deploy/.env to point at your Musify checkout and fill in the real S3 keys +# edit backend/deploy/.env if your Infrastructure checkout isn't where +# ZITADEL_ADMIN_PAT_DIR assumes, or its S3 keys differ from the defaults docker compose -f backend/deploy/compose.yml -f backend/deploy/compose.dev.yml up -d ``` -That starts Postgres, LiveKit and the one-shot setup jobs (migrations, the Zitadel project for Ping, the storage bucket). From there, run the API and the frontend from your IDE or with `dotnet run` / `npm run dev` against them. See [backend/deploy/README.md](backend/deploy/README.md) for the full details, including how to also build and run the API and frontend inside Docker. +That starts LiveKit and the one-shot setup jobs (waiting for Postgres, +migrations, the Zitadel project for Ping, the storage bucket). From there, +run the API and the frontend from your IDE or with `dotnet run` / `npm run +dev` against them. See [backend/deploy/README.md](backend/deploy/README.md) +for the full details, including how to also build and run the API and +frontend inside Docker. diff --git a/backend/deploy/.env.example b/backend/deploy/.env.example index 70246ce..23e8ac5 100644 --- a/backend/deploy/.env.example +++ b/backend/deploy/.env.example @@ -1,29 +1,32 @@ # Development configuration for deploy/compose.yml + compose.dev.yml. -# Copy to deploy/.env and adjust. Not versioned. +# Copy to deploy/.env and adjust. # # cp deploy/.env.example deploy/.env +# +# Requires ../../../Infrastructure running first (Postgres, Zitadel, +# SeaweedFS). See ../../../Infrastructure/README.md. Everything marked +# "must match Infrastructure's .env" below has to be the same value as the +# one in that file. DOTNET_ENVIRONMENT=Development # --- Public URLs ------------------------------------------------------------- PUBLIC_WEB_URL=http://localhost:3100 PUBLIC_API_URL=http://localhost:5211 - -# Ping reuses the Zitadel and SeaweedFS instances already running for Musify instead of -# spinning up its own, so this must match Musify's PUBLIC_AUTH_URL exactly. PUBLIC_AUTH_URL=http://host.docker.internal:8080 -# --- PostgreSQL (Ping's own database — Zitadel keeps using Musify's) -------- +# --- PostgreSQL (Infrastructure's shared instance, Ping's own database) ----- +# POSTGRES_USER / POSTGRES_PASSWORD must match Infrastructure's .env. +POSTGRES_HOST=host.docker.internal POSTGRES_USER=postgres POSTGRES_PASSWORD=postgres POSTGRES_DB=ping_db -POSTGRES_HOST_PORT=59010 -# --- Zitadel (shared with Musify) -------------------------------------------- -# zitadel-init needs Musify's admin service-account PAT to create Ping's own project/apps -# in that same Zitadel instance. Point this at Musify's deploy/zitadel/.output folder — -# adjust if your Musify checkout lives somewhere else relative to this one. -ZITADEL_ADMIN_PAT_DIR=../../../Musify/MusifyM/deploy/zitadel/.output +# --- Zitadel (OIDC, shared instance) ----------------------------------------- +# Path to the admin service-account PAT Infrastructure's Zitadel writes on +# init. Adjust if your Infrastructure checkout lives somewhere else relative +# to this one. +ZITADEL_ADMIN_PAT_DIR=../../../Infrastructure/zitadel/.output ZITADEL_DEV_MODE=true # OIDC clients: the zitadel-init one-shot service (see compose.yml) fills these in @@ -31,9 +34,8 @@ ZITADEL_DEV_MODE=true AUTH_CLIENT_ID= WEB_CLIENT_ID= -# --- SeaweedFS (S3, shared with Musify) -------------------------------------- -# Same SeaweedFS instance as Musify, so these must be the REAL values from Musify's own -# deploy/.env (S3_ACCESS_KEY / S3_SECRET_KEY) — not the placeholders below. +# --- SeaweedFS (S3, shared instance, Ping's own bucket) ---------------------- +# S3_ACCESS_KEY / S3_SECRET_KEY must match Infrastructure's .env. S3_ACCESS_KEY=admin_access_key S3_SECRET_KEY=admin_secret_key S3_BUCKET=ping-storage @@ -41,9 +43,7 @@ S3_PUBLIC_URL=http://host.docker.internal:8333 S3_USE_HTTP=true # --- LiveKit (voice/video SFU) ------------------------------------------------ -# Overrides --dev's built-in "devkey/secret" pair (see compose.yml's LIVEKIT_KEYS): the -# LiveKit .NET server SDK rejects secrets shorter than 256 bits. Generate your own with: -# $b = [byte[]]::new(32); [System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($b); [Convert]::ToBase64String($b) +# --dev mode uses this fixed dev key pair; fine for local development only. LIVEKIT_API_KEY=devkey LIVEKIT_API_SECRET=9U1eDtZx9eQrSWern4UcWa3lYIrfvrQqExLlkUXpP+o= diff --git a/backend/deploy/README.md b/backend/deploy/README.md index 89c2857..b8454e3 100644 --- a/backend/deploy/README.md +++ b/backend/deploy/README.md @@ -1,20 +1,21 @@ -# Ping — deploy +# Ping deploy -Reuses the Zitadel and SeaweedFS containers already running for Musify instead of starting new -ones. Musify's stack must be up first. +Reuses the Zitadel and SeaweedFS instances already running in the separate +`Infrastructure` repository, instead of starting new ones. That stack must be +up first (see its own README for the exact command). ```bash cp deploy/.env.example deploy/.env -# edit deploy/.env: -# - ZITADEL_ADMIN_PAT_DIR must point at Musify's deploy/zitadel/.output folder -# - S3_ACCESS_KEY / S3_SECRET_KEY must match the REAL values in Musify's own deploy/.env +# edit deploy/.env if your Infrastructure checkout isn't where +# ZITADEL_ADMIN_PAT_DIR assumes, or if its S3_ACCESS_KEY/SECRET_KEY differ +# from the defaults docker compose -f deploy/compose.yml -f deploy/compose.dev.yml up -d ``` -This starts Postgres, LiveKit, and the one-shot jobs (EF Core migrations, the Zitadel -project/apps for Ping, the `ping-storage` S3 bucket) — the usual dev loop runs the API and -frontend from the IDE against these. +This starts LiveKit and the one-shot jobs (waiting for Postgres, EF Core +migrations, the Zitadel project for Ping, the `ping-storage` S3 bucket). The +usual dev loop runs the API and frontend from the IDE against these. To also build and run the API/frontend in Docker instead: @@ -22,8 +23,9 @@ To also build and run the API/frontend in Docker instead: docker compose -f deploy/compose.yml -f deploy/compose.dev.yml --profile apps up -d --build ``` -By then `zitadel-init` has already written `AUTH_CLIENT_ID` / `WEB_CLIENT_ID` into `deploy/.env`. +By then `zitadel-init` has already written `AUTH_CLIENT_ID` / `WEB_CLIENT_ID` +into `deploy/.env`. -Ports (dev): API `5211`, web `3100`, Postgres `59010`, LiveKit `7880` (signaling) / `7881` (RTC -TCP) / `50000-50100/udp` (media). Zitadel (`8080`) and the SeaweedFS S3 API (`8333`) come from -Musify's own stack. +Ports (dev): API `5211`, web `3100`, LiveKit `7880` (signaling), `7881` (RTC +TCP) and `50000-50100/udp` (media). Postgres, Zitadel and the SeaweedFS S3 API +come from Infrastructure's own stack. See its README for that port table. diff --git a/backend/deploy/compose.dev.yml b/backend/deploy/compose.dev.yml index d8f036e..d66e43f 100644 --- a/backend/deploy/compose.dev.yml +++ b/backend/deploy/compose.dev.yml @@ -1,6 +1,6 @@ name: ping-dev -# Development overlay: publishes the infrastructure ports to the host so the .NET apps and +# Development overlay: publishes the ports to the host so the .NET apps and # the web frontend can run from the IDE. # # docker compose -f deploy/compose.yml -f deploy/compose.dev.yml up -d @@ -10,16 +10,15 @@ x-host-alias: &host-alias - "host.docker.internal:host-gateway" services: - postgres: - ports: - - "${POSTGRES_HOST_PORT}:5432" - livekit: ports: - "7880:7880" # HTTP / WebSocket signaling - "7881:7881" # RTC TCP fallback - "50000-50100:50000-50100/udp" # WebRTC media + wait-for-postgres: + extra_hosts: *host-alias + zitadel-init: extra_hosts: *host-alias volumes: @@ -28,6 +27,9 @@ services: seaweedfs-init: extra_hosts: *host-alias + migrate: + extra_hosts: *host-alias + api: ports: - "5211:5211" diff --git a/backend/deploy/compose.yml b/backend/deploy/compose.yml index fb6fd63..042f067 100644 --- a/backend/deploy/compose.yml +++ b/backend/deploy/compose.yml @@ -1,25 +1,22 @@ -# Ping — base stack, shared by development and production. +# Ping: base stack, shared by development and production. # -# This intentionally does NOT run Zitadel or SeaweedFS: Ping reuses the same instances -# already running for Musify (see deploy/.env.example — ZITADEL_ADMIN_PAT_DIR / S3_*). -# Bring Musify's stack up first, then this one. +# Zitadel and SeaweedFS are NOT run here: Ping reuses the shared instances in +# ../../../Infrastructure (see .env.example, ZITADEL_ADMIN_PAT_DIR / S3_*). +# Bring that stack up first. # # dev docker compose -f deploy/compose.yml -f deploy/compose.dev.yml up -d # -# The `api` and `web` services sit behind the `apps` profile, so a plain `up` starts -# Postgres, LiveKit and the one-shot bootstrap jobs (migrations, Zitadel project/apps, -# SeaweedFS bucket) and stops there — the usual dev setup, with the apps running from the -# IDE. Add `--profile apps up -d --build` as a SECOND command to also build and run them in -# Docker: by then zitadel-init has already written the OIDC client ids into the env file. - -x-app-depends: &app-depends - postgres: - condition: service_healthy +# The `api` and `web` services sit behind the `apps` profile, so a plain `up` +# starts LiveKit and the one-shot bootstrap jobs (Postgres wait, migrations, +# Zitadel project/apps, SeaweedFS bucket) and stops there. That's the usual +# dev setup, with the apps running from the IDE. Add `--profile apps up -d +# --build` as a SECOND command to also build and run them in Docker: by then +# zitadel-init has already written the OIDC client ids into the env file. x-backend-env: &backend-env - Database__ConnectionString: "Host=postgres;Port=5432;Database=${POSTGRES_DB};Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD}" - # The public S3 URL, not the internal one: the same address is used to sign the - # presigned upload/download URLs the browser has to be able to reach. + Database__ConnectionString: "Host=${POSTGRES_HOST};Port=5432;Database=${POSTGRES_DB};Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD}" + # The public S3 URL, not the internal one: the same address is used to sign + # the presigned upload/download URLs the browser has to be able to reach. InfrastructureStorage__Address: ${S3_PUBLIC_URL} InfrastructureStorage__AccessKey: ${S3_ACCESS_KEY} InfrastructureStorage__SecretAccessKey: ${S3_SECRET_KEY} @@ -30,22 +27,6 @@ x-backend-env: &backend-env services: # --- Infrastructure ------------------------------------------------------- - postgres: - image: postgres:17-alpine - container_name: ping-postgres - environment: - POSTGRES_USER: ${POSTGRES_USER} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - POSTGRES_DB: ${POSTGRES_DB} - volumes: - - postgres_data:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] - interval: 10s - timeout: 5s - retries: 10 - restart: unless-stopped - # Voice/video SFU. --dev keeps it single-node with in-memory state (fine for local # development), but the key pair is overridden via LIVEKIT_KEYS: the LiveKit .NET server SDK # rejects secrets shorter than 256 bits, and --dev's built-in "devkey/secret" pair is too short. @@ -56,18 +37,36 @@ services: environment: LIVEKIT_KEYS: "${LIVEKIT_API_KEY}: ${LIVEKIT_API_SECRET}" # Node IP is "automatically determined" by default, which inside a container means the - # Docker bridge network's internal address (e.g. 172.x.x.x) — unreachable from the + # Docker bridge network's internal address (e.g. 172.x.x.x), unreachable from the # browser, so ICE/media never connects even though the WebSocket signaling (which goes # through the published port, not this address) succeeds. Every public URL in this dev # stack is already hardcoded to localhost (see PUBLIC_WEB_URL, VITE_LIVEKIT_URL), so the - # browser and this container are always on the same machine — advertise that directly. + # browser and this container are always on the same machine. Advertise that directly. NODE_IP: 127.0.0.1 restart: unless-stopped - # One-shot: creates the Ping project and its two OIDC apps in Musify's Zitadel instance - # (or reuses them if they already exist), then writes AUTH_CLIENT_ID / WEB_CLIENT_ID back - # into the env file mounted at /workspace/.env — so a later `--profile apps up` picks up - # real values instead of empty ones. Both apps are public clients (PKCE, no secret): the + # Waits for Infrastructure's Postgres to accept connections before `migrate` + # runs. A cross-compose-project health check isn't possible, since + # Postgres is no longer part of this compose file. + wait-for-postgres: + image: postgres:17-alpine + container_name: ping-wait-for-postgres + entrypoint: ["/bin/sh", "-c"] + command: + - | + set -e + deadline=$$(( $$(date +%s) + 60 )) + until pg_isready -h ${POSTGRES_HOST} -p 5432 -U ${POSTGRES_USER} >/dev/null 2>&1; do + [ "$$(date +%s)" -lt "$$deadline" ] || { echo "postgres did not respond at ${POSTGRES_HOST}:5432. Is Infrastructure running?" >&2; exit 1; } + sleep 2 + done + restart: "no" + + # One-shot: creates the Ping project and its two OIDC apps in the shared + # Zitadel (or reuses them if they already exist), then writes + # AUTH_CLIENT_ID / WEB_CLIENT_ID back into the env file mounted at + # /workspace/.env, so a later `--profile apps up` picks up real values + # instead of empty ones. Both apps are public clients (PKCE, no secret): the # Scalar docs UI and the React SPA. zitadel-init: image: alpine:3.20 @@ -101,7 +100,7 @@ services: } if [ ! -f /zitadel-output/admin-sa.pat ]; then - echo "admin-sa.pat not found under ZITADEL_ADMIN_PAT_DIR — is Musify's Zitadel running?" >&2 + echo "admin-sa.pat not found under ZITADEL_ADMIN_PAT_DIR. Is Infrastructure's Zitadel running?" >&2 exit 1 fi PAT=$$(tr -d '\r\n' web app (React SPA — public client, PKCE)" + echo "==> web app (React SPA, public client, PKCE)" WEB_APP_NAME="Ping Web" WEB_APP_ID=$$(find_app "$$WEB_APP_NAME") if [ -n "$$WEB_APP_ID" ]; then @@ -190,8 +189,9 @@ services: echo "==> wired into $$ENV_FILE" restart: "no" - # One-shot: SeaweedFS does not auto-create buckets. Reuses Musify's SeaweedFS instance - # (same S3 endpoint/credentials) but creates Ping's own, separate bucket. + # One-shot: SeaweedFS does not auto-create buckets. Reuses the shared + # SeaweedFS instance (same S3 endpoint/credentials) but creates Ping's own, + # separate bucket. `aws s3 ls` doubles as the readiness probe. seaweedfs-init: image: amazon/aws-cli:latest container_name: ping-seaweedfs-init @@ -202,6 +202,12 @@ services: entrypoint: ["/bin/sh", "-c"] command: - | + set -e + deadline=$$(( $$(date +%s) + 60 )) + until aws --endpoint-url ${S3_PUBLIC_URL} s3 ls >/dev/null 2>&1; do + [ "$$(date +%s)" -lt "$$deadline" ] || { echo "seaweedfs did not respond at ${S3_PUBLIC_URL}. Is Infrastructure running?" >&2; exit 1; } + sleep 2 + done aws --endpoint-url ${S3_PUBLIC_URL} s3 mb s3://${S3_BUCKET} 2>/dev/null || true aws --endpoint-url ${S3_PUBLIC_URL} s3 ls restart: "no" @@ -213,10 +219,10 @@ services: dockerfile: Ping.Migrator.Dockerfile container_name: ping-migrate depends_on: - postgres: - condition: service_healthy + wait-for-postgres: + condition: service_completed_successfully environment: - Database__ConnectionString: "Host=postgres;Port=5432;Database=${POSTGRES_DB};Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD}" + Database__ConnectionString: "Host=${POSTGRES_HOST};Port=5432;Database=${POSTGRES_DB};Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD}" restart: "no" # --- Applications (profile: apps) ----------------------------------------- @@ -228,7 +234,6 @@ services: dockerfile: Ping.Api.Dockerfile container_name: ping-api depends_on: - <<: *app-depends migrate: condition: service_completed_successfully environment: @@ -266,6 +271,3 @@ services: depends_on: - api restart: unless-stopped - -volumes: - postgres_data: