#!/usr/bin/env bash # Two-instance dev environment for the Warmbly Cloud pool link. # # cloud : plays the hosted product. DEPLOYMENT_MODE=cloud, Stripe billing # gates ON (placeholder keys, so checkout fails but plans, trials # and the 10-mailbox free allowance behave like production), email # verification through Mailpit, the Sunrise Labs sandbox org seeded # so the pool has real mailboxes. # self : plays a brand-new self-hosted install. Nothing seeded, no # accounts, so the first visit is the /setup claim flow, exactly as # a self-hoster sees it. WARMBLY_CLOUD_URL points at the cloud. # # Both share the docker infra from `make infra` (postgres :15432, redis # :16379, nats :4222, mailpit :18025, dovecot :10993) but use their own # databases, redis db indexes and blob roots, so `make dev` keeps working # next to them. # # scripts/dev-poollink.sh up boot both stacks (idempotent) # scripts/dev-poollink.sh status ports, pids, health # scripts/dev-poollink.sh setup-link print a fresh claim link for the self-hosted instance # scripts/dev-poollink.sh down stop everything, keep the databases # scripts/dev-poollink.sh reset stop everything and drop the databases set -euo pipefail ROOT=$(cd "$(dirname "$0")/.." && pwd) RUN=/tmp/warmbly-poollink mkdir -p "$RUN" HOST=${PUBLIC_HOST:-localhost} CLOUD_API_PORT=${CLOUD_API_PORT:-18401} CLOUD_WEB_PORT=${CLOUD_WEB_PORT:-18301} SELF_API_PORT=${SELF_API_PORT:-18402} SELF_WEB_PORT=${SELF_WEB_PORT:-18302} CLOUD_DB=warmbly_cloud_dev SELF_DB=warmbly_selfhost_dev PG="postgres://warmbly:warmbly@localhost:15432" # Same dev secrets the Makefile uses, so sealed values are readable by both. common_env() { export APP_ENV=dev AWS_CONFIG_ENABLED=false EVENTBUS_PROVIDER=nats NATS_URL=nats://localhost:4222 CODEC_PROVIDER=json export KMS_PROVIDER=local KMS_LOCAL_MASTER_KEY=Xr0JA7gqF2POy29a7MRByyqddivTNt8WOyKsOXklazk= export CREDENTIALS_ENCRYPTION_KEY=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef export BLOB_PROVIDER=filesystem TASKS_PROVIDER=local CAPTCHA_PROVIDER=none PUBSUB_ENABLED=false ENCRYPTED_KEYS_PROVIDER=postgres export MAIL_TRANSPORT=smtp SMTP_HOST=localhost SMTP_PORT=11025 SMTP_SECURITY=none EMAIL_NAME='Warmbly Dev' EMAIL_ADDRESS=dev@warmbly.local export GIN_MODE=release AUTH_SECRET=local-dev-auth-secret-minimum-32-characters-long export GEODB_PATH=data/GeoLite2-City.mmdb INTERNAL_API_TOKEN=local-dev-internal-token TRACKING_DOMAIN=localhost:3000 export WEBSOCKET_URL=ws://localhost:4000/socket/websocket } cloud_env() { common_env export DEPLOYMENT_MODE=cloud BILLING_PROVIDER=stripe export STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY:-sk_test_placeholder} STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET:-whsec_placeholder} STRIPE_PUBLISHABLE_KEY=${STRIPE_PUBLISHABLE_KEY:-pk_test_placeholder} export PRIMARY_DB="$PG/$CLOUD_DB?sslmode=disable" REDIS=redis://localhost:16379/5 BLOB_FS_ROOT=/tmp/warmbly-poollink-blobs-cloud export API_HOST=0.0.0.0:$CLOUD_API_PORT APP_URL=http://$HOST:$CLOUD_WEB_PORT BLOB_PUBLIC_BASE_URL=http://$HOST:$CLOUD_API_PORT/public export CORS_ALLOW_ORIGINS=http://$HOST:$CLOUD_WEB_PORT,http://localhost:$CLOUD_WEB_PORT export ENV_LABEL=cloud } self_env() { common_env export DEPLOYMENT_MODE=self_hosted BILLING_PROVIDER=none export PRIMARY_DB="$PG/$SELF_DB?sslmode=disable" REDIS=redis://localhost:16379/6 BLOB_FS_ROOT=/tmp/warmbly-poollink-blobs-self export API_HOST=0.0.0.0:$SELF_API_PORT APP_URL=http://$HOST:$SELF_WEB_PORT BLOB_PUBLIC_BASE_URL=http://$HOST:$SELF_API_PORT/public export CORS_ALLOW_ORIGINS=http://$HOST:$SELF_WEB_PORT,http://localhost:$SELF_WEB_PORT export WARMBLY_CLOUD_URL=http://localhost:$CLOUD_API_PORT INSTANCE_NAME=${INSTANCE_NAME:-my-selfhost} WARMBLY_VERSION=dev export ENV_LABEL=self-hosted } psql_admin() { docker exec -i warmbly-postgres-1 psql -U warmbly -d postgres -v ON_ERROR_STOP=1 "$@"; } psql_db() { docker exec -i warmbly-postgres-1 psql -U warmbly -d "$1" -v ON_ERROR_STOP=1 -tA "${@:2}"; } need() { command -v "$1" >/dev/null || { echo "$1 is required"; exit 1; }; } ensure_infra() { need docker; need go; need pnpm if ! docker exec warmbly-postgres-1 pg_isready -U warmbly >/dev/null 2>&1; then echo "The dev infra is not running. Start it first: make infra" exit 1 fi } ensure_db() { local db=$1 if [ "$(psql_admin -tAc "select 1 from pg_database where datname='$db'")" != "1" ]; then psql_admin -c "CREATE DATABASE $db" >/dev/null echo "created database $db" fi } ensure_pools() { # warmup_pools rows are seeded, not migrated; the pool needs both tiers. psql_db "$1" -c "insert into warmup_pools (pool_type, name, description, max_participants) select v.t::warmup_pool_type, v.n, '', 100000 from (values ('free','Free'),('premium','Premium')) v(t,n) where not exists (select 1 from warmup_pools p where p.pool_type = v.t::warmup_pool_type)" >/dev/null } start_bg() { # name, logfile, command... local name=$1 log=$2; shift 2 if [ -f "$RUN/$name.pid" ] && kill -0 "$(cat "$RUN/$name.pid")" 2>/dev/null; then echo "$name already running (pid $(cat "$RUN/$name.pid"))"; return fi setsid nohup "$@" >"$log" 2>&1 < /dev/null & echo $! > "$RUN/$name.pid" echo "$name started (pid $!), log $log" } wait_health() { # url, label local i for i in $(seq 1 60); do if [ "$(curl -s -o /dev/null -w '%{http_code}' "$1/health")" = 200 ]; then return 0; fi sleep 1 done echo "$2 did not become healthy; see the log"; return 1 } build_backend() { ( cd "$ROOT" && go build -o "$RUN/backend" ./cmd/backend ) } cmd_up() { ensure_infra cd "$ROOT" ensure_db "$CLOUD_DB"; ensure_db "$SELF_DB" [ -d web/node_modules ] || (cd web && pnpm install) echo "== migrating" ( cloud_env; go run ./cmd/migrate >/dev/null ); ( self_env; go run ./cmd/migrate >/dev/null ) if [ "$(psql_db "$CLOUD_DB" -c "select count(*) from users")" = "0" ]; then echo "== seeding the cloud with the Sunrise Labs sandbox org (pool mailboxes)" ( cloud_env; go run ./cmd/sandbox -seed-only >/dev/null ) fi ensure_pools "$CLOUD_DB"; ensure_pools "$SELF_DB" echo "== building backend" build_backend ( cloud_env; start_bg cloud-api "$RUN/cloud-api.log" "$RUN/backend" ) ( self_env; start_bg self-api "$RUN/self-api.log" "$RUN/backend" ) wait_health "http://localhost:$CLOUD_API_PORT" "cloud backend" wait_health "http://localhost:$SELF_API_PORT" "self-hosted backend" ( cd web && start_bg cloud-web "$RUN/cloud-web.log" env VITE_APP_URL=http://$HOST:$CLOUD_WEB_PORT VITE_API_URL=http://$HOST:$CLOUD_API_PORT VITE_TURNSTILE_KEY=1x00000000000000000000AA VITE_TURNSTILE_BYPASS_TOKEN=warmbly-local-turnstile-bypass pnpm dev --port "$CLOUD_WEB_PORT" --strictPort ${PUBLIC_HOST:+--host 0.0.0.0} ) ( cd web && start_bg self-web "$RUN/self-web.log" env VITE_APP_URL=http://$HOST:$SELF_WEB_PORT VITE_API_URL=http://$HOST:$SELF_API_PORT VITE_TURNSTILE_KEY=1x00000000000000000000AA VITE_TURNSTILE_BYPASS_TOKEN=warmbly-local-turnstile-bypass pnpm dev --port "$SELF_WEB_PORT" --strictPort ${PUBLIC_HOST:+--host 0.0.0.0} ) echo cmd_status echo echo "Walkthrough (the self-hoster's first minute):" echo " 1. Claim the self-hosted instance with the setup link below (or: scripts/dev-poollink.sh setup-link)." echo " 2. Connect a mailbox there: SMTP localhost:11025 (no auth, Mailpit), IMAP localhost:10993 TLS," echo " any user like you@sunrise.test with password 'sandbox' (Dovecot accepts every user)." echo " 3. Settings > Warmbly Cloud > Connect. Open the link it shows: that is the 'prod cloud' at http://$HOST:$CLOUD_WEB_PORT," echo " where you register a new account (verification mail lands in Mailpit http://localhost:18025), then approve the code." echo " 4. Back on the instance, enroll the mailbox. The cloud workspace is on a free trial with Stripe gates on," echo " so the 10-mailbox allowance and the upgrade card behave like production (checkout itself needs real keys)." echo " Existing cloud login with a full pool: sandbox@warmbly.test / password123 (Sunrise Labs)." echo cmd_setup_link } cmd_setup_link() { cd "$ROOT" ( self_env; go run ./cmd/warmblyctl setup-link 2>&1 ) | sed 's/^/ /' } cmd_status() { local n for n in cloud-api self-api cloud-web self-web; do if [ -f "$RUN/$n.pid" ] && kill -0 "$(cat "$RUN/$n.pid")" 2>/dev/null; then printf ' %-10s running (pid %s)\n' "$n" "$(cat "$RUN/$n.pid")" else printf ' %-10s stopped\n' "$n" fi done echo echo " cloud (prod-like): dashboard http://$HOST:$CLOUD_WEB_PORT api http://localhost:$CLOUD_API_PORT db $CLOUD_DB" echo " self-hosted: dashboard http://$HOST:$SELF_WEB_PORT api http://localhost:$SELF_API_PORT db $SELF_DB" echo " mailpit: http://localhost:18025 logs: $RUN/*.log" } cmd_down() { local n for n in cloud-web self-web cloud-api self-api; do if [ -f "$RUN/$n.pid" ]; then local pid; pid=$(cat "$RUN/$n.pid") kill -- "-$pid" 2>/dev/null || kill "$pid" 2>/dev/null || true rm -f "$RUN/$n.pid" echo "$n stopped" fi done } cmd_reset() { cmd_down psql_admin -c "DROP DATABASE IF EXISTS $CLOUD_DB" -c "DROP DATABASE IF EXISTS $SELF_DB" >/dev/null rm -rf /tmp/warmbly-poollink-blobs-cloud /tmp/warmbly-poollink-blobs-self docker exec warmbly-redis-1 redis-cli -n 5 flushdb >/dev/null; docker exec warmbly-redis-1 redis-cli -n 6 flushdb >/dev/null echo "databases dropped" } case "${1:-up}" in up) cmd_up ;; status) cmd_status ;; setup-link) cmd_setup_link ;; down) cmd_down ;; reset) cmd_reset ;; *) echo "usage: $0 up|status|setup-link|down|reset"; exit 1 ;; esac