#!/usr/bin/env bash # SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0 # AT3.7 proof: a real local Convex backend -> backup runner -> fake Blob -> empty backend. set -euo pipefail app="$(cd "$(dirname "$0")/../../.." && pwd)" repo="$(cd "$app/../.." && pwd)" node_modules="$app/node_modules" docker_backend_image="${CONVEX_BACKEND_IMAGE:-build.conusai.com/conusai/convex-backend@sha256:1738f1673f8d63161043a7859710d2301b1e9d6271e06afbb7af31594ea3a58f}" work="$(mktemp -d)" suffix="at37-$$" source_name="${suffix}-source" target_name="${suffix}-target" fake_name="${suffix}-platform" better_auth_name="${suffix}-better-auth" # kit:convex-better-auth network_name="${suffix}-network" source_port="" target_port="" platform_port="" cleanup() { local status=$? if [[ "$status" -ne 0 ]] && docker inspect "$source_name" >/dev/null 2>&1; then echo "AT3.7 local Convex backend log on failure:" >&2 docker logs --tail 100 "$source_name" >&2 || true fi docker rm -f "$source_name" "$target_name" "$fake_name" >/dev/null 2>&1 || true docker rm -f "$better_auth_name" >/dev/null 2>&1 || true # kit:convex-better-auth docker network rm "$network_name" >/dev/null 2>&1 || true rm -rf "$work" } trap cleanup EXIT command -v docker >/dev/null || { echo "AT3.7 round-trip needs Docker" >&2; exit 1; } # An explicit small fixture subnet avoids exhausting a shared host's default pools. subnet="$(node -e 'const b=require("node:crypto").randomBytes(3);process.stdout.write("198."+(18+(b[0]&1))+"."+b[1]+"."+(b[2]&240)+"/28")')" docker network create --subnet "$subnet" "$network_name" >/dev/null for name in "$source_name" "$target_name"; do docker run -d --name "$name" --network "$network_name" --add-host=host.docker.internal:host-gateway -e INSTANCE_NAME="$name" \ -e INSTANCE_SECRET="$(node -e 'process.stdout.write(require("node:crypto").randomBytes(32).toString("hex"))')" \ -e CONVEX_CLOUD_ORIGIN="http://$name:3210" -e CONVEX_SITE_ORIGIN="http://$name:3211" \ -p "127.0.0.1::3210" "$docker_backend_image" >/dev/null assigned_port="$(docker port "$name" 3210/tcp | sed -E 's/.*:([0-9]+)$/\1/')" if [[ "$name" == "$source_name" ]]; then source_port="$assigned_port"; else target_port="$assigned_port"; fi done docker run -d --name "$fake_name" --network "$network_name" -p "127.0.0.1::4010" \ -v "$repo:$repo:ro" -w "$app" \ node:22 node profiles/convex/backup/test-platform.mjs 4010 >"$work/platform.log" 2>&1 platform_port="$(docker port "$fake_name" 4010/tcp | sed -E 's/.*:([0-9]+)$/\1/')" platform_host_url="http://127.0.0.1:$platform_port/api" platform_action_url="http://$fake_name:4010/api" wait_backend() { local port="$1" i for i in $(seq 1 60); do curl -fsS "http://127.0.0.1:$port/version" >/dev/null 2>&1 && return 0 sleep 2 done echo "Convex backend on $port did not become ready" >&2 return 1 } wait_backend "$source_port" wait_backend "$target_port" source_key="$(docker exec "$source_name" ./generate_admin_key.sh 2>/dev/null)" target_key="$(docker exec "$target_name" ./generate_admin_key.sh 2>/dev/null)" # kit:convex-better-auth:begin # A Better Auth app: its own Better Auth Convex deployment, a third backend from the same image. The # deploy job deploys the Better Auth functions to it and installs its URL, admin key and namespace in # the app's backend, whose account actions end a deleted person's Better Auth account there. docker run -d --name "$better_auth_name" --network "$network_name" -e INSTANCE_NAME="$better_auth_name" \ -e INSTANCE_SECRET="$(node -e 'process.stdout.write(require("node:crypto").randomBytes(32).toString("hex"))')" \ -e CONVEX_CLOUD_ORIGIN="http://127.0.0.1:3210" -e CONVEX_SITE_ORIGIN="http://127.0.0.1:3211" \ -p "127.0.0.1::3210" -p "127.0.0.1::3211" "$docker_backend_image" >/dev/null better_auth_port="$(docker port "$better_auth_name" 3210/tcp | sed -E 's/.*:([0-9]+)$/\1/')" better_auth_site_port="$(docker port "$better_auth_name" 3211/tcp | sed -E 's/.*:([0-9]+)$/\1/')" wait_backend "$better_auth_port" better_auth_key="$(docker exec "$better_auth_name" ./generate_admin_key.sh 2>/dev/null | tail -n 1)" export AUTH_PROVIDER=convex-better-auth AUTH_IDENTITY_NAMESPACE=better-auth:convex-smoke export BETTER_AUTH_CONVEX_URL="http://127.0.0.1:$better_auth_port" BETTER_AUTH_CONVEX_ADMIN_KEY="$better_auth_key" export BETTER_AUTH_SECRET="$(node -e 'process.stdout.write(require("node:crypto").randomBytes(32).toString("base64url"))')" export CONUSAI_PUBLIC_URL=http://127.0.0.1:3000 # kit:convex-better-auth:end cd "$app" # Import the same shaped notes row the app's notes fixture exercises. Import is the supported # Convex path for seeded documents; backup/export is independent of the function deployment. cat > "$work/notes.jsonl" <<'JSONL' {"id":"backup-notes-fixture","orgId":"backup-org","workspaceId":"backup-workspace","projectId":"backup-project","createdBy":"backup-test","title":"AT3.7 notes fixture","body":"restored from the Blob archive","createdAt":"2026-10-21T00:00:00.000Z","updatedAt":"2026-10-21T00:00:00.000Z","version":1} JSONL CONVEX_SELF_HOSTED_URL="http://127.0.0.1:$source_port" \ CONVEX_SELF_HOSTED_ADMIN_KEY="$source_key" \ pnpm exec convex import --table notes_note "$work/notes.jsonl" >/dev/null for i in $(seq 1 30); do curl -fsS "http://127.0.0.1:$platform_port/__e2e/health" >/dev/null 2>&1 && break sleep 1 done if ! curl -fsS "http://127.0.0.1:$platform_port/__e2e/health" >/dev/null; then echo "fake platform did not become ready; log follows" >&2 cat "$work/platform.log" >&2 || true exit 1 fi # Deploy the generated app (including its Convex schema and component) into the source backend, # install only test-scoped platform variables, and exercise real functions over the self-hosted # HTTP API. This proves module codegen/deploy and the admin-auth identity boundary, beyond the # adapter's convex-test unit suite. # The support module's effect actions mint sessions for this channel against the fake platform. export CONUSAI_SUPPORT_CHANNEL=convex-smoke-channel # kit:support CONVEX_SELF_HOSTED_URL="http://127.0.0.1:$source_port" \ CONVEX_SELF_HOSTED_ADMIN_KEY="$source_key" \ CONUSAI_API_URL="$platform_action_url" CONUSAI_API_TOKEN=dev-token CONUSAI_PROJECT_ID=1 \ CONUSAI_ORG_SLUG=fixture-org \ node profiles/convex/deploy.mjs # kit:convex-better-auth:begin # deploy.mjs installed the Better Auth deployment's variables in the app's backend. The deploy ran on # this host, through the published port; the backend reaches the Better Auth backend on their shared # network instead, so its URL is the one variable set again here (in a deployment both are one URL). [[ "$(CONVEX_SELF_HOSTED_URL="http://127.0.0.1:$source_port" CONVEX_SELF_HOSTED_ADMIN_KEY="$source_key" \ pnpm exec convex env get AUTH_IDENTITY_NAMESPACE)" == "better-auth:convex-smoke" ]] \ || { echo "deploy.mjs did not install the Better Auth variables in the app's backend" >&2; exit 1; } CONVEX_SELF_HOSTED_URL="http://127.0.0.1:$source_port" CONVEX_SELF_HOSTED_ADMIN_KEY="$source_key" \ pnpm exec convex env set BETTER_AUTH_CONVEX_URL "http://$better_auth_name:3210" >/dev/null export SMOKE_BETTER_AUTH_SITE_URL="http://127.0.0.1:$better_auth_site_port" export SMOKE_AUTH_IDENTITY_NAMESPACE="$AUTH_IDENTITY_NAMESPACE" SMOKE_PUBLIC_URL="$CONUSAI_PUBLIC_URL" # kit:convex-better-auth:end # `convex deploy` generated the app-specific API types for this local deployment. Typecheck every # query/mutation/action against those generated contexts before exercising the HTTP functions. pnpm exec tsc -p convex/tsconfig.json --noEmit CONVEX_SELF_HOSTED_URL="http://127.0.0.1:$source_port" \ CONVEX_SELF_HOSTED_ADMIN_KEY="$source_key" \ CONUSAI_API_URL="$platform_host_url" \ node profiles/convex/backup/test-deployed-app.mjs CONVEX_SELF_HOSTED_URL="http://127.0.0.1:$source_port" CONVEX_SELF_HOSTED_ADMIN_KEY="$source_key" \ CONUSAI_API_URL="$platform_host_url" CONUSAI_API_TOKEN=dev-token \ CONUSAI_PROJECT_ID=1 CONVEX_BACKUP_RETENTION=1 CONVEX_CLI="$app/node_modules/.bin/convex" \ CONVEX_APP_DIR="$app" node profiles/convex/backup/runner.mjs --once sleep 1 CONVEX_SELF_HOSTED_URL="http://127.0.0.1:$source_port" CONVEX_SELF_HOSTED_ADMIN_KEY="$source_key" \ CONUSAI_API_URL="$platform_host_url" CONUSAI_API_TOKEN=dev-token \ CONUSAI_PROJECT_ID=1 CONVEX_BACKUP_RETENTION=1 CONVEX_CLI="$app/node_modules/.bin/convex" \ CONVEX_APP_DIR="$app" node profiles/convex/backup/runner.mjs --once backup_path="$(node --input-type=module - "$platform_port" <<'JS' import { createObjectStore } from "@conusai-kit/platform" const port = process.argv[2] const store = createObjectStore({ CONUSAI_API_URL: `http://127.0.0.1:${port}/api`, CONUSAI_API_TOKEN: "dev-token", CONUSAI_PROJECT_ID: "1" }) const listed = await store.list({ prefix: "convex-backups/1/" }) if (listed.blobs.length !== 1) throw new Error(`expected one backup, found ${listed.blobs.length}`) console.log(listed.blobs[0].pathname) JS )" curl -fsS -H 'Authorization: Bearer dev-token' \ "http://127.0.0.1:$platform_port/api/blob/1/$backup_path" -o "$work/backup.zip" CONVEX_SELF_HOSTED_URL="http://127.0.0.1:$target_port" \ CONVEX_SELF_HOSTED_ADMIN_KEY="$target_key" \ CONUSAI_API_URL="$platform_action_url" CONUSAI_API_TOKEN=dev-token CONUSAI_PROJECT_ID=1 \ CONUSAI_ORG_SLUG=fixture-org \ node profiles/convex/deploy.mjs CONVEX_SELF_HOSTED_URL="http://127.0.0.1:$target_port" \ CONVEX_SELF_HOSTED_ADMIN_KEY="$target_key" \ pnpm exec convex import "$work/backup.zip" >/dev/null CONVEX_SELF_HOSTED_URL="http://127.0.0.1:$target_port" \ CONVEX_SELF_HOSTED_ADMIN_KEY="$target_key" \ pnpm exec convex export --include-file-storage --path "$work/restored.zip" >/dev/null # Read the complete archive: an early-exiting grep can SIGPIPE unzip under pipefail # and report a missing fixture even after finding it. python3 - "$work/restored.zip" <<'RESTORED_PROOF' import json, sys, zipfile with zipfile.ZipFile(sys.argv[1]) as archive: names = [name for name in archive.namelist() if name == "notes_note/documents.jsonl" or name.endswith("/notes_note/documents.jsonl")] rows = [json.loads(line) for name in names for line in archive.read(name).splitlines() if line.strip()] assert any(row.get("id") == "backup-notes-fixture" and row.get("title") == "AT3.7 notes fixture" and row.get("body") == "restored from the Blob archive" for row in rows), "restored archive lacks the exact notes fixture" RESTORED_PROOF echo "AT3.7 round-trip green: local Convex notes fixture -> fake Blob -> empty local Convex backend"