#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" usage() { printf 'usage: sync-convex-env \n' >&2 exit 2 } ENVIRONMENT="${1:-}" [[ "$ENVIRONMENT" == dev || "$ENVIRONMENT" == staging ]] || usage if [[ "${2:-}" != "--from-current-env" ]]; then ENV_FILE="$(mktemp "${TMPDIR:-/tmp}/spoon-convex-env.XXXXXX.env")" trap 'rm -f "$ENV_FILE"' EXIT INT TERM HUP sh "$ROOT_DIR/scripts/export-env" "$ENVIRONMENT" > "$ENV_FILE" exec bunx dotenv -e "$ENV_FILE" -- "$0" "$ENVIRONMENT" --from-current-env fi info() { printf '▶ %s\n' "$*"; } warn() { printf 'Warning: %s\n' "$*" >&2; } STATE_FILE="$ROOT_DIR/.local/$ENVIRONMENT.generated.env" convex_env_names() { (cd "$ROOT_DIR/packages/backend" && bunx convex env list) 2>/dev/null \ | sed -n 's/^\([A-Za-z_][A-Za-z0-9_]*\)=.*/\1/p' } convex_env_has() { local name="$1" printf '%s\n' "$CURRENT_CONVEX_ENV_NAMES" | grep -qx "$name" } set_convex_env() { local name="$1" local value="${!name-}" local tmp if [[ -z "$value" ]]; then warn "Skipping $name; it is not present in exported $ENVIRONMENT environment." return 0 fi tmp="$(mktemp "${TMPDIR:-/tmp}/spoon-convex-value.XXXXXX")" printf '%s' "$value" > "$tmp" (cd "$ROOT_DIR/packages/backend" && bunx convex env set "$name" --from-file "$tmp" >/dev/null) rm -f "$tmp" printf ' synced %s\n' "$name" } set_literal_convex_env() { local name="$1" local value="$2" local tmp tmp="$(mktemp "${TMPDIR:-/tmp}/spoon-convex-value.XXXXXX")" printf '%s' "$value" > "$tmp" (cd "$ROOT_DIR/packages/backend" && bunx convex env set "$name" --from-file "$tmp" >/dev/null) rm -f "$tmp" printf ' synced %s\n' "$name" } upsert_state() { local key="$1" value="$2" tmp escaped mkdir -p "$ROOT_DIR/.local" tmp="$(mktemp "${TMPDIR:-/tmp}/spoon-state.XXXXXX.env")" [ ! -f "$STATE_FILE" ] || grep -v "^${key}=" "$STATE_FILE" > "$tmp" || true escaped="$(printf '%s' "$value" | sed "s/'/'\\\\''/g")" printf "%s='%s'\n" "$key" "$escaped" >> "$tmp" mv "$tmp" "$STATE_FILE" } generate_secret() { node -e "console.log(require('node:crypto').randomBytes(32).toString('base64url'))" } sync_generated_dev_auth_keys() { [[ "$ENVIRONMENT" == dev ]] || return 0 if convex_env_has JWT_PRIVATE_KEY && convex_env_has JWKS; then return 0 fi info "Generating local Convex Auth signing keys" local auth_keys jwt jwks auth_keys="$(node "$ROOT_DIR/scripts/generate-convex-auth-keys.mjs")" jwt="$(printf '%s\n' "$auth_keys" | sed -n 's/^JWT_PRIVATE_KEY="\(.*\)"$/\1/p')" jwks="$(printf '%s\n' "$auth_keys" | sed -n 's/^JWKS=//p')" [[ -n "$jwt" && -n "$jwks" ]] || { printf 'sync-convex-env: failed to generate Convex Auth keys.\n' >&2 exit 1 } set_literal_convex_env JWT_PRIVATE_KEY "$jwt" set_literal_convex_env JWKS "$jwks" } sync_generated_dev_encryption_key() { [[ "$ENVIRONMENT" == dev ]] || return 0 if [[ -n "${SPOON_ENCRYPTION_KEY:-}" ]]; then set_convex_env SPOON_ENCRYPTION_KEY return 0 fi if convex_env_has SPOON_ENCRYPTION_KEY; then return 0 fi info "Generating local Spoon encryption key" local encryption_key encryption_key="$(generate_secret)" [[ -n "$encryption_key" ]] || { printf 'sync-convex-env: failed to generate Spoon encryption key.\n' >&2 exit 1 } upsert_state SPOON_ENCRYPTION_KEY "$encryption_key" export SPOON_ENCRYPTION_KEY="$encryption_key" set_literal_convex_env SPOON_ENCRYPTION_KEY "$encryption_key" } sync_generated_dev_worker_token() { [[ "$ENVIRONMENT" == dev ]] || return 0 if [[ -n "${SPOON_WORKER_TOKEN:-}" ]]; then set_convex_env SPOON_WORKER_TOKEN return 0 fi if convex_env_has SPOON_WORKER_TOKEN; then return 0 fi info "Generating local Spoon worker token" local worker_token worker_token="$(generate_secret)" [[ -n "$worker_token" ]] || { printf 'sync-convex-env: failed to generate Spoon worker token.\n' >&2 exit 1 } upsert_state SPOON_WORKER_TOKEN "$worker_token" export SPOON_WORKER_TOKEN="$worker_token" set_literal_convex_env SPOON_WORKER_TOKEN "$worker_token" } sync_site_url() { if [[ -n "${SITE_URL:-}" ]]; then set_convex_env SITE_URL return 0 fi if [[ "$ENVIRONMENT" == dev ]]; then set_literal_convex_env SITE_URL "http://localhost:3000" return 0 fi if [[ -n "${NEXT_PUBLIC_SITE_URL:-}" ]]; then set_literal_convex_env SITE_URL "$NEXT_PUBLIC_SITE_URL" else warn "Skipping SITE_URL; neither SITE_URL nor NEXT_PUBLIC_SITE_URL is present." fi } CURRENT_CONVEX_ENV_NAMES="$(convex_env_names || true)" info "Syncing $ENVIRONMENT environment variables into Convex" sync_generated_dev_auth_keys sync_generated_dev_encryption_key sync_generated_dev_worker_token for name in \ JWT_PRIVATE_KEY \ JWKS \ AUTH_AUTHENTIK_ID \ AUTH_AUTHENTIK_SECRET \ AUTH_AUTHENTIK_ISSUER \ AUTH_GITHUB_ID \ AUTH_GITHUB_SECRET \ GITHUB_APP_ID \ GITHUB_APP_CLIENT_ID \ GITHUB_APP_CLIENT_SECRET \ GITHUB_APP_PRIVATE_KEY \ GITHUB_APP_WEBHOOK_SECRET \ GITHUB_APP_SLUG \ GITHUB_APP_INSTALLATION_ID \ GITHUB_APP_OWNER \ SPOON_WORKER_TOKEN \ USESEND_API_KEY \ USESEND_URL \ USESEND_FROM_EMAIL do if [[ "$ENVIRONMENT" == dev && ( "$name" == JWT_PRIVATE_KEY || "$name" == JWKS ) && -z "${!name-}" ]]; then continue fi set_convex_env "$name" done sync_site_url info "Convex environment sync complete"