Update Convex with no payload to be just like convex with payload but without payload

This commit is contained in:
Gabriel Brown
2026-06-21 15:35:42 -05:00
parent 13b8b36c4c
commit fba73a92ce
130 changed files with 15637 additions and 32018 deletions
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
ENVIRONMENT="${1:-staging}"
[[ "$ENVIRONMENT" == dev || "$ENVIRONMENT" == staging ]] || { echo "usage: build-next-app [dev|staging]" >&2; exit 2; }
ENV_FILE="${CI_ENV_FILE:-}"
cleanup() { [[ -n "$ENV_FILE" && "$ENV_FILE" != "${CI_ENV_FILE:-}" ]] && rm -f "$ENV_FILE" || true; }
trap cleanup EXIT
if [[ -z "$ENV_FILE" && -z "${CI:-}" ]]; then ENV_FILE="$(mktemp)"; sh "$ROOT_DIR/scripts/export-env" "$ENVIRONMENT" > "$ENV_FILE"; fi
args=(); [[ -z "$ENV_FILE" ]] || args+=(--env-file "$ENV_FILE")
docker compose "${args[@]}" -f "$ROOT_DIR/docker/compose.yml" build convexmonorepo-next
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd -- "$SCRIPT_DIR/../.." && pwd)"
COMPOSE_FILE="$ROOT_DIR/docker/compose.local.yml"
STATE_FILE="$ROOT_DIR/.local/dev.generated.env"
WIPE=false
[ "${1:-}" = "--wipe" ] && WIPE=true
if command -v docker >/dev/null 2>&1; then RUNTIME=docker
elif command -v podman >/dev/null 2>&1; then RUNTIME=podman
else echo "Docker or Podman not found; nothing to stop." >&2; exit 0; fi
ENV_FILE="$(mktemp "${TMPDIR:-/tmp}/convex-monorepo-local.XXXXXX.env")"
trap 'rm -f "$ENV_FILE"' EXIT
sh "$ROOT_DIR/scripts/export-env" dev > "$ENV_FILE"
if [ "$WIPE" = true ]; then
"$RUNTIME" compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" down -v
if [ -f "$STATE_FILE" ]; then
tmp="${STATE_FILE}.tmp"
grep -v '^CONVEX_SELF_HOSTED_ADMIN_KEY=' "$STATE_FILE" > "$tmp" || true
mv "$tmp" "$STATE_FILE"
fi
echo "Local stack and Convex data volume removed; generated admin key cleared."
else
"$RUNTIME" compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" down
echo "Local stack stopped; Convex data preserved."
fi
Executable
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd -- "$SCRIPT_DIR/../.." && pwd)"
COMPOSE_FILE="$ROOT_DIR/docker/compose.local.yml"
STATE_FILE="$ROOT_DIR/.local/dev.generated.env"
ENV_FILE=""
info() { printf '▶ %s\n' "$*"; }
die() { printf 'Error: %s\n' "$*" >&2; exit 1; }
if command -v docker >/dev/null 2>&1; then RUNTIME=docker
elif command -v podman >/dev/null 2>&1; then RUNTIME=podman
else die "Docker or Podman is required."; fi
"$RUNTIME" info >/dev/null 2>&1 || die "$RUNTIME is not usable."
mkdir -p "$ROOT_DIR/.local"
cleanup() { [ -z "$ENV_FILE" ] || rm -f "$ENV_FILE"; }
trap cleanup EXIT
refresh_env() {
local next
next="$(mktemp "${TMPDIR:-/tmp}/convex-monorepo-local.XXXXXX.env")"
sh "$ROOT_DIR/scripts/export-env" dev > "$next" || { rm -f "$next"; die "Unable to export Infisical dev."; }
[ -z "$ENV_FILE" ] || rm -f "$ENV_FILE"
ENV_FILE="$next"
set -a
# shellcheck disable=SC1090
source "$ENV_FILE"
set +a
}
dc() { "$RUNTIME" compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" "$@"; }
upsert_state() {
local key="$1" value="$2" tmp escaped
tmp="$(mktemp "${TMPDIR:-/tmp}/convex-monorepo-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"
}
refresh_env
info "Starting local Convex and dashboard"
dc up -d
info "Waiting for Convex at http://localhost:${BACKEND_PORT:-3210}"
for i in $(seq 1 60); do
curl -fs "http://localhost:${BACKEND_PORT:-3210}/version" >/dev/null 2>&1 && break
[ "$i" -lt 60 ] || die "Convex did not become ready."
sleep 2
done
if [ -z "${CONVEX_SELF_HOSTED_ADMIN_KEY:-}" ]; then
admin_key="$(dc exec -T convex-backend ./generate_admin_key.sh 2>/dev/null | grep -E '.+\|.+' | tail -n1 | tr -d '\r')"
[ -n "$admin_key" ] || die "Unable to generate the Convex admin key."
upsert_state CONVEX_SELF_HOSTED_ADMIN_KEY "$admin_key"
refresh_env
info "Generated the machine-local Convex admin key"
fi
info "Deploying Convex schema and functions"
(cd "$ROOT_DIR/packages/backend" && bun run setup)
convex_env_names="$(
sh "$ROOT_DIR/scripts/with-env" dev -- bash -c \
'cd packages/backend && bunx convex env list' 2>/dev/null \
| sed -n 's/^\([A-Za-z_][A-Za-z0-9_]*\)=.*/\1/p'
)"
if ! printf '%s\n' "$convex_env_names" | grep -qx 'JWT_PRIVATE_KEY' \
|| ! printf '%s\n' "$convex_env_names" | grep -qx 'JWKS' \
|| ! printf '%s\n' "$convex_env_names" | grep -qx 'SITE_URL'; then
info "Configuring local Convex Auth keys"
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')"
JWT_VAL="$jwt" JWKS_VAL="$jwks" sh "$ROOT_DIR/scripts/with-env" dev -- bash -c '
cd packages/backend
bunx convex env set "JWT_PRIVATE_KEY=$JWT_VAL" >/dev/null
bunx convex env set "JWKS=$JWKS_VAL" >/dev/null
bunx convex env set "SITE_URL=http://localhost:3000" >/dev/null
'
fi
printf '\nLocal stack ready:\n App: http://localhost:3000\n Convex: http://localhost:%s\n Dashboard: http://localhost:%s\n' "${BACKEND_PORT:-3210}" "${DASHBOARD_PORT:-6791}"
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
COMPOSE_FILE="$ROOT_DIR/docker/compose.yml"
ENVIRONMENT="${1:-staging}"
if [[ "$ENVIRONMENT" == dev || "$ENVIRONMENT" == staging ]]; then shift || true; else ENVIRONMENT=staging; fi
ENV_FILE="${CI_ENV_FILE:-}"
cleanup() { [[ -n "$ENV_FILE" && "$ENV_FILE" != "${CI_ENV_FILE:-}" ]] && rm -f "$ENV_FILE" || true; }
trap cleanup EXIT
if [[ -z "$ENV_FILE" && -z "${CI:-}" ]]; then
ENV_FILE="$(mktemp "${TMPDIR:-/tmp}/convex-monorepo-compose.XXXXXX.env")"
sh "$ROOT_DIR/scripts/export-env" "$ENVIRONMENT" > "$ENV_FILE"
fi
args=()
[[ -z "$ENV_FILE" ]] || args+=(--env-file "$ENV_FILE")
translated=()
for arg in "$@"; do
case "$arg" in backend) translated+=(convexmonorepo-backend) ;; dashboard) translated+=(convexmonorepo-dashboard) ;; next) translated+=(convexmonorepo-next) ;; *) translated+=("$arg") ;; esac
done
set +e
docker compose "${args[@]}" -f "$COMPOSE_FILE" "${translated[@]}"
status=$?
set -e
exit "$status"
Executable
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -euo pipefail
if [ -n "${CI:-}" ]; then echo "CI detected; skipping local e2e."; exit 0; fi
if [ -n "${SKIP_E2E:-}" ]; then echo "SKIP_E2E set; skipping local e2e."; exit 0; fi
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)"
bash "$ROOT_DIR/scripts/db/up"
echo "Local-stack smoke checks passed; no seeded browser flow is configured."
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env sh
set -eu
[ "$#" -eq 1 ] || { echo "usage: export-env <dev|staging>" >&2; exit 2; }
ENVIRONMENT="$1"
case "$ENVIRONMENT" in dev|staging) ;; *) echo "export-env: expected dev or staging" >&2; exit 2 ;; esac
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
STATE_FILE="$ROOT_DIR/.local/$ENVIRONMENT.generated.env"
if [ -n "${CI:-}" ]; then
echo "export-env: refusing to export secrets in CI; use injected variables or CI_ENV_FILE." >&2
exit 1
fi
[ -f "$ROOT_DIR/.infisical.json" ] || { echo "export-env: run 'infisical init' in this repository." >&2; exit 1; }
command -v infisical >/dev/null 2>&1 || { echo "export-env: Infisical CLI is required." >&2; exit 1; }
(cd "$ROOT_DIR" && infisical export --env="$ENVIRONMENT" --format=dotenv --silent) || {
echo "export-env: failed to export '$ENVIRONMENT'; check login and project access." >&2
exit 1
}
if [ -f "$STATE_FILE" ]; then
printf '\n'
while IFS= read -r line || [ -n "$line" ]; do
case "$line" in ''|'#'*) printf '%s\n' "$line"; continue ;; esac
key=${line%%=*}
value=${line#*=}
case "$value" in \'*\') value=${value#\'}; value=${value%\'} ;; \"*\") value=${value#\"}; value=${value%\"} ;; esac
escaped=$(printf '%s' "$value" | sed "s/'/'\\\\''/g")
printf "%s='%s'\n" "$key" "$escaped"
done < "$STATE_FILE"
fi
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
ENVIRONMENT="${1:-staging}"
[[ "$ENVIRONMENT" == dev || "$ENVIRONMENT" == staging ]] || { echo "usage: generate-convex-admin-key [dev|staging]" >&2; exit 2; }
ENV_FILE="$(mktemp)"; trap 'rm -f "$ENV_FILE"' EXIT
sh "$ROOT_DIR/scripts/export-env" "$ENVIRONMENT" > "$ENV_FILE"
docker compose --env-file "$ENV_FILE" -f "$ROOT_DIR/docker/compose.yml" exec convexmonorepo-backend ./generate_admin_key.sh
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env node
import { exportJWK, exportPKCS8, generateKeyPair } from 'jose';
const keys = await generateKeyPair('RS256', {
extractable: true,
});
const privateKey = await exportPKCS8(keys.privateKey);
const publicKey = await exportJWK(keys.publicKey);
const jwks = JSON.stringify({ keys: [{ use: 'sig', ...publicKey }] });
process.stdout.write(
`JWT_PRIVATE_KEY="${privateKey.trimEnd().replace(/\n/g, ' ')}"`,
);
process.stdout.write('\n');
process.stdout.write(`JWKS=${jwks}`);
process.stdout.write('\n');
+102
View File
@@ -0,0 +1,102 @@
import { readFile, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const usesendDir = path.join(__dirname, '..', 'node_modules', 'usesend-js');
const ensureReplacement = (content, searchValue, replaceValue, filePath) => {
if (content.includes(replaceValue)) {
return content;
}
if (!content.includes(searchValue)) {
throw new Error(`Expected snippet not found in ${filePath}`);
}
return content.replace(searchValue, replaceValue);
};
const patchFile = async (relativePath, replacements) => {
const filePath = path.join(usesendDir, relativePath);
let content = await readFile(filePath, 'utf8');
for (const [searchValue, replaceValue] of replacements) {
content = ensureReplacement(
content,
searchValue,
replaceValue,
relativePath,
);
}
await writeFile(filePath, content);
};
const patchUseSend = async () => {
const packageJsonPath = path.join(usesendDir, 'package.json');
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'));
if (packageJson.version !== '1.6.3') {
console.log(
`Skipping UseSend patch for version ${packageJson.version ?? 'unknown'}.`,
);
return;
}
const runtimeHelper = `function getNodeCrypto() {
const builtinModuleLoader = globalThis.process?.getBuiltinModule;
if (typeof builtinModuleLoader === "function") {
const nodeCrypto = builtinModuleLoader("node:crypto");
if (nodeCrypto) {
return nodeCrypto;
}
}
throw new WebhookVerificationError(
"UNSUPPORTED_RUNTIME",
"Webhook verification requires a Node.js runtime with node:crypto support"
);
}
`;
await patchFile('dist/index.mjs', [
['import { createHmac, timingSafeEqual } from "crypto";\n', ''],
[
'function computeSignature(secret, timestamp, body) {\n',
`${runtimeHelper}function computeSignature(secret, timestamp, body) {\n`,
],
[
' const hmac = createHmac("sha256", secret);\n',
' const { createHmac } = getNodeCrypto();\n const hmac = createHmac("sha256", secret);\n',
],
[
'function safeEqual(a, b) {\n',
'function safeEqual(a, b) {\n const { timingSafeEqual } = getNodeCrypto();\n',
],
]);
await patchFile('dist/index.js', [
['var import_crypto = require("crypto");\n', ''],
[
'function computeSignature(secret, timestamp, body) {\n',
`${runtimeHelper}function computeSignature(secret, timestamp, body) {\n`,
],
[
' const hmac = (0, import_crypto.createHmac)("sha256", secret);\n',
' const { createHmac } = getNodeCrypto();\n const hmac = createHmac("sha256", secret);\n',
],
[
'function safeEqual(a, b) {\n',
'function safeEqual(a, b) {\n const { timingSafeEqual } = getNodeCrypto();\n',
],
[
' return (0, import_crypto.timingSafeEqual)(aBuf, bBuf);\n',
' return timingSafeEqual(aBuf, bBuf);\n',
],
]);
console.log('Patched usesend-js 1.6.3 for Convex-compatible bundling.');
};
await patchUseSend();
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
ENVIRONMENT="${1:-staging}"
[[ "$ENVIRONMENT" == dev || "$ENVIRONMENT" == staging ]] || { echo "usage: update-convex [dev|staging]" >&2; exit 2; }
ENV_FILE="$(mktemp)"; trap 'rm -f "$ENV_FILE"' EXIT
sh "$ROOT_DIR/scripts/export-env" "$ENVIRONMENT" > "$ENV_FILE"
docker compose --env-file "$ENV_FILE" -f "$ROOT_DIR/docker/compose.yml" pull convexmonorepo-backend convexmonorepo-dashboard
docker compose --env-file "$ENV_FILE" -f "$ROOT_DIR/docker/compose.yml" up -d convexmonorepo-backend convexmonorepo-dashboard
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
ENVIRONMENT="${1:-staging}"
[[ "$ENVIRONMENT" == dev || "$ENVIRONMENT" == staging ]] || { echo "usage: update-next-app [dev|staging]" >&2; exit 2; }
ENV_FILE="$(mktemp)"; trap 'rm -f "$ENV_FILE"' EXIT
sh "$ROOT_DIR/scripts/export-env" "$ENVIRONMENT" > "$ENV_FILE"
docker compose --env-file "$ENV_FILE" -f "$ROOT_DIR/docker/compose.yml" build convexmonorepo-next
docker compose --env-file "$ENV_FILE" -f "$ROOT_DIR/docker/compose.yml" up -d convexmonorepo-next
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env sh
set -eu
if [ "$#" -lt 1 ]; then
echo "usage: with-env <dev|staging> -- <command> [args...]" >&2
exit 2
fi
ENVIRONMENT="$1"
shift
[ "${1:-}" = "--" ] && shift
[ "$#" -gt 0 ] || { echo "with-env: no command given" >&2; exit 2; }
case "$ENVIRONMENT" in dev|staging) ;; *) echo "with-env: expected dev or staging" >&2; exit 2 ;; esac
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
STATE_FILE="$ROOT_DIR/.local/$ENVIRONMENT.generated.env"
if [ -n "${CI:-}" ]; then
export WITH_ENV_SOURCE=ci WITH_ENV_ENVIRONMENT="$ENVIRONMENT" WITH_ENV_STATE_FILE="$STATE_FILE"
exec "$@"
fi
command -v infisical >/dev/null 2>&1 || {
echo "with-env: install Infisical, run 'infisical login', and link this repo with 'infisical init'." >&2
exit 1
}
[ -f "$ROOT_DIR/.infisical.json" ] || { echo "with-env: .infisical.json is missing." >&2; exit 1; }
TMP_ENV="$(mktemp "${TMPDIR:-/tmp}/convex-monorepo-$ENVIRONMENT.XXXXXX.env")"
trap 'rm -f "$TMP_ENV"' EXIT INT TERM HUP
sh "$ROOT_DIR/scripts/export-env" "$ENVIRONMENT" > "$TMP_ENV"
export WITH_ENV_SOURCE=infisical WITH_ENV_ENVIRONMENT="$ENVIRONMENT" WITH_ENV_STATE_FILE="$STATE_FILE"
set +e
bunx dotenv -e "$TMP_ENV" -- "$@"
status=$?
set -e
exit "$status"